WebSocket原理与实践(四)
从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的。我们自己需要去生成数据帧,解析数据帧的时候我们需要分片。
消息分片:
有时候数据需要分成多个数据包发送,需要使用到分片,也就是说多个数据帧来传输一个数据。比如将大数据分成多个数据包传输,分片的目的是允许发送未知长度的消息。
这样做的好处是:
1. 大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不够的情况。
2. 和http的chunk一样,可以边生成数据边传递消息,可以提高传输效率。
如果大数据不能被碎片化,那么一端就必须将消息整个载入内存缓冲之中,然后需要计算长度等操作并发送,但是有了碎片化机制,服务器端或者中间件就可以选取适用的内存缓冲长度,然后当缓冲满了之后就发送一个消息碎片。
分片规则:
1. 如果一个消息不分片的话,那么该消息只有一帧(FIN为1,opcode非0);
2. 如果一个消息分片的话,它的构成是由起始帧(FIN为0,opcode非0),然后若干(0个或多个)帧(FIN为0,opcode为0),然后结束帧(FIN为1,opcode为0)。
注意:
1. 当前已经定义了控制帧包括 0x8(close), 0x9(Ping), 0xA(Pong). 控制帧可以出现在分片消息中间,但是控制帧不允许分片,控制帧是通过它的opcode
的最高有效位是1去确定的。
2. 组成消息的所有帧都是相同的数据类型,在第一帧中的opcode中指明。组成消息的碎片类型必须是文本,二进制,或者其他的保留类型。
下面我们来理解下上面分片规则2中的话的含义:
1. 开始帧(1个)---消息分片起始帧的构成是 (FIN为0,opcode非0);即:FIN=0, Opcode > 0;
2. 传输帧(0个或多个)---是由若干个(0个或多个)帧组成; 即 FIN = 0, Opcode = 0;
3. 终止帧(1个)--- FIN = 1, Opcode = 0;
还是看基本帧协议如下:
1 2 31 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 ... | +---------------------------------------------------------------+
demo解析:
比如我们现在第三节我们讲到的 "解析数据帧" 里面的代码,我们发送的消息123456789后,返回的数据部分是:
<Buffer 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89>{ FIN: 1, Opcode: 1, Mask: 1, PayloadLength: '123456789', MaskingKey: [ 176, 35, 82, 90 ] }
上面返回的数据部分是16进制,因此我们需要他们转换成二进制,有关16进制,10进制,2进制的转换表如下:
16进制-->10进制-->2进制转换查看
我们现在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 这些16进制先转换成10进制,然后转换成二进制,分析代码如下:
16进制(a=10, b=11, ... 依次类推)
16进制 10进制 2进制81 8*16的1次方 + 1*16的0次方 = 129 1000000189 8*16的1次方 + 9*16的0次方 = 137 10001001b0 11*16的1次方 + 0*16的0次方 = 176 10110000 23 2*16的1次方 + 3*16的0次方 = 35 0010001152 5*16的1次方 + 2*16的0次方 = 82 010100105a 5*16的1次方 + 10*16的0次方 = 90 0101101081 8*16的1次方 + 1*16的0次方 = 129 1000000111 1*16的1次方 + 1*16的0次方 = 17 0001000161 6*16的1次方 + 1*16的0次方 = 97 001111016e 6*16的1次方 + 14*16的0次方 = 110 0110111085 8*16的1次方 + 5*16的0次方 = 133 1000010115 1*16的1次方 + 5*16的0次方 = 21 0001010165 6*16的1次方 + 5*16的0次方 = 101 0110010162 6*16的1次方 + 2*16的0次方 = 98 0110001089 8*16的1次方 + 9*16的0次方 = 137 10001001
我们把上面的转换后的二进制 对照上面的 基本帧协议表看下:
1. 先看 FIN 的含义是: 第一位是否为消息的最后一个数据帧,如果为1的话,说明是,否则为0的话就不是,那说明是最后一个数据帧。
2. 第2~4位都为0,对应的RSV(1~3), 5~8为 0001,是属于opcode的部分了,opcode是代表是帧的类型;它有如下类型:
0x0 表示附加数据帧
0x1 表示文本数据帧
0x2 表示二进制数据帧
0x3-7 暂时无定义,为以后的非控制帧保留
0x8 表示连接关闭
0x9 表示ping
0xA 表示pong
0xB-F 暂时无定义,为以后的控制帧保留
注意:其中8进制是以0开头的,16进制是以0x开头的。
0001,是文本数据帧了。
3. 第九位是1,那么对应的帧协议表就是MASK部分了,Mask(占1位): 表示是否经过掩码处理, 1 是经过掩码的,0是没有经过掩码的。说明是经过掩码处理的,
也就是说可以理解为是客户端向服务器端发送数据的。(因为服务器端给客户端是不需要掩码的,否则连接中断)。
4. 第10~16位是 0001001 = 9 < 125, 对应帧协议中的 payload length的部分了,数据长度为9,因此小于125位,因此使用7位来表示实际数据长度。
5. b0, 23, 52, 5a 对应的部分是 属于Masking-key(0或者4个字节),该区块用于存储掩码密钥,只有在第二个子节中的mask为1,也就是消息进行了掩码处理时才有。
6. 81 11 61 6e 85 15 65 62 89 这些就是对应表中的数据部分了。
下面我们再来理解下 消息 123456789 怎么通过掩码加密成 81 11 61 6e 85 15 65 62 89 这些数据了。
数字字符1的ASCLL码的16进制为31,转换成10进制就是49了。其他的数字依次类推+1;
数字 10进制 二进制1 49 001100012 50 001100103 51 001100114 52 001101005 53 001101016 54 001101107 55 001101118 56 001110009 57 00111001
6-1: 其中字符1的二进制位 00110001,掩码b0的二进制位 10110000, 因此:
00110001
10110000
进行交配的话,二进制就变成:10000001,转换成10进制为 129了,那么转换成16进制就是 81了。
6-2:字符2的二进制位 00110010,掩码23的二进制位 00100011,因此:
00110010
00100011
进行交配的话,二进制就变成 00010001,转换10进制为17,那么转换成16进制就是 11了。
6-3: 字符3的二进制位 00110011,掩码52的二进制位 01010010,因此:
00110011
01010010
进行交配的话,二进制就变成:01100001,转换成10进制为 97,那么转换成16进制就是 61了。
6-4: 字符4的二进制位 00110100,掩码 5a 的二进制位 01011010,因此:
00110100
01011010
进行交配的话,二进制就变成 01101110,转换成10进制为 110,那么转换成16进制为 6e.
6-5: 字符5的二进制位 00110101,掩码b0的二进制位 10110000, 因此:
00110101
10110000
进行交配的话,二进制就变成:10000101,转换成10进制为 133,那么转换成16进制就是 85了。
6-6: 字符6的二进制位 00110110,掩码23的二进制位 00100011,因此:
00110110
00100011
进行交配的话,二进制就变成:00010101,转换成10进制为 21,那么转换成16进制就是 15了。
6-7: 字符7的二进制位 00110111,掩码52的二进制位 01010010,因此:
00110111
01010010
进行交配的话,二进制就变成:01100101,转换成10进制为 101,那么转换成16进制就是 65了。
6-8: 字符8的二进制位 00111000,掩码 5a 的二进制位 01011010,因此:
00111000
01011010
进行交配的话,二进制就变成:01100010,转换成10进制为 98,那么转换成16进制就是 62了。
6-9: 字符9的二进制位 00111001,掩码b0的二进制位 10110000, 因此:
00111001
10110000
进行交配的话,二进制就变成:10001001,转换成10进制为 137,那么转换成16进制就是 89了。
字符123456789与掩码加密的整个过程如上面分析,可以看到,字符分别依次与掩码交配,如果掩码不够的话,依次从头循环即可。
因此我们可以编写如下encodeDataFrame.js代码:
var crypto = require('crypto');var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';require('net').createServer(function(o) { var key; o.on('data', function(e) { if (!key) { key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; // WS的字符串 加上 key, 变成新的字符串后做一次sha1运算,最后转换成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 输出字段数据,返回到客户端, o.write('HTTP/1.1 101 Switching Protocol\r\n'); o.write('Upgrade: websocket\r\n'); o.write('Connection: Upgrade\r\n'); o.write('Sec-WebSocket-Accept:' +key+'\r\n'); // 输出空行,使HTTP头结束 o.write('\r\n'); // 握手成功后给客户端发送数据 o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789" })) } else { } })}).listen(8001);/* >> 含义是右移运算符, 右移运算符是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0. 比如 11 >> 2, 意思是说将数字11右移2位。 首先将11转换为二进制数为 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2个数字移出,因为该数字是正数, 所以在高位补零,则得到的最终结果为:0000 0000 0000 0000 0000 0000 0000 0010,转换为10进制是2. << 含义是左移运算符 左移运算符是将一个二进制位的操作数按指定移动的位数向左移位,移出位被丢弃,右边的空位一律补0. 比如 3 << 2, 意思是说将数字3左移2位, 首先将3转换为二进制数为 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把该数字高位(左侧)的两个零移出,其他的数字都朝左平移2位, 最后在右侧的两个空位补0,因此最后的结果是 0000 0000 0000 0000 0000 0000 0000 1100,则转换为十进制是12(1100 = 1*2的3次方 + 1*2的2字方) 注意1: 在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1(一般情况下). 比如:十进制数13在计算机中表示为00001101,其中第一位0表示的是符号 注意2:负数的二进制位如何计算? 比如二进制的原码为 10010101,它的补码怎么计算呢? 首先计算它的反码是 01101010; 那么补码 = 反码 + 1 = 01101011 再来看一个列子: -7 >> 2 意思是将数字 -7 右移2位。 负数先用它的绝对值正数取它的二进制代码,7的二进制位为: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二进制位就是 取反, 取反后再加1,就变成补码。 因此-7的二进制位: 1111 1111 1111 1111 1111 1111 1111 1001, 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此转换成十进制的话 -7 >> 2 ,值就变成 -2了。*/function decodeDataFrame(e) { var i = 0, j, s, arrs = [], frame = { // 解析前两个字节的基本数据 FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; // 处理特殊长度126和127 if (frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if (frame.PayloadLength === 127) { i += 4; // 长度一般用4个字节的整型,前四个字节一般为长整型留空的。 frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++]; } // 判断是否使用掩码 if (frame.Mask) { // 获取掩码实体 frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]]; // 对数据和掩码做异或运算 for(j = 0, arrs = []; j < frame.PayloadLength; j++) { arrs.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { // 否则的话 直接使用数据 arrs = e.slice(i, i + frame.PayloadLength); } // 数组转换成缓冲区来使用 arrs = new Buffer(arrs); // 如果有必要则把缓冲区转换成字符串来使用 if (frame.Opcode === 1) { arrs = arrs.toString(); } // 设置上数据部分 frame.PayloadLength = arrs; // 返回数据帧 return frame;}function encodeDataFrame(e) { var arrs = [], o = new Buffer(e.PayloadData), l = o.length; // 处理第一个字节 arrs.push((e.FIN << 7)+e.Opcode); // 处理第二个字节,判断它的长度并放入相应的后溪长度 if (l < 126) { arrs.push(l); } else if(l < 0x0000) { arrs.push(126, (1&0xFF00) >> 8, 1&0xFF); } else { arrs.push(127, 0, 0, 0, 0, (l&0xFF000000)>>24,(l&0xFF0000)>>16,(l&0xFF00)>>8,l&0xFF ); } // 返回头部分和数据部分的合并缓冲区 return Buffer.concat([new Buffer(arrs), o]);}
然后index.html代码如下:
<html><head> <title>WebSocket Demo</title></head><body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:8001"); ws.onerror = function(e) { console.log(e); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('123456789'); } ws.onmessage = function(e) { console.log(e); } </script></body></html>
进入目录后,运行node encodeDataFrame.js后,打开index.html页面,在控制台看待效果图如下:
使用分片的方式重新修改代码:
上面是基本的使用方法,但是有时候我们需要将一个大的数据包需要分成多个数据帧来传输,因此分片它分为3个部分:
1个开始帧:FIN=0, Opcode > 0;
零个或多个传输帧: FIN=0, Opcode=0;
1个终止帧:FIN=1, Opcode=0;
因此之前的握手成功后发送的数据代码:
o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789"}))
需要分成三部分来发送了;
改成如下代码:
// 握手成功后给客户端发送数据o.write(encodeDataFrame({ FIN: 0, Opcode: 1, PayloadData: "123"}));o.write(encodeDataFrame({ FIN: 0, Opcode: 0, PayloadData: "456"}));o.write(encodeDataFrame({ FIN: 1, Opcode: 0, PayloadData: "789"}));