WebSocket 理解

HTTP 协议在设计上就是一个单向的网络协议,服务器只能被动的接收请求,然后返回相应的数据。对于需要双向通信的场景,虽然可以通过轮询,Comet 等方式实现,但每次链接都要三次握手,效率低下。

 

与http比较:

   1.都基于 TCP 的、应用层的可靠性传输协议

   2.WebSocket 在握手时的数据是通过 HTTP 传输的,一旦连接建立后就不再依赖 HTTP 了

社区开源方案

    socket.io,  ws 等

webSocket的应用场景

  • 通知: 由业务服务端发起,由客户端接收的场景,这类场景下业务通常会有兜底逻辑
  • 聊天:服务端和客户端发双向消息进行交互,用在聊天场景
  • 游戏:服务端和客户端做高频消息交互
  • 语音:从客户端持续不断产生语音包,语音包由大语音包切分而来,需要在服务端重新做组合,要求大包传输 + 顺序性保证
  • 直播:大量用户加入同一个直播间,同一直播间内的用户可发弹幕,礼物
  • ioT:边缘节点设备,如单车,共享充电宝等
  • 数据上报:从客户端持续上报数据到服务端

通信建立:

  • cloent向服务端发出一个 Upgrade: WebSocket 的协议升级 HTTP 请求,该请求附带了一个标识 Sec-WebSocket-Key; 
  • 服务端接收到协议升级请求后返回,其状态码为 101 ,表明服务端已经成功升级为 WebSocket 协议了。该信息中同样也包含了一个标识 Sec-WebSocket-Accept,该标识符是服务端根据客户端发请求中的 Sec-WebSocket-Key 值计算出来的;
  • 客户端接受到服务器的返回后,会判断服务端返回的 Sec-WebSocket-Accept 标识是否和发出 Sec-WebSocket-Key 对应,如果不是就会抛出一个 “Error during WebSocket handshake” 的错误并关闭连接。

Sec-WebSocket-Accept 值的计算:

  • Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 做字符串拼接;
  • 通过 SHA1 计算出摘要,并转成 base64 字符串。

 

上代码

 client 建立socket链接

 

...
<script>
  const host = '127.0.0.1';
  const port = 8001;
  const ws = new WebSocket(`ws://${host}:${port}`);
</script>

  

node端监听 WebSocket 升级请求,返回升级成功的数据:

server.on('upgrade', (req, socket) => {
  if (req.headers['upgrade'] !== 'websocket') {
    res.end('HTTP/1.1 400 Bad Request');
    return;
  }
  const secWsKey = req.headers['sec-websocket-key'];
  const secWsAccept = generateSecWsAccept(secWsKey);
  const responseHeaders = [
    'HTTP/1.1 101 Web Socket Protocol Handshake',
    'Upgrade: WebSocket',
    'Connection: Upgrade',
    'Sec-WebSocket-Accept: ' + secWsAccept
  ];
  socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
}

  

Sec-WebSocket-Accept值生成函数

function generateSecWsAccept (secWsKey) {
  return crypto
    .createHash('SHA1')
    .update(secWsKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
    .digest('base64');
}

 

开浏览器调试工具,可以看到 WebSocket 通信确实建立起来了。

WebSocket 理解

 

 

 

 前端发送消息

<script>
  ...
  ws.addEventListener('open', () => {
    ws.send('I am client.');
  });
</script>

服务端监听消息

server.on('upgrade', (req, socket) => {
  ...
  socket.on('data', (data) => {
    console.log(data.toString());
  })
});

数据帧解析

此时发现服务端监听打印的信息是乱码,因为这里接收的 data 并不完全等同于消息的信息,拿到的是 WebSocket 的数据帧。

WebSocket 通信的最小信息单位就是帧,一个或多个帧构成一条完成的消息。
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

 

字段名

长度.                  

说明

FIN

1bit

表示是否是信息的最后一帧

RVS

3 bit

默认都是 0

opcode

4 bit

表示帧的类型。例如文本或者二进制

MASK

1 bit

表示是否加密数据。默认为 1

Payload len

7 / 7+16 / 7+64 bit

表示数据长度(Bytes)。因为 7 位无符号整数的最大值为 127,为了能表示更大的数据长度,这里规定当这 7 位表示的值等于 126 时,后 16 bit 表示数据长度;当这 7 位表示的值等于 127 时,后 64 bit 表示数据长度

Marking-Key

32 bit

加密掩码,当 MASK 的值为 1 时存在

Payload Data

 

数据

      假定数据的长度一定是小于 126 Byte 的,忽略 Payload len 长度的各种判断
socket.on('data', (data) => {
  // Receive message
  const payloadLen = data[1] & parseInt(1111111, 2); // 假设发送的数据长度小于 125
  const maskingKey = data.slice(2, 6);
  const payloadData = new Buffer(payloadLen);
  for (let i = 0; i < payloadLen; i++) {
    let j = i % 4;
    payloadData[i] = data[6 + i] ^ maskingKey[j];
  }
  console.log(payloadData.toString());
});

  

上一篇:LG P6478 [NOI Online #2 提高组] 游戏


下一篇:NOIP2020 T1 water