前端协议《二》Websocket浅谈

10/24/2020 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==
1
2
3
4
5
6
7

服务端响应协议升级:

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
1
2
3
4

服务器给浏览器推送的时候,浏览器会发送ack,但是浏览器给服务器发送的时候,服务器貌似没有返回ack?

服务器(tcp)推送消息的时候把ack也带上了。而不是发送两个tcp包。这就是tcp的机制。tcp不会对每个包都发ack,他会累积确认(发ack),以减少网络的包,但是他也需要保证尽快地回复ack,否则就会导致客户端触发超时重传。tcp什么时候发送确认呢?比如需要发送数据的时候,或者超过一定时间没有收到数据包,或者累积的确认数量达到阈值等。

websocket连接持续多久?

在websocket连接上,一直不通信的话,websocket连接所维持的时间是依赖tcp实现的。因为我们发现tcp层会一直发送探测包。达到阈值之后,连接就会被断开。所以我们想维持websocket连接的话,需要自己去发送心跳包,比如ping,pong。

  • # webcocket数据帧结构

image-20201015220950045

具体的含义如下:

  1. **FIN:**1位,表示当前数据帧是否为最后一帧。1代表最后帧,0代表还在传输。如果消息只由一帧构成,那么起始帧就是结束帧。
  2. **RSV1,RSV2,RSV3:**各1位,如果未定义拓展,那么都为0;如果定义了拓展则为非0值。如果接收的帧为非0值但拓展中没有该值的定义,那么连接关闭。
  3. **opcode:**4位,为PayloadData有效负荷,如果接收到未知的opcode,关闭连接。opcode主要有以下类型
    • 0x0:表示附加数据帧,延续帧
    • 0x1:表示文本数据帧
    • 0x2:表示二进制数据帧
    • 0x3-7:暂无定义,保留
    • 0x8:表示连接关闭
    • 0x9:表示ping
    • 0xA:表示pong,ping pong用于心跳连接,检测连接是否断开
    • 0xB-F:暂无定义,保留
  4. **MASK:**1位,用于标识PlayloadData是否经过掩码处理,如果是1,Masking-key域的数据即是掩码秘钥,用于解码PlayloadData。websocket的安全机制,从客户端向服务器传输的数据帧必须经过掩码处理,服务端若收到未经掩码处理的数据帧,则主动关闭连接(发送closed帧,状态码1002)。
  5. **MASKing-key:**客户端设置得32位的随机数,掩码算法主要是:
    • origin-i:原始数据的第i字节
    • transform-i:转换后的第i字节
    • a:选取的i mod 4 位的值(掩码key只有4个字节)
    • transform-i = origin-i XOR a 。两者异或得到掩码后的值。
  6. 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了。