WebSocket原理与实践(四)--生成数据帧
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 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解析:
比如我们现在第三节我们讲到的 "解析数据帧" 里面的代码,我们发送的消息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 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
我们把上面的转换后的二进制 对照上面的 基本帧协议表看下:
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 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
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"
}));
WebSocket原理与实践(四)--生成数据帧的更多相关文章
- WebSocket原理与实践(三)--解析数据帧
WebSocket原理与实践(三)--解析数据帧 1-1 理解数据帧的含义: 在WebSocket协议中,数据是通过帧序列来传输的.为了数据安全原因,客户端必须掩码(mask)它发送到服务器的所有 ...
- WebSocket原理与实践(二)---WebSocket协议
WebSocket原理与实践(二)---WebSocket协议 WebSocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信问题而设计的.协议定义ws和wss协议,分别为普通请求和基 ...
- WebSocket原理与实践(一)---基本原理
WebSocket原理与实践(一)---基本原理 一:为什么要使用WebSocket?1. 了解现有的HTTP的架构模式:Http是客户端/服务器模式中请求-响应所用的协议,在这种模式中,客户端(一般 ...
- WebSocket原理与实践
开题思考:如何实现客户端及时获取服务端数据? Polling 指客户端每隔一段时间(周期性)请求服务端获取数据,可能有更新数据返回,也可能什么都没有,它并不在乎服务端数据有无更新.(Web端一般采用a ...
- 20165223《网络对抗技术》Exp3 免杀原理与实践
目录 -- 免杀原理与实践 免杀原理与实践 本次实验任务 基础知识问答 免杀扫描引擎 实验内容 正确使用msf编码器,msfvenom生成jar等文件,veil-evasion,加壳工具,使用shel ...
- kafka原理和实践(四)spring-kafka消费者源码
系列目录 kafka原理和实践(一)原理:10分钟入门 kafka原理和实践(二)spring-kafka简单实践 kafka原理和实践(三)spring-kafka生产者源码 kafka原理和实践( ...
- 图解ARP协议(四)代理ARP原理与实践(“善意的欺骗”)
一.代理ARP概述 我:当电脑要访问互联网上的服务器,目标MAC是什么? 很多小伙伴在刚学习网络协议的时候,经常这样直接回应:不就是服务器的MAC嘛! 这时我会反问:那电脑怎么拿到这个服务器的MAC地 ...
- 搞懂分布式技术10:LVS实现负载均衡的原理与实践
搞懂分布式技术10:LVS实现负载均衡的原理与实践 浅析负载均衡及LVS实现 原创: fireflyc 写程序的康德 2017-09-19 负载均衡 负载均衡(Load Balance,缩写LB)是一 ...
- WebSocket原理
一 . WebSocket原理 1.1.背景 WebSocket 是基于Http 协议的改进,Http 为无状态协议,基于短连接,需要频繁的发起请求,第二 Http 只能客户端发起请求,服务端无法主动 ...
随机推荐
- 修改Mysql字符集
第一种 一.修改my.ini配置文件(MySQL配置文件) character_set_server = utf8 #设置字符集 重启mysql数据库服务 查看当前数据库字符集 show VARIAB ...
- SSM+solr 通过商品搜索学习solr的简单使用
学习了一下https://github.com/TyCoding/ssm-redis-solr这个github上的solr搜索功能,现在来记录一下. 我的理解就是solr有点类似于数据库,但它是有索引 ...
- blfs(systemd版本)学习笔记-总页
我的邮箱地址:zytrenren@163.com欢迎大家交流学习纠错! lfs(systemd版本)学习笔记:https://www.cnblogs.com/renren-study-notes/p/ ...
- blfs(systemv版本)学习笔记-安装、配置和使用wpa_supplicant无线网络连接工具
我的邮箱地址:zytrenren@163.com欢迎大家交流学习纠错! wireless项目地址:http://www.linuxfromscratch.org/blfs/view/8.3/basic ...
- Python 练习: 计算器
import re def format_string(s): # 对表达式进行格式化 s = s.replace(' ', '') s = s.replace("--", &qu ...
- WEB前端面试2014阿里旺旺
NO1.下图绿色区域的宽度为100%,其中有三个矩形,第一个矩形的宽度是200px,第二个和第三个矩形的宽度相等.请使用css3中的功能实现它们的布局. 已知HTML结构是: <div clas ...
- Vagrant安装配置
转载自:https://my.oschina.net/u/3424381/blog/888205 Vagrant安装配置 实际上Vagrant只是一个让你可以方便设置你想要的虚拟机的便携式工具,它底层 ...
- ComponetOne 2014 v3版本正式发布
2014年11月18日---ComponentOne Studio Enterprise 2014 v3版全球正式发布.ComponentOne Studio Enterprise是世界知名的Micr ...
- 浅谈 Mysql 中的索引
文章归属:http://feiyan.info/16.html,我想自己去写了,但是发现此君总结的非常详细.直接搬过来了 关于MySQL索引的好处,如果正确合理设计并且使用索引的MySQL是一辆兰博基 ...
- python内置小工具
python -m http.server # 启动一个下载服务器 echo '{"job": "developer", "job": &q ...