本文主要介绍 webSocket(下文简写为 ws),并使用 node 原生实现基本功能,难点主要是解析和组装数据。须要的知识点:javascript
首先咱们看看 ws 数据帧格式:html
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 ... |
+---------------------------------------------------------------+
复制代码
要理解 ws 就离不开上面这个图,可是对数据帧不熟悉的,会彻底搞不懂这个图是表达的啥意思。因此咱们先解释下这个图是干吗的,咱们应该看。java
有上面这两个概念再看上面的图:node
第一行(占 32 位)git
位
,在这个位上可能值就只能是 0 或者 1opcode(4)
这里表示数据操做码,占据 4 位,取值返回是:0000-1111,注意是二进制MASK
掩码标识,占 1 位,payload len(7)
,接受到的数据长度,占 7 位。Extended payload length(16/54)...
第一行的最后一格,占 8 位这里的数据含义会有变化,稍后详说。第二行(占 32 位)github
Extended payload length continued, if payload len == 127
扩展数据长度,这里为何要分行呢?web
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) | Extended payload length continued, |
|N|V|V|V| | | | | if payload len == 127 |
| | | | | |S| | (if payload len==126/127) | |
| |1|2|3| |K| | | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +------------------------------------+
复制代码
因此后面几行都是能够以此拼接到后面。算法
若是客户端(浏览器)要发送一个hello
给服务器,咱们服务端收到的数据实际上是一个二进制数据一系列的 0 或者 1,就像这样10001000111...
,咱们要知道到底发给咱们的是啥,就须要对这一些列的 0/1 作解析,上面的图就解析这系列 0/1 的规则,咱们按照上面的规则一步步解析就能获得咱们想要的数据。api
举个例子:浏览器
假如收到客户端发来的数据
10000001
(这里只是截取数据开始的一部分(第一个字节),后面还有不少),对应的值以下:
FIN | RSV1 | RSV2 | RSV3 | opcode |
---|---|---|---|---|
1 | 0 | 0 | 0 | 0001 |
表示这是一个消息的最后的一帧。第一个帧也多是最后一个。
%x0 : 还有后续帧
%x1 : 最后一帧
除非一个扩展通过协商赋予了非零值以某种含义,不然必须为0 若是没有定义非零值,而且收到了非零的RSV,则websocket连接会失败
解释说明 “Payload data” 的用途/功能 若是收到了未知的opcode,最后会断开连接 定义了如下几个opcode值: %x0 : 表明连续的帧 %x1 : text帧 %x2 : binary帧 %x3-7 : 为非控制帧而预留的 %x8 : 关闭握手帧 %x9 : ping帧 %xA : pong帧 %xB-F : 为非控制帧而预留的
定义“payload data”(实际提交的数据)是否被添加掩码若是置1, “Masking-key”就会被赋值全部从客户端发往服务器的帧都会被置1
若是是0~125,它就是“payload length”(收到数据的长度,好比收到的是
hello
,那么就是5), 若是是126,紧随其后的被表示为16 bits无符号整型就是“payload length”, 若是是127,紧随其后的被表示为64 bits无符号整型就是“payload length”
payload length
只有7位,二级制最大是1111111
转换为十进制就是127
,若是“payload length”大于127
了,就无法正确的表示。咱们须要更多的位来表示“payload length”,因此咱们在Payload length
后面用另外的位来表示。那直接定义一个64位来表示不就好了么?虽然这样能行,可是也得考虑到性能问题,如上面说的hello
长度只有“5”,转换为二进制是101
,三位就能够了,若是用64位就有点太浪费了。因此分别定义了这三种状况。全部从客户端发送到服务器的帧都包含一个32 bits的掩码(若是“mask bit”被设置成1),不然为0 bit。一旦掩码被设置,全部接收到的payload data都必须与该值以一种算法作异或运算来获取真实值。
它是"Extension data"和"Application data"的总和,通常扩展数据为空。
除非扩展被定义,不然就是0,任何扩展必须指定其Extension data的长度
占据"Extension data"以后的剩余帧的空间
知道了帧结构和含义,接下来就能够按照规则解析数据
function parseFrams() {
// buffer接受到的数据
const buffer = this.buffer;
// 数据默认从第三个字节开始,默认数据长度小于125
let payloadIndex = 2;
// 获取第字节,包含FIN和操做码(opcode)
const byte1 = buffer.readUInt8(0);
// 0:还有后续帧
// 1:最后一帧
const FIN = (byte1 >>> 7) & 0x1;
// 获取操做码,后面会根据操做码处理数据
const opcode = byte1 & 0x0f;
if (!FIN) {
// 不是最后一帧须要暂存当前的操做码,协议要求:
// 必需要暂存第一帧的操做码
// 分片编号 0 1 ... N-2 N-1
// FIN 0 0 ... 0 1
// opcode !0 0 ... 0 0
this.frameOpcode = opcode;
}
// 获取掩码(MASK)和数据长度(payload length)
let byte2 = buffer.readUInt8(1);
// 定义“payload data”是否被添加掩码
// 若是置1, “Masking-key”就会被赋值
// 全部从客户端发往服务器的帧都会被置1
let MASK = (byte2 >>> 7) & 0x1;
// 获取数据长度
let payloadLength = byte2 & 0x7f;
let mask_key;
if (payloadLength === 126) {
// 大于126小于65536,那么后面字节表示的是数据的长度,那么真实的数据就会后移两字节
payloadLength = buffer.readUInt16BE(payloadIndex);
// 真实数据后移2位
payloadIndex += 2;
} else if (payloadLength === 127) {
// 大于等于65536,那么后面字节表示的是数据的长度,数据最长为64位,可是数据太大就很差处理了,这里限制最大为32位
// 因此第2-6字节的数据始终应该为0,真实数据的长度在6-10字节
// 4:2-6字节的位置
payloadLength = buffer.readUInt32BE(payloadIndex + 4);
// 8:数据长度占据了8字节,真实数据就须要后移8字节
payloadIndex += 8;
}
// 若是MASK位被置为1那么Mask_key将占据4位 MASK_KEY_LENGTH===4
const maskKeyLen = MASK ? MASK_KEY_LENGTH : 0;
// 若是当前接受到的数据长度小于发送的数据总长度加上协议头部的数据长度,表示数据没有接受完,暂不处理,须要等到全部数据都接受到后再处理
if (buffer.length < payloadIndex + maskKeyLen + payloadLength) {
return;
}
// 若是有掩码,那么在真实数据以前会有四字节的掩码key(Masking-key)
let payload = Buffer.alloc(0);
if (MASK) {
// 获取掩码
mask_key = buffer.slice(payloadIndex, payloadIndex + MASK_KEY_LENGTH);
// 真实数据再次后移4位
payloadIndex += MASK_KEY_LENGTH;
// 有掩码须要解码,解码算法是规定死的,可见文后源码
payload = unmask(mask_key, buffer.slice(payloadIndex));
} else {
// 没有掩码就直接截取数据
payload = buffer.slice(payloadIndex);
}
// 多是分片传输,须要缓存数据帧,等待全部帧接受完毕后再处理完整数据
this.payloadFrames = Buffer.concat([this.payloadFrames, payload]);
this.buffer = Buffer.alloc(0);
// 数据接受完毕
if (FIN) {
const _opcode = opcode || this.frameOpcode;
const payloadFrames = this.payloadFrames.slice(0);
this.payloadFrames = Buffer.alloc(0);
this.frameOpcode = 0;
// 根据不一样opcode处理成不一样的数据
this.processPayload(_opcode, payloadFrames);
}
}
复制代码
/** * * @param {number} opcode * @param {string|buffer} payload * @param {boolean} isFinal */
function encodeMessage(opcode, payload, isFinal = true) {
const len = payload.length;
let buffer;
let byte1 = (isFinal ? 0x80 : 0x00) | opcode;
if (len < 126) {
// 数据长度0~125
// 构建返回数据容器
buffer = Buffer.alloc(2 + len); // 2:[FIN+RSV1/2/3+OPCODE](占1bytes) + [MASK+payload length](占1bytes)
// 写入FIN+RSV1/2/3+OPCODE
buffer.writeUInt8(byte1);
// 从第二字节写入MASK+payload length
buffer.writeUInt8(len, 1);
// 从第三字节写入真实数据
payload.copy(buffer, 2);
} else if (len < 1 << 16) {
// 数据长度126~65535
buffer.Buffer.alloc(2 + 2 + len);
buffer.writeUInt8(byte1);
buffer.writeUInt8(126, 1);
buffer.writeUInt16(len, 2);
payload.copy(buffer, 4);
} else {
// 数据长度65536~..
buffer.Buffer.alloc(2 + 8 + len);
buffer.writeUInt8(byte1);
buffer.writeUInt8(127, 1);
buffer.writeUInt32(0, 2);
buffer.writeUInt32(len, 6);
payload.copy(buffer, 10);
}
return buffer;
}
复制代码
上面两段代码都有很详细的注释,应该能看懂,就再也不具体的解析,实现源码见github