从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的。我们自己需要去生成数据帧,解析数据帧的时候我们需要分片。 消息分片: 如果大数据不能被碎片化,那么一端就必须将消息整个载入内存缓冲之中,然后需要计算长度等操作并发送,但是有了碎片化机制,服务器端或者中间件就可以选取适用的内存缓冲长度,然后当缓冲满了之后就发送一个消息碎片。 分片规则: 注意: 下面我们来理解下上面分片规则2中的话的含义: 还是看基本帧协议如下: 1 2 3 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 ... | +---------------------------------------------------------------+ demo解析: <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进制的转换表如下: 我们现在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 这些16进制先转换成10进制,然后转换成二进制,分析代码如下: 16进制 10进制 2进制 81 8*16的1次方 + 1*16的0次方 = 129 10000001 89 8*16的1次方 + 9*16的0次方 = 137 10001001 b0 11*16的1次方 + 0*16的0次方 = 176 10110000 23 2*16的1次方 + 3*16的0次方 = 35 00100011 52 5*16的1次方 + 2*16的0次方 = 82 01010010 5a 5*16的1次方 + 10*16的0次方 = 90 01011010 81 8*16的1次方 + 1*16的0次方 = 129 10000001 11 1*16的1次方 + 1*16的0次方 = 17 00010001 61 6*16的1次方 + 1*16的0次方 = 97 00111101 6e 6*16的1次方 + 14*16的0次方 = 110 01101110 85 8*16的1次方 + 5*16的0次方 = 133 10000101 15 1*16的1次方 + 5*16的0次方 = 21 00010101 65 6*16的1次方 + 5*16的0次方 = 101 01100101 62 6*16的1次方 + 2*16的0次方 = 98 01100010 89 8*16的1次方 + 9*16的0次方 = 137 10001001 我们把上面的转换后的二进制 对照上面的 基本帧协议表看下: 0x0 表示附加数据帧 注意:其中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 00110001 2 50 00110010 3 51 00110011 4 52 00110100 5 53 00110101 6 54 00110110 7 55 00110111 8 56 00111000 9 57 00111001 6-1: 其中字符1的二进制位 00110001,掩码b0的二进制位 10110000, 因此: 00110001 进行交配的话,二进制就变成:10000001,转换成10进制为 129了,那么转换成16进制就是 81了。 6-2:字符2的二进制位 00110010,掩码23的二进制位 00100011,因此: 00110010 进行交配的话,二进制就变成 00010001,转换10进制为17,那么转换成16进制就是 11了。 6-3: 字符3的二进制位 00110011,掩码52的二进制位 01010010,因此: 00110011 进行交配的话,二进制就变成:01100001,转换成10进制为 97,那么转换成16进制就是 61了。 6-4: 字符4的二进制位 00110100,掩码 5a 的二进制位 01011010,因此: 00110100 进行交配的话,二进制就变成 01101110,转换成10进制为 110,那么转换成16进制为 6e. 6-5: 字符5的二进制位 00110101,掩码b0的二进制位 10110000, 因此: 00110101 进行交配的话,二进制就变成:10000101,转换成10进制为 133,那么转换成16进制就是 85了。 6-6: 字符6的二进制位 00110110,掩码23的二进制位 00100011,因此: 00110110 进行交配的话,二进制就变成:00010101,转换成10进制为 21,那么转换成16进制就是 15了。 6-7: 字符7的二进制位 00110111,掩码52的二进制位 01010010,因此: 00110111 进行交配的话,二进制就变成:01100101,转换成10进制为 101,那么转换成16进制就是 65了。 6-8: 字符8的二进制位 00111000,掩码 5a 的二进制位 01011010,因此: 00111000 进行交配的话,二进制就变成:01100010,转换成10进制为 98,那么转换成16进制就是 62了。 6-9: 字符9的二进制位 00111001,掩码b0的二进制位 10110000, 因此: 00111001 进行交配的话,二进制就变成: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; 因此之前的握手成功后发送的数据代码: o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789" })) 需要分成三部分来发送了; 改成如下代码: |
|
来自: 猎狐肥 > 《WebSocket》