您的位置:首页 > 运维架构 > Nginx

WebSocket 结合 Nginx 实现域名及 WSS 协议访问 区分每个客户端

2018-01-22 11:45 573 查看


简单了解一下 WebSocket

现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

以上信息摘自维基百科( https://zh.wikipedia.org/wiki/WebSocket )

简单点说,WebSocket 就是减小客户端与服务器端建立连接的次数,减小系统资源开销,只需要一次 HTTP 握手,整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直与客户端保持连接,直到你关闭请求,同时由原本的客户端主动询问,转换为服务器有信息的时候推送。当然,它还能做实时通信、更好的二进制支持、支持扩展、更好的压缩效果等这些优点。

推荐一个知乎上叫 Ovear 的网友关于 WebSocket 原理的回答,嘻哈风格科普文,简直不要更赞了!地址: https://www.zhihu.com/question/20215561/answer/40316953


ws 和 wss 又是什么鬼?

Websocket使用 
ws
 或 
wss
 的统一资源标志符,类似于 
HTTP
 或 
HTTPS
 ,其中 
wss
 表示在
TLS 之上的 Websocket ,相当于 HTTPS 了。如:
ws://example.com/chat
wss://example.com/chat


默认情况下,Websocket 的 ws 协议使用 80 端口;运行在TLS之上时,wss 协议默认使用 443 端口。其实说白了,wss 就是 ws 基于 SSL 的安全传输,与 HTTPS 一样样的道理。

如果你的网站是 HTTPS 协议的,那你就不能使用 
ws://
 了,浏览器会
block 掉连接,和 HTTPS 下不允许 HTTP 请求一样,如下图:




Mixed Content: The page at 'https://domain.com/' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://x.x.x.x:xxxx/'. This request has been blocked; this endpoint must be available over WSS.


这种情况,毫无疑问我们就需要使用 
wss:\
 安全协议了,我们是不是简单的把 
ws:\ 改为 wss:\
 就行了?那试试呗。

改好了,报错啦!!!




VM512:35 WebSocket connection to 'wss://IP地址:端口号/websocket' failed: Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR


很明显 SSL 协议错误,说明就是证书问题了。记着,这时候我们一直拿的是 
IP地址
+ 端口号
 这种方式连接 WebSocket 的,这根本就没有证书存在好么,况且生成环境你也要用 
IP地址
+ 端口号
 这种方式连接 WebSocket 吗?肯定不行阿,要用域名方式连接 WebSocket 阿。


Nginx 配置域名支持 WSS

不用废话,直接在配置 HTTPS 域名位置加入如下配置:
location /websocket {
proxy_pass http://backend; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}


接着拿域名再次连接试一下,不出意外会看 101 状态码:




这样就完成了,在 HTTPPS 下以域名方式连接 WebSocket ,可以开心的玩耍了。

稍微解释一下 Nginx 配置

Nginx 自从 1.3 版本就开始支持 WebSocket 了,并且可以为 WebSocket 应用程序做反向代理和负载均衡。

WebSocket 和 HTTP 协议不同,但是 WebSocket 中的握手和 HTTP 中的握手兼容,它使用 HTTP 中的 Upgrade 协议头将连接从 HTTP 升级到 WebSocket,当客户端发过来一个 
Connection:
Upgrade
 请求头时,Nginx 是不知道的,所以,当 Nginx 代理服务器拦截到一个客户端发来的 
Upgrade
 请求时,需要显式来设置 
Connection
 、 
Upgrade
 头信息,并使用
101(交换协议)返回响应,在客户端和代理服务器、后端服务器之间建立隧道来支持 WebSocket。

当然,还需要注意一下,WebSockets 仍然受到 Nginx 缺省为60秒的 proxy_read_timeout 的影响。这意味着,如果你有一个程序使用了 WebSockets,但又可能超过60秒不发送任何数据的话,那你要么需要增加超时时间,要么实现一个 ping 的消息以保持联系。使用 ping 的解决方法有额外的好处,可以发现连接是否被意外关闭。

更具体文档详见 Nginx 官方文档: http://nginx.org/en/docs/http/websocket.html

小伙伴的需求可能还需要区分每一个websocket链接,在这我提供一下我实现的方法(基于spring boot)

spring boot的 websocket的maven依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-websocket</artifactId>

</dependency>
配置文件

@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}


@Bean标注在方法上(返回某个实例的方法),等价于spring的xml配置文件中的
<bean>
,作用为:注册bean对象

@Configuration标注在类上,相当于把该类作为spring的xml配置文件中的
<beans>
,作用为:配置spring容器(应用上下文)
WebSocketServer代码

package com.zity.xx.xx.controller.websocket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Copyright (C) 2012-2018 天津紫藤科技有限公司. Co. Ltd. All Rights Reserved.
*
* @author sun
* @version v1
* @description 请在此处填写该类的描述
* @module
* @date 17/8/15
*/
@ServerEndpoint(value = "/websocket/{telephone}")
@Component
public class WebSocketServer {

public static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);

//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;

//concurrent包的线程安全ConcurrentHashMap,手机号为key用来存放每个客户端对应的WebSocketServer对象。
private static Map<String,WebSocketServer> webSocketSet = new ConcurrentHashMap<String, WebSocketServer>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(@PathParam("telephone")String telephone, Session session) {
log.info("telephone:{},session:{}",telephone,session.getId());
if(!webSocketSet.containsKey(telephone)){
addOnlineCount();           //在线数加1
log.info("有新连接加入!当前在线人数为" + getOnlineCount());
}
this.session = session;
webSocketSet.put(telephone,this);
sendMessage(telephone + "创建链接");
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
String key = getKey(webSocketSet,this);
if(!StringUtils.isEmpty(key)){
log.info("key:{}", key);
webSocketSet.remove(key);
subOnlineCount();           //在线数减1
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("来自客户端的消息:" + message);
}

@OnError
public void onError(Session session, Throwable error) {
log.info("非正常关闭,发生错误!====>" + error.toString() + "当前在线人数为" + getOnlineCount());
}

/**
* 发送消息
*
* @param message 消息
*/
public void sendMessage(String message){
try {
//判断session链接是否存在
if(this.session.isOpen() == true){
this.session.getBasicRemote().sendText(message);
}else {
log.info("该连接已经断开!");
}
} catch (IOException e) {
e.printStackTrace();
}
}

public void onFailure(Throwable t){
log.info(t.toString());
}
/**
* 自定义消息
*/
public static void sendOneInfo(String id,String message){
WebSocketServer socketServer = webSocketSet.get(id);
//判当前webSocket是否存在
if(socketServer != null){
socketServer.sendMessage(message);
}else {
log.info("该--" + id + "--WebSocket已经断开连接!");
}
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}

public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}

private static String getKey(Map<String,WebSocketServer> map,WebSocketServer value){
String key="";
for (Map.Entry<String, WebSocketServer> entry : map.entrySet()) {
if(value.equals(entry.getValue())){
key=entry.getKey();
}
}
return key;
}
}

js代码块


$(function () {

var websocket = null;

//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("服务器地址/websocket/手机号");
}
else {
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function () {
};

//连接成功建立的回调方法
websocket.onopen = function (event) {
}

//接收到消息的回调方法
websocket.onmessage = function (e) {
//需要实时显示的内容,在此操作
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
websocket.close();
}

//关闭连接
function closeWebSocket() {
websocket.close();
}
})

希望能给大家提供帮助,转载部分WebSocket 结合 Nginx 实现域名及 WSS 协议访问





                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: