阿里云优惠活动,点击链接进行购买: 一年仅需96.9元即可以购买服务器~
腾讯云优惠活动, 点击链接进行购买一年仅需99元
腾讯云限时开团活动, 点击链接进行购买一年仅需95元
项目地址: https://github.com/hua1995116/websocket-ws
1 kb = 1024 byte
1 byte = 8 bit
前面已经说过了 WebSocket 在客户端与服务端的“Hand-Shaking”实现,所以这里讲数据传输。 WebSocket 传输的数据都是以 Frame(帧)的形式实现的,就像 TCP/UDP 协议中的报文段 Segment。下面就是一个 Frame:(以 bit 为单位表示)
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 ... |
+---------------------------------------------------------------+
表示此帧是否是消息的最后帧。第一帧也可能是最后帧。
必须是0,除非协商了扩展定义了非0的意义。如果接收到非0,且没有协商扩展定义 此值的意义,接收端必须使WebSocket连接失败。
定义了"Payloaddata"的解释。如果接收到未知的操作码,接收端必须使WebSocket 连接失败。下面的值是定义了的。
%x0 表示一个后续帧
%x1 表示一个文本帧
%x2 表示一个二进制帧
%x3-7 为以后的非控制帧保留
%x8 表示一个连接关闭
%x9 表示一个ping
%xA 表示一个pong
%xB-F 为以后的控制帧保留
定义了"Payload data"是否标记了。如果设为1,必须有标记键出现在masking-key,用 来unmask "payload data",见5.3节。所有从客户端发往服务器的帧必须把此位设为1。
"Payloaddata"的长度,字节单位。如果值是0-125,则是有效载荷长度。如果是126, 接下来的2字节解释为16位无符号整数,作为有效载荷长度。如果127,接下来的8 字节解释为64位无符号整数(最高位必须是0),作为有效载荷长度。多字节长度数值 以网络字节序表示。注意,在任何情况下,必须用最小数量的字节来编码长度,例如, 124字节 长的字符串不能编码为序列126, 0, 124。有效载荷长度是"Extension data"的长 度加上"Application data"的长度。"Extension data"的长度可能是0,在这种情况下, 有效载荷长度是"Applicationdata"的长度。
所有从客户端发往服务器的帧必须用32位值标记,此值在帧里。如果mask位设为1, 此字段(32位值)出现,否则缺失。更多的信息在5.3节,客户端到服务器标记。
"Payloaddata" 定义为"extensiondata" 后接"application data"。
"Extensiondata"是0字节,除非协商了扩张。所有扩张必须指定"extensiondata"的长度, 或者如何计算长度,如何使用扩展必须在打开握手时进行协商。如果有,"Extension data"包括在有效载荷长度。
任意"Applicationdata"占据了帧的剩余部分,在"Extensiondata"之后。 "Applicationdata"的长度等于有效载荷长度减去"Extensiondata"的长度。
示意图

因为 JavaScript 语言没有用于读取或操作二进制数据流的机制,而是用 buffer 来进行代替操作。
首先我们先来解析出前 8 个字节。占比情况如下(单位 bit):
FIN(1),RSV1(1),RSV2(1),RSV3(1),Opcode(4)
我们先来获取出 buffer 中的第一个字节,buffer 可以理解为一个类似的数组,每一个值代表一个字节,并且以 16 进制存储。而一个字节等于八个 bit。
例如读出第一个 buffer 值为 7b, 如果将他转化为 10 进制为 123, 转化为 2 进制为 01111011, 因此我们肉眼可以看出, FIN = 0, RSV1 = 1, RSV2 = 1, RSV3 = 1, Opcode = 1011 (2 进制表示)
但是我们在代码中无法使用这样的方式来查看,因此我们需要用到位移(>>)符号和按位与(&)符号。
r_queue; // buffer
var FIN = r_queue[0] >> 7;
var Opcode = r_queue[0] & 0x0f;
0x0F 转化为 2 进制为 00001111,获取后 4 位。
有了上面的描述相信你对如何解析数据有了初步的了解, 继续来解析第二个字节, 占比情况(单位 bit ):
Mask(1), Payload length(7)
r_queue; // buffer
var MASK = r_queue[1] >> 7;
var Payload_len = r_queue[1] & 0x7f;
0x7F 转化为 2 进制为 01111111, 用来获取后 7 位。
如果 Payload_len 为 125, 则 Payload_len 长度就到此为止,如果 Payload_len 为 126, 则后面的 2 个字节 来代表长度, 可取范围为 0 - 65535。 如果 Payload_len 为 127,则后面 8 个字节 用来标识长度,可取范围为 0 - 2 ^ 64 -1 (1kb = 2 ^ 13 bit), 可以想象这个数值还是非常大的,基本可以认为 websocket 的存储数据量基本上没什么限制。
总结:
Payload_len = 125, Payload_len = 4bit
Payload_len = 126, Payload_len = 4 + 16 bit
Payload_len = 125, Payload_len = 4 + 64 4bit
Masking-key(0/4 字节)
如果前面获取的 mask = 1, 则在 Payload_len 后会额外占据 4 个字节。
Payload data 情况 1: mask = 0,直接将获取剩余的 buffer,转化为 string,即为我们传输的数据。 情况 2: mask = 1, 需要根据以下公式来进行额外计算。 https://tools.ietf.org/html/rfc6455#page-32
Octet i of the transformed data ("transformed-octet-i") is the XOR of
octet i of the original data ("original-octet-i") with octet at index
i modulo 4 of the masking key ("masking-key-octet-j"):
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
The payload length, indicated in the framing as frame-payload-length,
does NOT include the length of the masking key. It is the length of
the "Payload data", e.g., the number of bytes following the masking
key.
公式:当前字节[i]的实际值 = 当前数据[i] ^(异或) masking-key[j] (j = i % 4)
有了解析的过程,生成的过程就非常简单了。下面只考虑最简单的情况。不考虑掩码的情况, 只考虑长度小于 125, 126 的情况。
const json = JSON.stringify(data);
// 获取 buffer 长度
const jsonByteLength = Buffer.byteLength(json);
// 判断长度
const lengthByteCount = jsonByteLength < 126 ? 0 : 2;
const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;
// 构造 buffer 长度 = 前面两个字节 + 后面的第三部分
const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);
// 写入第一个字节
buffer.writeUInt8(0b10000001, 0);
// 写入第二个字节
buffer.writeUInt8(payloadLength, 1);
let payloadOffset = 2;
// 如果长度大于 126
if (lengthByteCount > 0) {
// 偏移 2个字节写入
buffer.writeUInt16BE(jsonByteLength, 2);
payloadOffset += lengthByteCount;
}
// 写入数据
buffer.write(json, payloadOffset);