• WebSocket原理与实践(三)--解析数据帧


    WebSocket原理与实践(三)--解析数据帧

    1-1 理解数据帧的含义:
       在WebSocket协议中,数据是通过帧序列来传输的。为了数据安全原因,客户端必须掩码(mask)它发送到服务器的所有帧,当它收到一个
    没有掩码的帧时,服务器必须关闭连接。不过服务器端给客户端发送的所有帧都不是掩码的,如果客户端检测到掩码的帧时,也一样必须关闭连接。
    当帧被关闭的时候,可能发送状态码1002(协议错误)。

    基本帧协议如下:

      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 ...                |
     +---------------------------------------------------------------+

    如上是基本帧协议,它带有操作码(opcode)的帧类型,负载长度,和用于 "扩展数据" 与 "应用数据" 及 它们一起定义的 "负载数据"的指定位置,
    某些字节和操作码保留用于未来协议的扩展。

    FIN(1位): 是否为消息的最后一个数据帧。
    RSV1,RSV2,Rsv3(每个占1位),必须是0,除非一个扩展协商为非零值定义的。
    Opcode表示帧的类型(4位),例如这个传输的帧是文本类型还是二进制类型,二进制类型传输的数据可以是图片或者语音之类的。(这4位转换成16进制值表示的意思如下):

    0x0 表示附加数据帧
    0x1 表示文本数据帧
    0x2 表示二进制数据帧
    0x3-7 暂时无定义,为以后的非控制帧保留
    0x8 表示连接关闭
    0x9 表示ping
    0xA 表示pong
    0xB-F 暂时无定义,为以后的控制帧保留

    Mask(占1位): 表示是否经过掩码处理, 1 是经过掩码的,0是没有经过掩码的。

    payload length (7位+16位,或者 7位+64位),定义负载数据的长度。
       1. 如果数据长度小于等于125的话,那么该7位用来表示实际数据长度。
       2. 如果数据长度为126到65535(2的16次方)之间,该7位值固定为126,也就是 1111110,往后扩展2个字节(16为,第三个区块表示),用于存储数据的实际长度。
       3. 如果数据长度大于65535, 该7位的值固定为127,也就是 1111111 ,往后扩展8个字节(64位),用于存储数据实际长度。

    Masking-key(0或者4个字节),该区块用于存储掩码密钥,只有在第二个子节中的mask为1,也就是消息进行了掩码处理时才有,否则没有,
    所以服务器端向客户端发送消息就没有这一块。

    Payload data 扩展数据,是0字节,除非已经协商了一个扩展。

    1-2 客户端到服务器掩码
    WebSocket协议要求客户端所发送的帧必须掩码,掩码的密钥是一个32位的随机值。所有数据都需要与掩码做一次异或运算。帧头在第二个字节的第一位表示该帧是否使用了掩码。
    WebSocket服务器接收的每个载荷在处理之前首先需要处理掩码,解除掩码之后,服务器将得到原始消息内容。二进制消息可以直接交付。文本消息将进行UTF-8解码
    并输出到字符串中。

    二进制位运算符知识扩展:

    >> 含义是右移运算符,
       右移运算符是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补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了。

    数据帧解析的程序如下代码:(decodeDataFrame.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
    ');
          o.write('Upgrade: websocket
    ');
          o.write('Connection: Upgrade
    ');
          o.write('Sec-WebSocket-Accept:' +key+'
    ');
          // 输出空行,使HTTP头结束
          o.write('
    ');
        } else {
          // 数据处理
          onmessage(e);
        }
      })
    }).listen(8000);
    /* 
     >> 含义是右移运算符,
       右移运算符是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补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 onmessage(e) {
      console.log(e)
      e = decodeDataFrame(e);  // 解析数据帧
      console.log(e);  // 把数据帧输出到控制台
    }

    index.html代码如下:

    <html>
    <head>
      <title>WebSocket Demo</title>
    </head>
    <body>
      <script type="text/javascript">
        var ws = new WebSocket("ws://127.0.0.1:8000");
        ws.onerror = function(e) {
          console.log(e);
        };
        ws.onopen = function(e) {
          console.log('握手成功');
          ws.send('次碳酸钴');
        }
      </script>
    </body>
    </html>

    查看github上的源码

    demo还是一样,decodeDataFrame.js 和 index.html, 先进入项目中对应的目录后,使用node decodeDataFrame.js,  然后打开index.html后查看效果

    如下:

    这样服务器接收客户端穿过了的数据就没问题了。

  • 相关阅读:
    C# MJPEG 客户端简单实现
    CefSharp 实现多标签页 调用ChromiumWebBrowser的SetAsPopup()后浏览页卡死,的另一种解决方案
    opencv findContours 报错解决记录
    什么叫网关
    分别实现网页播放mp3、flv、wmv、Flash,代码兼容FireFox
    ToString()和Convert.ToString()的用法区别
    asp,asp.net中关于双引号和单引号的用法
    .NET中继承和多态深入剖析(上)
    ASP.NET中不常用的另类绑定方法<%$ %>
    C#日期函数所有样式大全
  • 原文地址:https://www.cnblogs.com/tugenhua0707/p/8542890.html
Copyright © 2020-2023  润新知