前端协议《二》Websocket浅谈
# websocket特点
- 双向通信技术
- 没有同源限制
- 数据分片,按帧传输(文本数据类型,二进制数据类型,控制帧类型)
websocket就服务端推送提供了另外一种解决方案,相比HTTP1.1,它优势就在于服务端推送。他本质上是在tcp协议上封装的另一种应用层协议(websocket协议)。因为他是基于tcp的,所以服务端推送自然不是什么难题。但是在实现上,他并不是直接连接一个tcp连接,然后在上面传输基于websocket协议的数据包。他涉及到一个协议升级(交换)的过程。
客户端request格式如下:
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
2
3
4
5
6
7
服务端响应协议升级:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
2
3
4
服务器给浏览器推送的时候,浏览器会发送ack,但是浏览器给服务器发送的时候,服务器貌似没有返回ack?
服务器(tcp)推送消息的时候把ack也带上了。而不是发送两个tcp包。这就是tcp的机制。tcp不会对每个包都发ack,他会累积确认(发ack),以减少网络的包,但是他也需要保证尽快地回复ack,否则就会导致客户端触发超时重传。tcp什么时候发送确认呢?比如需要发送数据的时候,或者超过一定时间没有收到数据包,或者累积的确认数量达到阈值等。
websocket连接持续多久?
在websocket连接上,一直不通信的话,websocket连接所维持的时间是依赖tcp实现的。因为我们发现tcp层会一直发送探测包。达到阈值之后,连接就会被断开。所以我们想维持websocket连接的话,需要自己去发送心跳包,比如ping,pong。
# webcocket数据帧结构

具体的含义如下:
- **FIN:**1位,表示当前数据帧是否为最后一帧。1代表最后帧,0代表还在传输。如果消息只由一帧构成,那么起始帧就是结束帧。
- **RSV1,RSV2,RSV3:**各1位,如果未定义拓展,那么都为0;如果定义了拓展则为非0值。如果接收的帧为非0值但拓展中没有该值的定义,那么连接关闭。
- **opcode:**4位,为PayloadData有效负荷,如果接收到未知的opcode,关闭连接。opcode主要有以下类型:
- 0x0:表示附加数据帧,延续帧
- 0x1:表示文本数据帧
- 0x2:表示二进制数据帧
- 0x3-7:暂无定义,保留
- 0x8:表示连接关闭
- 0x9:表示ping
- 0xA:表示pong,ping pong用于心跳连接,检测连接是否断开
- 0xB-F:暂无定义,保留
- **MASK:**1位,用于标识PlayloadData是否经过掩码处理,如果是1,Masking-key域的数据即是掩码秘钥,用于解码PlayloadData。websocket的安全机制,从客户端向服务器传输的数据帧必须经过掩码处理,服务端若收到未经掩码处理的数据帧,则主动关闭连接(发送closed帧,状态码1002)。
- **MASKing-key:**客户端设置得32位的随机数,掩码算法主要是:
- origin-i:原始数据的第i字节
- transform-i:转换后的第i字节
- a:选取的i mod 4 位的值(掩码key只有4个字节)
- transform-i = origin-i XOR a 。两者异或得到掩码后的值。
- Playload data:x+y 位,包含拓展数据和应用数据。
# 状态码
当关闭一个已建立的连接时,端点可以使用预设的状态码来解释关闭原因,主要有以下状态码:
状态码 描述 1000 正常关闭 1001 终端离开,可能服务端错误或客户端离开 1002 协议错误 1003 接收到不允许的数据类型(文本类型收到二进制数据) 1004-1006 保留 1007 接收到格式不符的数据(文本消息收到非utf-8) 1008 收到不符数据,用于不适1003和1009场景 1009 收到过大数据帧 1011 客户端阻止完成请求 1012 服务端重启 # Node构建websocket
模拟浏览器切换协议
var WebSocket = function (url) { // 伪代码 解析ws://127.0.0.1:12010/updates 请求 this.options = parseUrl(url); this.connect(); }; WebSocket.prototype.onopen = function () { // TODO }; WebSocket.prototype.setSocket = function (socket) { this.socket = socket; }; WebSocket.prototype.connect = function () { var this = that; var key = new Buffer(this.options.protocolVersion + '-' + Date.now()).toString('base64'); var shasum = crypto.createHash('sha1'); var expected = shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64'); var options = { port: this.options.port, // 12010 host: this.options.hostname, // 127.0.0.1 headers: { 'Connection': 'Upgrade', 'Upgrade': 'websocket', 'Sec-WebSocket-Version': this.options.protocolVersion, 'Sec-WebSocket-Key': key } }; var req = http.request(options); req.end(); req.on('upgrade', function(res, socket, upgradeHead) { // 连接成功 that.setSocket(socket); //触发open事件 that.onopen(); }); };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36服务端
var server = http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }); server.listen(12010); // 在收ڟupgrade请求ࢫLjߢኮ客户端ሎႹൎ换ၹᅱ server.on('upgrade', function (req, socket, upgradeHead) { var head = new Buffer(upgradeHead.length); upgradeHead.copy(head); var key = req.headers['sec-websocket-key']; var shasum = crypto.createHash('sha1'); key = shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest('base64'); var headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + key, 'Sec-WebSocket-Protocol: ' + protocol ]; // 发送数据 socket.setNoDelay(true); socket.write(headers.concat('', '').join('\r\n')); // 建立服务器端WebSocket连接 var websocket = new WebSocket(); websocket.setSocket(socket); });1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26客户端
var socket = new WebSocket('ws://127.0.0.1:12010/updates'); //握手完成后触发 socket.onopen = function () { setInterval(function() { if (socket.bufferedAmount == 0) socket.send(getUpdateData()); }, 50); }; socket.onmessage = function (event) { // TODO:event.data };1
2
3
4
5
6
7
8
9
10
11# 兼容性
websocket虽然功能强大,但不是所有的浏览器都能支持websocket,IE11以下的浏览器就无法使用websocket。这只能使用HTTP的轮询和长轮询(用户的扫码登录就是长轮询)来替代websocket了。