前两篇是严重划水跑题的,这篇是交互篇,算是正片吧。
这篇分三个部分讲
交互流程不是本篇最重要的部分,直接看图
封包协议分析
https://segmentfault.com/a/1190000013298527 WebSocket 协议
https://blog.youkuaiyun.com/valada/article/details/98676216 开发者必知必会的 WebSocket 协议
我这边主要以具体示例来分析 准备工作
#客户端
var WebSocket = require('_ws@1.1.5@ws');
var ws = new WebSocket('http://127.0.0.1:19999/');
#服务端
var WebSocketServer = require('_ws@1.1.5@ws').Server;
var wss = new WebSocketServer({port: 19999});
wss.on('connection', function connection(ws) {
ws.send("1111");
});
#源码/_ws@1.1.5@ws/lib/Sender.js
Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, compressed, cb) {
console.log("this._socket.write", data,outputBuffer)
this._socket.write(outputBuffer, 'binary', cb);
};
下面是打印信息
this._socket.write <Buffer 32 34 34 34 04 00 00 00 ff ff 00> <Buffer c1 0b 32 34 34 34 04 00 00 00 ff ff 00>
前面那个是 压缩后的
1111
\color{red}{1111}
1111 后面是真正发送出去的封包数据
引用上图, <Buffer c1 0b 32 34 34 34 04 00 00 00 ff ff 00> 这个主要看 c1 0b 转成2进制 11000001 00001011
fin(1位) | 1 | 表示最后的片段, 封包结束 |
---|---|---|
rsv1(1位) | 1 | 扩展协议1. 使用zip 压缩 |
rsv2(1位) | 0 | 无扩展 |
rsv3(1位) | 0 | 无扩展 |
opcode(4位) | 0001 | 操作码 1, 文本数据 |
mask(1位) | 0 | 没有掩码 |
payload len(7位) | 000 1011 \color{red}{0001011} 0001011 | 10进制 = 11 |
payload | 32 34 34 34 04 00 00 00 ff ff 00 | 正好 11个字节 |
不使用zip压缩 , 则修改参数即可
ws.send(“1111”, { compress: false });
下面是打印信息
this._socket.write <Buffer 31 31 31 31> <Buffer 81 04 31 31 31 31>
打印的buffer 很清楚能看出来 0x31 注意是16进制, 刚好是49(ascii码) 就是1 。0x81 0x04 转成2进制 10000001 00000100
fin(1位) | 1 | 表示最后的片段 |
---|---|---|
rsv1(1位) | 0 \color{red}{0} 0 | 无扩展 这里从1改成0 |
rsv2(1位) | 0 | 无扩展 |
rsv3(1位) | 0 | 无扩展 |
opcode(4位) | 0001 | 操作码 1, 文本数据 |
mask(1位) | 0 | 没有掩码 |
payload len(7位) | 0000100 | (10进制 = 4) |
payload | “31 31 31 31” | ( 正好 4个字节) |
我们看下加入掩码之后的 buffer, 为了方便看清,不做压缩
ws.send(“1111”, { compress: false, mask: true });
下面是打印信息
this._socket.write <Buffer 31 31 31 31> <Buffer 81 84 fc d6 55 43 cd e7 64 72>
打印的buffer 变长了, 我们按照协议依次分析 0x81 0x84 转成2进制 10000001 10000100
fin(1位) | 1 | 表示最后的片段 |
---|---|---|
rsv1(1位) | 0 | 无扩展 |
rsv2(1位) | 0 | 无扩展 |
rsv3(1位) | 0 | 无扩展 |
opcode(4位) | 0001 | 操作码 1, 文本数据 |
mask(1位) | 1 \color{red}{1} 1 | 该位变成了1 |
payload len(7位) | 0000100 | (10进制 = 4)这个数据没变, 依然是4位 |
masking-key(32位) | fc d6 55 43 | 这 个 就 是 掩 码 , 通 俗 点 说 就 是 加 密 k e y \color{red}{这个就是掩码, 通俗点说就是加密key} 这个就是掩码,通俗点说就是加密key |
payload | cd e7 64 72 | ( 剩下数据的长度刚好是4个字节) |
如果有对加密过程有兴趣的, 可以阅读下源码,
b
u
f
f
e
r
u
t
i
l
\color{blue}{bufferutil}
bufferutil 和
B
u
f
f
e
r
U
t
i
l
.
f
a
l
l
b
a
c
k
\color{blue}{BufferUtil.fallback}
BufferUtil.fallback
结合上面示例 我说下
B
u
f
f
e
r
U
t
i
l
.
f
a
l
l
b
a
c
k
\color{blue}{BufferUtil.fallback}
BufferUtil.fallback 的加密过程, 因为比较简单:
首先使用 masking-key [fc d6 55 43] 转成 32位无符号长整型 0x4355d6fc , 然后 0x4355d6fc ^ 0x31313131 得到 0x7264e7cd(如果得到的值小于0, 则要取补码) 转成 buffer 即 [cd e7 64 72]
实际根据数据长度不同, 加密方式更加复杂, 有兴趣的看源码
如果发送的数据比较长,超过125个字节,下面字符串长度 181
ws.send( ‘data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAAdCAYAAAAtm2eGAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u’, {compress: false});
下面是打印信息
this._socket.write <Buffer 64 61 74 61 3a 69 6d 61 67 65 2f 70 6e 67 3b 62 61 73 65 36 34 2c 69 56 4
2 4f 52 77 30 4b 47 67 6f 41 41 41 41 4e 53 55 68 45 55 67 41 41 41 4b 41 41 … > <Buffer 81 7e 00
b5 64 61 74 61 3a 69 6d 61 67 65 2f 70 6e 67 3b 62 61 73 65 36 34 2c 69 56 42 4f 52 77 30 4b 47 67 6
f 41 41 41 41 4e 53 55 68 45 55 67 41 41 … >
我们按照协议依次分析 0x81 0x7e 转成2进制 10000001 01111110, 超过125后协议 会有扩展的 extended payload length
fin(1位) | 1 | 表示最后的片段 |
---|---|---|
rsv1(1位) | 0 | 无扩展 |
rsv2(1位) | 0 | 无扩展 |
rsv3(1位) | 0 | 无扩展 |
opcode(4位) | 0001 | 操作码 1, 文本数据 |
mask(1位) | 0 | 不加密 |
payload len(7位) | 1111110 | (10进制 = 126) |
extended payload length(16位) | 0 x 00 b 5 \color{red}{0x00b5} 0x00b5 | (10进制 = 181, 正好符合字符串长度) |
masking-key(0位) | 由于 mask是0,所以 该字段不存在 | |
payload | 64 61 74 61… | ( 打印不全, 可对比以上数据得知) |
推断下, 如果发送的数据超过 65536 也就是 extended payload length(16位) 能表示的最大长度, 则扩展 extended payload length(16位) 变成 extended payload length(64位); 其他不变。
再看下ping和pong的流程和交互吧, 有了上面的基础看这个应该是很轻松的
#客户端
var WebSocket = require('_ws@1.1.5@ws');
var ws = new WebSocket('http://127.0.0.1:19999/');
#服务端
var WebSocketServer = require('_ws@1.1.5@ws').Server;
var wss = new WebSocketServer({port: 19999});
wss.on('connection', function connection(ws) {
ws.ping();
});
#源码/_ws@1.1.5@ws/lib/Sender.js
Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, compressed, cb) {
console.log("this._socket.write", data,outputBuffer)
this._socket.write(outputBuffer, 'binary', cb);
};
代码太多,还是上图轻松愉快
跟send的区别就是少了 数据的转换, 但ping一定会有pong回复,这是程序自动处理的。 但是我并没有找到没有回复pong情况下的处理, 而我注视掉 pong的代码, 也应证了这个问题。。而程序所提供的 pong事件和ping事件的触发, 就是留给玩家自己处理这个问题, 也符合文档上的 协议 5.5.2 的注释
再看下封包
ping <Buffer 89 00> 10001001
fin(1位) | 1 | 表示最后的片段 |
---|---|---|
rsv1(1位) | 0 | 无扩展 |
rsv2(1位) | 0 | 无扩展 |
rsv3(1位) | 0 | 无扩展 |
opcode(4位) | 1001 | 操作码 9 |
pong <Buffer 8a 80 00 00 00 00> 10001010 10000000
fin(1位) | 1 | 表示最后的片段 |
---|---|---|
rsv1(1位) | 0 | 无扩展 |
rsv2(1位) | 0 | 无扩展 |
rsv3(1位) | 0 | 无扩展 |
opcode(4位) | 1010 | 操作码 10 |
mask(1位) | 1 | 加密 |
payload len(7位) | 0000000 | 数据长度0 |
masking-key(32位) | 00 00 00 00 | 由 于 没 有 数 据 所 以 补 上 k e y 就 结 束 了 \color{red}{由于没有数据所以补上key就结束了} 由于没有数据所以补上key就结束了 |
如果是客户端返回pong 强制加密, 如果是服务端就不加密, 这个搞不懂为什么要这么处理?保证数据安全性?文档上没有数要做这个
ws是一个完全忠于 websokcet的实现, 并没有过多的封装。也很好体现了 WebSocket协议的内容。