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页面,在控制台看待效果图如下:

查看git上代码

使用分片的方式重新修改代码:

上面是基本的使用方法,但是有时候我们需要将一个大的数据包需要分成多个数据帧来传输,因此分片它分为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"}));
(0)

相关推荐

  • 什么是流式输出?

    阿里妹导读:流式输出在阿里内部已经遍地开花,大家耳熟能详却又好奇不已.清楚的是知道它是性能利器从而提升业务转化,不清楚的是到底什么样的技术才算是流式输出?支撑流式输出的技术理论又有哪些?流式输出适合什 ...

  • 城市规划原理(第四版)

    第1篇?城市与城市规划 第1章?城市与城镇化 第1节?城市的产生与定义 第2节?城市的发展 第3节?城镇化 第2章?城市规划思想发展 第1节?古代的城市规划思想 第2节?现代城市规划思想的产生与发展 ...

  • 市场营销:原理与实践(第17版)

    第 1 篇 定义市场营销和市场营销过程 第 1 章?营销:创造顾客价值和顾客契合? 1.1 什么是市场营销 1.2 理解市场与顾客需求 1.3 设计顾客价值导向的市场营销战略和计划 1.4 管理顾客关 ...

  • 力量训练原理与实践

    第一章 导论--定义.目标.任务与原则 一.训练的定义 二.训练的目标 三.训练任务 (一)个性培养 (二)专项体能 (三)神经肌肉的适应性 四.训练原则 (一)超负荷原则 (二)持续训练原则 (三) ...

  • 【赠书】如何深入浅出掌握联邦学习技术的原理与实践

    周末又到了,本周末给大家赠送3本人工智能领域的新书,本次赠送的书籍是<深入浅出联邦学习>,下面来看详情. 这是一本什么样的书 这是一部大型金融集团联邦学习负责人撰写,中外院士及清华.华科. ...

  • 【直播】5.25日晚摄影图像处理原理与实践直播、赠书+书籍解读+案例剖析

    各位朋友,本周二晚8点(5月25日20:00),有三会开设一场深度学习与摄影图像处理相关的直播,届时会有书籍内容解读,实践案例讲解,赠书+纪念文化产品等环节,欢迎大家及时参与! 直播主题 本次主题是带 ...

  • 市场营销:原理与实践 第 2篇

    第 2 篇 理解市场和顾客价值 第 3 章?分析市场营销环境? 3.1 微观环境与宏观环境 3.2 人口与经济环境 3.3 自然与技术环境 3.4 政治 - 社会与文化环境 3.5 应对市场营销环境 ...

  • 市场营销:原理与实践 第 1 篇

    第 1 篇 定义市场营销和市场营销过程 第 1 章?营销:创造顾客价值和顾客契合? 1.1 什么是市场营销 1.2 理解市场与顾客需求 1.3 设计顾客价值导向的市场营销战略和计划 1.4 管理顾客关 ...

  • 市场营销:原理与实践 内容简介

    数字技术的迅猛发展创造了一个崭新的.更加契合的.更加互联的市场营销世界. 新消费者赋能和新市场营销分析,对于市场营销者及其服务的消费者都产生了重大影响.今天的市场营销,就是在数字和社交网络迅速变化的市 ...

  • 中医经络的基本运行原理(第四章)终

    三.背腰骶部的要害穴位(共8个) 1.肺俞穴位置:第三胸椎棘突旁开1.5寸经属:足太阳膀胱经.击中后,冲击第三胁动.静脉和神经,震动心肺.破气机. 2.厥阴俞穴位置:在第四胸椎棘突下旁开1.5寸处.经 ...