¿
本文包括如下内容:
WebSocket
协议第四章 - 连接握手WebSocket
协议第五章 - 数据帧nodejs ws
库源码分析 - 连接握手过程nodejs ws
库源码分析 - 数据帧解析过程
参考
本文对WebSocket
的概念、定义、解释和用途等基础知识不会涉及, 稍微偏干一点, 篇幅较长, markdown大约800行, 阅读需要耐心
1. 连接握手过程
关于WebSocket
有一句很常见的话: Websocket复用了HTTP的握手通道, 它具体指的是:
客户端通过HTTP请求与WebSocket服务器协商升级协议, 协议升级完成后, 后续的数据交换则遵照WebSocket协议
1.1 客户端: 申请协议升级
首先由客户端换发起协议升级请求, 根据WebSocket
协议规范, 请求头必须包含如下的内容
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
复制代码
- 请求行: 请求方法必须是GET, HTTP版本至少是1.1
- 请求必须含有Host
- 如果请求来自浏览器客户端, 必须包含Origin
- 请求必须含有Connection, 其值必须含有"Upgrade"记号
- 请求必须含有Upgrade, 其值必须含有"websocket"关键字
- 请求必须含有Sec-Websocket-Version, 其值必须是13
- 请求必须含有Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接
1.2 服务器: 响应协议升级
服务器返回的响应头必须包含如下的内容
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
复制代码
- 响应行:
HTTP/1.1 101 Switching Protocols
- 响应必须含有Upgrade, 其值为"weboscket"
- 响应必须含有Connection, 其值为"Upgrade"
- 响应必须含有Sec-Websocket-Accept, 根据请求首部的Sec-Websocket-key计算出来
1.3 Sec-WebSocket-Key/Accept的计算
规范提到:
Sec-WebSocket-Key值由一个随机生成的16字节的随机数通过base64(见RFC4648的第四章)编码得到的
例如, 随机选择的16个字节为:
// 十六进制 数字1~16
0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10
复制代码
通过base64编码后值为: AQIDBAUGBwgJCgsMDQ4PEA==
测试代码如下:
const list = Array.from({ length: 16 }, (v, index) => ++index)
const key = Buffer.from(list)
console.log(key.toString('base64'))
// AQIDBAUGBwgJCgsMDQ4PEA==
复制代码
而Sec-WebSocket-Accept
值的计算方式为:
- 将
Sec-Websocket-Key
的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接 - 通过
SHA1
计算出摘要, 并转成base64
字符串
此处不需要纠结神奇字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
, 它就是一个GUID
, 没准儿是写RFC的时候随机生成的
测试代码如下:
const crypto = require('crypto')
function hashWebSocketKey (key) {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
return crypto.createHash('sha1')
.update(key + GUID)
.digest('base64')
}
console.log(hashWebSocketKey('w4v7O6xFTi36lq3RNcgctw=='))
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
复制代码
1.4 Sec-WebSocket-Key的作用
前面简单提到他的作用为: 提供基础的防护, 减少恶意连接, 进一步阐述如下:
Key
可以避免服务器收到非法的WebSocket
连接, 比如http
请求连接到websocket
, 此时服务端可以直接拒绝Key
可以用来初步确保服务器认识ws
协议, 但也不能排除有的http服务器只处理Sec-WebSocket-Key
, 并不实现ws
协议Key
可以避免反向代理缓存- 在浏览器中发起ajax请求,
Sec-Websocket-Key
以及相关header是被禁止的, 这样可以避免客户端发送ajax请求时, 意外请求协议升级
最终需要强调的是: Sec-WebSocket-Key/Accept并不是用来保证数据的安全性, 因为其计算/转换公式都是公开的, 而且非常简单, 最主要的作用是预防一些意外的情况
2. 数据帧
WebSocket
通信的最小单位是帧, 由一个或多个帧组成一条完整的消息, 交换数据的过程中, 发送端和接收端需要做的事情如下:
- 发送端: 将消息切割成多个帧, 并发送给服务端
- 接收端: 接受消息帧, 并将关联的帧重新组装成完整的消息
数据帧格式作为核心内容, 一眼看去似乎难以理解, 但本文作者下死命令了, 必须理解, 冲冲冲
2.1 数据帧格式详解
-
FIN
: 占1bit0
表示不是消息的最后一个分片1
表示是消息的最后一个分片
-
RSV1
,RSV2
,RSV3
: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错 -
Opcode
: 占4bit%x0
: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片%x1
: 表示这是一个文本帧%x2
: 表示这是一个二进制帧%x3-7
: 保留的操作代码, 用于后续定义的非控制帧%x8
: 表示连接断开%x9
: 表示这是一个心跳请求(ping)%xA
: 表示这是一个心跳响应(pong)%xB-F
: 保留的操作代码, 用于后续定义的非控制帧
-
Mask
: 占1bit0
表示不对数据载荷进行掩码异或操作1
表示对数据载荷进行掩码异或操作
-
Payload length
: 占7或7+16或7+64bit0~125
: 数据长度等于该值126
: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度127
: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度