websocketws协议简介
⼀、内容概览
WebSocket的出现,使得浏览器具备了实时双向通信的能⼒。本⽂由浅⼊深,介绍了WebSocket如何建⽴连接、交换数据的细节,以及数据帧的格式。此外,还简要介绍了针对WebSocket的安全攻击,以及协议是如何抵御类似攻击的。
⼆、什么是WebSocket
HTML5开始提供的⼀种浏览器与服务器进⾏全双⼯通讯的⽹络技术,属于应⽤层协议。它基于TCP传输协议,并复⽤HTTP的握⼿通道。对⼤部分web开发者来说,上⾯这段描述有点枯燥,其实只要记住⼏点:
1. WebSocket可以在浏览器⾥使⽤
2. ⽀持双向通信
3. 使⽤很简单
1、有哪些优点
说到优点,这⾥的对⽐参照物是HTTP协议,概括地说就是:⽀持双向通信,更灵活,更⾼效,可扩展性更好。
1. ⽀持双向通信,实时性更强。
2. 更好的⼆进制⽀持。
3. 较少的控制开销。连接创建后,ws客户端、服务端进⾏数据交换时,协议控制的数据包头部较⼩。在不包含头部的情况下,服务端到
客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。⽽HTTP协议每次通信都需要携带完整的头部。
4. ⽀持扩展。ws协议定义了扩展,⽤户可以扩展协议,或者实现⾃定义的⼦协议。(⽐如⽀持⾃定义压缩算法等)
对于后⾯两点,没有研究过WebSocket协议规范的同学可能理解起来不够直观,但不影响对WebSocket的学习和使⽤。
2、需要学习哪些东西
对⽹络应⽤层协议的学习来说,最重要的往往就是连接建⽴过程、数据交换教程。当然,数据的格式是逃不掉的,因为它直接决定了协议本⾝的能⼒。好的数据格式能让协议更⾼效、扩展性更好。
下⽂主要围绕下⾯⼏点展开:
1. 如何建⽴连接
2. 如何交换数据
3. 数据帧格式
4. 如何维持连接
三、⼊门例⼦
在正式介绍协议细节前,先来看⼀个简单的例⼦,有个直观感受。例⼦包括了WebSocket服务端、WebSocket客户端(⽹页端)。完整代码可以在  到。
这⾥服务端⽤了ws这个库。相⽐⼤家熟悉的socket.io,ws实现更轻量,更适合学习的⽬的。
1、服务端
代码如下,监听8080端⼝。当有新的连接请求到达时,打印⽇志,同时向客户端发送消息。当收到到来⾃客户端的消息时,同样打印⽇志。
var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
<('connection', function connection(ws) {
console.log('server: receive connection.');
<('message', function incoming(message) {
console.log('server: received: %s', message);
});
ws.send('world');
});
<('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
app.listen(3000);
2、客户端
代码如下,向8080端⼝发起WebSocket连接。连接建⽴后,打印⽇志,同时向服务端发送消息。接收到来⾃服务端的消息后,同样打印⽇志。
<script>
var ws = new WebSocket('ws://localhost:8080');
console.log('ws onopen');
ws.send('from client: hello');
};
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
</script>
3、运⾏结果
可分别查看服务端、客户端的⽇志,这⾥不展开。
服务端输出:
server: receive connection.
server: received hello
客户端输出:
client: ws connection is open
client: received world
四、如何建⽴连接
前⾯提到,WebSocket复⽤了HTTP的握⼿通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
1、客户端:申请协议升级
⾸先,客户端发起协议升级请求。可以看到,采⽤的是标准的HTTP报⽂格式,且只⽀持GET⽅法。
GET / HTTP/1.1
Host: localhost:8080
Origin: 127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重点请求⾸部意义如下:
Connection: Upgrade:表⽰要升级协议
Upgrade: websocket:表⽰要升级到websocket协议。
Sec-WebSocket-Version: 13:表⽰websocket的版本。如果服务端不⽀持该版本,需要返回⼀个Sec-WebSocket-Version header,⾥⾯包含服务端⽀持的版本号。websocket和socket
Sec-WebSocket-Key:与后⾯服务端响应⾸部的Sec-WebSocket-Accept是配套的,提供基本的防护,⽐如恶意的连接,或者⽆意的连接。
注意,上⾯请求省略了部分⾮重点请求⾸部。由于是标准的HTTP请求,类似Host、Origin、Cookie等请
求⾸部会照常发送。在握⼿阶段,可以通过相关请求⾸部进⾏ 安全限制、权限校验等。
2、服务端:响应协议升级
服务端返回内容如下,状态代码101表⽰协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
备注:每个header都以\r\n结尾,并且最后⼀⾏加上⼀个额外的空⾏\r\n。此外,服务端回应的HTTP状态码只能在握⼿阶段使⽤。过了握⼿阶段后,就只能采⽤特定的错误码。
3、Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept根据客户端请求⾸部的Sec-WebSocket-Key计算出来。
计算公式为:
1. 将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
2. 通过SHA1计算出摘要,并转成base64字符串。
伪代码如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )
验证下前⾯的返回结果:
const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';
let secWebSocketAccept = ateHash('sha1')
.update(secWebSocketKey + magic)
.
digest('base64');
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
五、数据帧格式
客户端、服务端数据的交换,离不开数据帧格式的定义。因此,在实际讲解数据交换之前,我们先来看下WebSocket的数据帧格式。
WebSocket客户端、服务端通信的最⼩单位是帧(frame),由1个或多个帧组成⼀条完整的消息(message)。
1. 发送端:将消息切割成多个帧,并发送给服务端;
2. 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;
本节的重点,就是讲解数据帧的格式。详细定义可参考  。
1、数据帧格式概览
下⾯给出了WebSocket数据帧的统⼀格式。熟悉TCP/IP协议的同学对这样的图应该不陌⽣。
1. 从左到右,单位是⽐特。⽐如FIN、RSV1各占据1⽐特,opcode占据4⽐特。
2. 内容包括了标识、操作代码、掩码、数据、数据长度等。(下⼀⼩节会展开)
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 ...                |
+---------------------------------------------------------------+
2、数据帧格式详解
针对前⾯的格式概览图,这⾥逐个字段进⾏讲解,如有不清楚之处,可参考协议规范,或留⾔交流。
FIN:1个⽐特。
如果是1,表⽰这是消息(message)的最后⼀个分⽚(fragment),如果是0,表⽰不是是消息(message)的最后⼀个分⽚(fragment)。
RSV1, RSV2, RSV3:各占1个⽐特。
⼀般情况下全为0。当客户端、服务端协商采⽤WebSocket扩展时,这三个标志位可以⾮0,且值的含义由扩展进⾏定义。如果出现⾮零的值,且并没有采⽤WebSocket扩展,连接出错。
Opcode: 4个⽐特。
操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:
%x0:表⽰⼀个延续帧。当Opcode为0时,表⽰本次数据传输采⽤了数据分⽚,当前收到的数据帧为其中⼀个数据分⽚。
%x1:表⽰这是⼀个⽂本帧(frame)
%x2:表⽰这是⼀个⼆进制帧(frame)
%x3-7:保留的操作代码,⽤于后续定义的⾮控制帧。
%x8:表⽰连接断开。
%x9:表⽰这是⼀个ping操作。
%xA:表⽰这是⼀个pong操作。
%xB-F:保留的操作代码,⽤于后续定义的控制帧。
Mask: 1个⽐特。
表⽰是否要对数据载荷进⾏掩码操作。从客户端向服务端发送数据时,需要对数据进⾏掩码操作;从服务端向客户端发送数据时,不需要对数据进⾏掩码操作。
如果服务端接收到的数据没有进⾏过掩码操作,服务端需要断开连接。
如果Mask是1,那么在Masking-key中会定义⼀个掩码键(masking key),并⽤这个掩码键来对数据载荷进⾏反掩码。所有客户端发送到服务端的数据帧,Mask都是1。
掩码的算法、⽤途在下⼀⼩节讲解。
Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。
假设数Payload length === x,如果
x为0~126:数据的长度为x字节。
x为126:后续2个字节代表⼀个16位的⽆符号整数,该⽆符号整数的值为数据的长度。
x为127:后续8个字节代表⼀个64位的⽆符号整数(最⾼位为0),该⽆符号整数的值为数据的长度。
此外,如果payload length占⽤了多个字节的话,payload length的⼆进制表达采⽤⽹络序(big endian,重要的位在前)。
Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进⾏了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。
备注:载荷数据的长度,不包括mask key的长度。
Payload data:(x+y) 字节
载荷数据:包括了扩展数据、应⽤数据。其中,扩展数据x字节,应⽤数据y字节。
扩展数据:如果没有协商使⽤扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使⽤必须在握⼿阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应⽤数据:任意的应⽤数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应⽤数据的长度。
3、掩码算法
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采⽤如下算法:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。