theme: channing-cyan
highlight: a11y-light

浅谈 websocket 协议

  1. websocket 协议是 html5 的一种全双工应用层通信协议,该协议兼容常见的浏览器,基于 TCP 传输协议,并复用 HTTP 的握手通道。它可以使客户端和服务端双向数据传输变得简单快捷,并且在 TCP 连接进行一次握手后保持长久连接,允许服务器对客户端主动推送数据。另外 websocket 也支持拓展,压缩请求头节省服务器资源和宽带资源。

websocket 相关技术简介

  1. websocket 连接的 URL 使用 ws:// 或者 wss:// 等开头,其加密、cookie 等策略和 HTTPS/HTTP 基本相同。
    websocket 与 HTTP

ws 和 wss 来进行通信协议的确定,和 HTTP 和 HTTPS 类似;
ws 表示纯文本通信,wss 表示加密通道通信(TCP + TLS);
考虑到 websocket 的其他应用场景,需要自定义协议,比如保证在非 HTTP 的情况下也可以进行数据交换。
ws 协议:普通请求,占用与 HTTP 相同的 80 端口;
wss 协议:基于 SSL 的安全传输,占用与 TLS 相同的 443 端口;

  1. HTTP 和 websocket 等应用层协议都是基于 TCP 协议来传输数据的,这些协议可以理解为是对 TCP 的封装。在 HTTP 协议下,客户端和服务器是单向的,服务器无法主动发送数据给客户端。而 websocket 是依赖于 HTTP 协议进行一次握手,以兼容浏览器的规范,在第一次 HTTP 请求后,后续就全部采用 TCP 通道进行双向通讯了。
    3.客户端 websocket 请求与相应示例
//客户端请求
GET /chat HTTP/1.1 
Host: example.com:8000 
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 
Sec-WebSocket-Version: 13
//服务器相应
HTTP/1.1 101 Switching Protocols 
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Upgrade 和 Connection 表示将请求切换到 websocket 协议,Sec-WebSocket-Key 则是浏览器随机生成的 base64 编码,结合相关的算法转化后就会成为相应体中的 Sec-WebSocket-Accept 。
4.客户端使用示例

var ws = new WebSocket('ws://www.xxx.com/some.php'); 
ws.send('xxx'); //每次只能发送字符串 
ws.onmessage = function(event) { 
    var data = event.data; 
}; 
ws.onerror = function() { 
    ws.close(); 
};

ws.send() 方法只允许发送字符串,当需要发送复杂数据时,可以结合 JSON.stringify()进行相关转化再进行数据传输。

与 websocket 类似的技术

  1. 轮询(Polling)
    前端借助于 setInterval() 等方式,不断的发送请求到服务端进行数据的更新,此方法比较简单,但需要考虑轮询时间问题,时间过长会导致用户不能及时接收数据,时间过短会导致请求次数过多,增加服务器端的负担,浪费资源。
    轮询(Polling)

  2. 长轮询(Long Polling)
    是对轮询的一种升级,客户端发出请求后,服务端用 while 等方式阻塞住请求,直到有数据才发送数据,而客户端收到相应后再发送下一个请求。
    实际上是基于 HTTP 的一种慢相应;且在数据更新频繁的情况下,效率不一定优于一般的轮询。
    长轮询(Long Polling)

  3. HTTP 流(streaming)
    使用 HTTP1.1 且响应头中包含Transfer-Encoding: chunked的情况下,服务端发送给客户端的数据可以分成多个部分,保持打开(while true,sleep 等),并周期性 flush() 分块传输。
    客户端只发送一个HTTP连接,在 xhr.readyState==3 状态下,用xhr.responseText.substring 获取每次的数据。
    但需要注意的是这种方式会存在延迟的情况,需要额外的检测进行切换到长轮询的方式,如代理服务器或防火墙等中间人攻击造成的延迟。
    HTTP 流(streaming)

    流技术机制: 流技术简单的说就是客户端的页面使用一个隐藏的窗口向服务器发送一个长链接的请求,服务器接收到请求后会不断的更新数据状态,保证连接不断和信息的时效性。这种方案需兼容不同浏览器来改进用户体验,同时如果在并发情况下发生,会对服务器造成很大压力。

    HTTP 判断流结束的两种方式:
    1、Content-Length:

    该方式适用于固定大小的数据传输中,需要在传输的数据前增加一个信息来告知对方要传输多少数据,这样在另一侧读取到这个长度的数据后就可以判断为接收已完成。
    2、使用消息 Header 字段,Transfer-Encoding:chunk
    如果要一边产生数据一边发送给客户端,服务器就需要使用Transfer-Encoding:chunk这样的方式来代替 Content-Length
    chunk 编码将数据分成一块一块的发生。Chunked 编码将使用若干个 Chunk 串连而成,由一个标明长度为0的 chunk 标示结束。每个 Chunk 分为头部和正文两部分,头部内容指定正文的字符总数(十六进制的数字)和数量单位(一般不写),正文部分就是指定长度的实际内容,两部分之间用回车换行(CRLF) 隔开。在最后一个长度为0的 Chunk 中的内容是称为 footer 的内容,是一些附加的 Header 信息(通常可以直接忽略)。

websocket 的应用场景及优势

  1. 应用场景
    1、实时性要求比较高的应用;
    2、聊天室;
    3、iot(物联网 - Internet of things);
    4、在线多人游戏;
  2. 优势
    1、高性能:
    根据测试环境数据的不同,大约会比正常的 Ajax 请求快2-10倍。HTTP 是文本协议,数据量比较大,且每次请求都会带有大量的重复请求头,传输性能较低,而 websocket 是基于二进制的协议,只在建立首次连接时用文本数据,后续请求传输的都是二进制的数据,因此性能比 Ajax 请求要高。
    2、双向通信:
    正常的 Ajax 请求如需获取实时数据,就需通过轮询等方式去获取数据,这样会浪费服务器的资源和流量。而通过 websocket 服务器可主动向前端发送消息。
    3、建立在 TCP 协议之上,服务端实现容易。且与 HTTP 协议有良好的兼容性,握手时不容易被屏蔽,可以通过各种 HTTP 代理服务器。
    4、可以发送文本和二进制数据,其中二进制数据传输可优化相关的传输性能。
    5、较少的控制开销。连接创建后,ws 客户端、服务器进行数据交换时,协议控制的数据包头部比较小。在不包含头部的情况下,服务端到客户端的包头只有2-10字节(取决于数据包的长度),客户端到服务端需要加上额外的4字节的掩码。而 HTTP 每次通信都需要携带完整的头部。

如何使用 websocket

对于前端来说,使用 websocket 还是挺简单的,因为 websocket 本身就是广播-收听模式(发布-订阅),因此前端只需要进行建立连接-监听动作-操作动作这几个步骤。

  1. 建立连接-监听动作-操作动作
    var ws = new WebSocket("ws://你的域名或ip");    
    ws.onopen = function(evt) { //用于指定连接成功后的回调函数。
        console.log("Connection open ...");    
        ws.send("Hello WebSockets!");  //用于向服务器发送数据
    };    
    ws.onmessage = function(evt) { //收到服务器返回数据后的回调函数。  
        console.log( "Received Message: " + evt.data);    
        ws.close(); 
    }; 
    ws.onclose = function(evt) { //用于连接关闭后的回调函数  
       console.log("Connection closed."); 
    };
    
  • new WebSocket("ws://你的域名或ip")后返回一个 ws 的实例,简介 ws 实例属性:

    1、ws.readyState属性返回实例对象的当前状态,共有四种,我们可以通过监听ws.readyState去判断socket的连接状态。

    CONNECTING:值为0,表示正在连接。
    OPEN:值为1,表示连接成功,可以通信了。
    CLOSING:值为2,表示连接正在关闭。
    CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

    2、ws.onerror 用于连接报错时的回调函数。一般连接报错,我们只需要执行重新连接就好了。

        ws.onerror = function(event) {    
            // handle error event 
            ......
        };
    

2、服务端(Node)

node中,使用最广泛的是通过ws模块来创建websocket服务,使用前需要先安装这个模块:

const express = require('express');
const SocketServer = require('ws').Server;
const port = 3000;
const server = express().listen(port, () => {
    console.log(`listening to ${port}`);
})
const wss = new SocketServer({server});
wss.on('connection', (ws) => {
    console.log('Client connected.');
    ws.on('message', (data) => {
        console.log(data);
        //服务端发送数据到客户端
        ws.send(data);
    })
    ws.on('close', () => {
        console.log('Close connected.')
    })
})

对接 websocket 时的常见问题

1、WebSocket 心跳及重连机制:
WebSocket 是前后端交互的长连接,但是会存在一些特殊情况导致连接失效且相互之间没有反馈提醒,因此为了保证连接的可持续性和稳定性就产生了 WebSocket 心跳重连机制。
原生 WebSocket 服务原因导致 WebSocket 断开不会触发 WebSocket 任何事件,前端无法得知当前连接是否断开,当调用 WebSocket.send方法浏览器才发现连接断开,然后触发 onclose 函数。
后端 WebSocket 服务也可能出现异常,造成连接断开,前端也没有收到消息,因此需要前端定时发送心跳消息 ping,后端收到消息后立刻返回 pong 消息,告知连接正常,当一定时间没有 pong 消息,前端会执行重连等操作。
2、心跳检测及重连思路:

1、页面初始化,创建 WebSocket:

function createWebSocket() {
  try {
    ws = new WebSocket(wsUrl);
    init();
  } catch(e) {
    console.log('catch');
    reconnect(wsUrl);
  }
}

2、初始化相关监听事件:
当网络断开的时候,会先调用 onerror,onclose 事件可以监听到,会调用 reconnect 方法进行重连操作。正常的情况下,是先调用 onopen 方法的,当接收到数据时,会被 onmessage 事件监听到。

function init() {
  ws.onclose = function () {
    console.log('链接关闭');
    reconnect(wsUrl);
  };
  ws.onerror = function() {
    console.log('发生异常了');
    reconnect(wsUrl);
  };
  ws.onopen = function () {
    //心跳检测重置
    heartCheck.start();
  };
  ws.onmessage = function (event) {
    //拿到任何消息都说明当前连接是正常的
    console.log('接收到消息');
    heartCheck.start();
  }
}

3、重连操作 reconnect:
如果网络断开的话,会执行 reconnect 方法,使用了一个定时器,4秒后会重新创建一个新的 websocket 链接,重新调用 createWebSocket 函数,重新会执行及发送数据给服务器端。

var lockReconnect = false;//避免重复连接
function reconnect(url) {
  if(lockReconnect) {
    return;
  };
  lockReconnect = true;
  //没连接上会一直重连,设置延迟避免请求过多
  tt && clearTimeout(tt);
  tt = setTimeout(function () {
    createWebSocket(url);
    lockReconnect = false;
  }, 4000);
}

4、心跳检测:
每隔一段固定的时间,向服务器端发送一个 ping 数据,如果在正常的情况下,服务器会返回一个 pong 给客户端,如果客户端通过 onmessage 事件能监听到的话,说明请求正常,重新心跳检测

//心跳检测
var heartCheck = {
  timeout: 3000,
  timeoutObj: null,
  serverTimeoutObj: null,
  start: function () {
    console.log('start');
    var self = this;
    this.timeoutObj && clearTimeout(this.timeoutObj);
    this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
    this.timeoutObj = setTimeout(function () {
      //这里发送一个心跳,后端收到后,返回一个心跳消息,
      //onmessage拿到返回的心跳就说明连接正常
      console.log('55555');
      ws.send("123456789");
      self.serverTimeoutObj = setTimeout(function () {
        console.log('reOpen',ws);
        ws.close();
        // createWebSocket();
      }, self.timeout);

    }, this.timeout)
  }
}