WebSocket 详解

摘要:
Sec-WebSocket-Accept值的计算:将Sec-WebSocket-Key和258EAFA5-E914-47DA-95CA-C5AB0DC85B11做字符串拼接;通过SHA1计算出摘要,并转成base64字符串。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('
') + '
');
}

Sec-WebSocket-Accept值生成函数

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

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

WebSocket 详解第1张

前端发送消息

<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 ...                |
+---------------------------------------------------------------+
假定数据的长度一定是小于 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());
});

向客户端发送消息

同上,在向客户端发送消息前,需要把要发送的消息数据 包装成一个帧信息。这里为了方便我们设置 MASK 字段为 0,表示发出的数据没有进行加密。

const dataBuffer = new Buffer('I am Server.');
const payloadLen = dataBuffer.length;
const assistData = [];
assistData.push(129);			// 129: 1000 0001
assistData.push(payloadLen);
let assistBuffer = new Buffer(assistData);
let message = Buffer.concat([assistBuffer, dataBuffer]);
socket.write(message);

客户端监听

<script>
  ...
	ws.addEventListener('message', (event) => {
    console.log('message:' + event.data); // 打印 message 信息
  });
</script>

免责声明:文章转载自《WebSocket 详解》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Python什么是二次开发的意义?python在.net项目采用TFTP启动内核、设备树,NFS启动FS下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

微信小程序 tab切换内容及分页

在实际项目中,经常会遇到点击切换不同内容的情况,尤其是个人中心的订单页,还要同时实现滚动分页效果。 效果如下: <view class="tabNav"> <view wx:for="{{navTab}}" wx:key="index" data-idx="{{index}}" bindtap="currentTab" cla...

CentOS7 复制文件夹和移动文件夹

CentOS7 复制文件夹和移动文件夹 linux下文件的复制、移动与删除命令为:cp,mv,rm一、文件复制命令cp 命令格式:cp [-adfilprsu] 源文件(source) 目标文件(destination) cp [option] source1 source2 source3 ... directory 参数说明:-a:是指archive的...

KendoGrid基础

一.KendoUI Grid 绑定单击双击事件 原文:http://blog.csdn.net/sakuya_tan/article/details/51437857 <div id="grid"></div> <script> var grid = $("#grid").kendoGrid({...

Angularjs总结(三)摸态框的使用

静态页面: <input class="btn btnStyle "value="提&emsp;取"type="button"ng-click="TQZJDFG() " /> controllers中的方法: 1 $scope.TQZJDFG = function() { 2 //可以将此参数传递到所弹出的摸态框的控制器中 3...

Qt中文乱码问题(比较清楚,同一个二进制串被解释成不同的语言)

文章来源:http://blog.csdn.net/brave_heart_lxl/article/details/7186631 以下是dbzhang关于qt中文乱码问题原因的阐述,觉得不错: 首先呢,声明一下,QString 是不存在中文支持问题的,很多人遇到问题,并不是本身 QString 的问题,而是没有将自己希望的字符串正确赋给QString。...

关于JAVASCRIPT中的DATASET

DataSet是ADO.NET的中心概念。可以把DataSet当成内存中的数据库,DataSet是不依赖于数据库的独立数据集合。所谓独立,就是说,即使断开数据链路,或者关闭数据库,DataSet依然是可用的,DataSet在内部是用XML来描述数据的,由于XML是一种与平台无关、与语言无关的数据描述语言,而且可以描述复杂关系的数据,比如父子关系的数据,所以...