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\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');
} 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后查看效果

如下:

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

WebSocket原理与实践(三)--解析数据帧的更多相关文章

  1. WebSocket原理与实践(四)--生成数据帧

    WebSocket原理与实践(四)--生成数据帧 从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的.我们自己需要去生成数据帧,解析数据帧的时候我们需要分片. 消息 ...

  2. WebSocket原理与实践(二)---WebSocket协议

    WebSocket原理与实践(二)---WebSocket协议 WebSocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信问题而设计的.协议定义ws和wss协议,分别为普通请求和基 ...

  3. WebSocket原理与实践(一)---基本原理

    WebSocket原理与实践(一)---基本原理 一:为什么要使用WebSocket?1. 了解现有的HTTP的架构模式:Http是客户端/服务器模式中请求-响应所用的协议,在这种模式中,客户端(一般 ...

  4. WebSocket原理与实践

    开题思考:如何实现客户端及时获取服务端数据? Polling 指客户端每隔一段时间(周期性)请求服务端获取数据,可能有更新数据返回,也可能什么都没有,它并不在乎服务端数据有无更新.(Web端一般采用a ...

  5. Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法

    Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...

  6. 嵌入式实时操作系统μCOS原理与实践任务控制与时间的解析

    /*************************************************************************************************** ...

  7. 2017-2018-2 20155228 《网络对抗技术》 实验三:MAL_免杀原理与实践

    2017-2018-2 20155228 <网络对抗技术> 实验三:MAL_免杀原理与实践 实验内容 正确使用msf编码器,msfvenom生成如jar之类的其他文件,veil-evasi ...

  8. kafka原理和实践(三)spring-kafka生产者源码

    系列目录 kafka原理和实践(一)原理:10分钟入门 kafka原理和实践(二)spring-kafka简单实践 kafka原理和实践(三)spring-kafka生产者源码 kafka原理和实践( ...

  9. nodejs 实践:express 最佳实践(三) express 解析

    nodejs 实践:express 最佳实践(三) express 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固, ...

随机推荐

  1. Java 10新特性

    ref:http://www.cocoachina.com/industry/20180309/22520.html https://www.oschina.net/news/94402/java-1 ...

  2. 功率因数cosφ仪表盘

    一.截图 二.说明 本篇博客主要是有三个亮点: ① 刻度标注在仪表盘标线外 ② 仪表盘存在两个刻度值,分别是(正)0.5~1 和(负)-1~-0.5 ③ 仪表盘内标注,分别是“超前”和“滞后” 三.代 ...

  3. blfs(systemv版本)学习笔记-使用apache创建简单的网页服务器

    我的邮箱地址:zytrenren@163.com欢迎大家交流学习纠错! apache项目地址:http://www.linuxfromscratch.org/blfs/view/stable/serv ...

  4. Python 练习: 计算器

    import re def format_string(s): # 对表达式进行格式化 s = s.replace(' ', '') s = s.replace("--", &qu ...

  5. 【转】用yum只下载rpm包而不安装

    转自:http://liucheng.name/1950/ CentOS用yum安装软件是非常方便的,有时,我们只需要下载其中的rpm包,而不直接安装时咋办呢? 一般情况下,yum是不提供只下载的功能 ...

  6. Kotlin入门(18)利用单例对象获取时间

    前面介绍了,使用扩展函数可以很方便地扩充数组Array的处理功能,例如交换两个数组元素.求数组的最大元素等等.那么除了数组之外,日期和时间的相关操作,也是很常见的,比如获取当前日期,获取当前时间.获取 ...

  7. retrofit框架接口调用时候报Throwing new exception

    最近在开发的时候遇到了一个很坑的问题,在三星6.0手机上请求接口时候报了一个异常:Throwing new exception 'length=1658; index=3248' with unexp ...

  8. Elasticsearch5.4署遇到的问题

    问题一 can not run elasticsearch as root Elastic 不建议通过root用户启动ES服务器,如果非要用root启动,可以在config/jvm.options配置 ...

  9. 对JavaScript中闭包的理解

    在前端开发中闭包是一个很重要的知识点,是面试中一定会被问到的内容.之前我对闭包的理解主要是"通过闭包可以在函数外部能访问到函数内部的变量",对闭包运用的也很少,甚至自己写过闭包自己 ...

  10. 精通initramfs构建step by step

    (一)hello world  一.initramfs是什么  在2.6版本的linux内核中,都包含一个压缩过的cpio格式 的打包文件.当内核启动时,会从这个打包文件中导出文件到内核的rootfs ...