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

简单了解一下 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:\ 就行了?那试试呗。

改好了,报错啦!!!

WebSocket 结合 Nginx 实现域名及 WSS 协议访问 区分每个客户端_第1张图片

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 状态码:

WebSocket 结合 Nginx 实现域名及 WSS 协议访问 区分每个客户端_第2张图片

这样就完成了,在 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.bootgroupId>

<artifactId>spring-boot-starter-websocketartifactId>

dependency>

配置文件

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
 
  @Bean标注在方法上(返回某个实例的方法),等价于spring的xml配置文件中的 
   
  ,作用为:注册bean对象 
  

@Configuration标注在类上,相当于把该类作为spring的xml配置文件中的,作用为:配置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 webSocketSet = new ConcurrentHashMap();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    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 map,WebSocketServer value){
        String key="";
        for (Map.Entry 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 协议访问

 
  

 
  




你可能感兴趣的:(WebSocket 结合 Nginx 实现域名及 WSS 协议访问 区分每个客户端)