• WebSocket 详解


    HTTP 协议在设计上就是一个单向的网络协议,服务器只能被动的接收请求,然后返回相应的数据。对于需要双向通信的场景,虽然可以通过轮询,Comet 等方式实现,但每次链接都要三次握手,效率低下。

     

    与http比较:

       1.都基于 TCP 的、应用层的可靠性传输协议

       2.WebSocket 在握手时的数据是通过 HTTP 传输的,一旦连接建立后就不再依赖 HTTP 了

    社区开源方案

        socket.io,  ws 等

    webSocket的应用场景

    • 通知: 由业务服务端发起,由客户端接收的场景,这类场景下业务通常会有兜底逻辑
    • 聊天:服务端和客户端发双向消息进行交互,用在聊天场景
    • 游戏:服务端和客户端做高频消息交互
    • 语音:从客户端持续不断产生语音包,语音包由大语音包切分而来,需要在服务端重新做组合,要求大包传输 + 顺序性保证
    • 直播:大量用户加入同一个直播间,同一直播间内的用户可发弹幕,礼物
    • ioT:边缘节点设备,如单车,共享充电宝等
    • 数据上报:从客户端持续上报数据到服务端

    通信建立:

    • cloent向服务端发出一个 Upgrade: WebSocket 的协议升级 HTTP 请求,该请求附带了一个标识 Sec-WebSocket-Key; 
    • 服务端接收到协议升级请求后返回,其状态码为 101 ,表明服务端已经成功升级为 WebSocket 协议了。该信息中同样也包含了一个标识 Sec-WebSocket-Accept,该标识符是服务端根据客户端发请求中的 Sec-WebSocket-Key 值计算出来的;
    • 客户端接受到服务器的返回后,会判断服务端返回的 Sec-WebSocket-Accept 标识是否和发出 Sec-WebSocket-Key 对应,如果不是就会抛出一个 “Error during WebSocket handshake” 的错误并关闭连接。

    Sec-WebSocket-Accept 值的计算:

    • Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 做字符串拼接;
    • 通过 SHA1 计算出摘要,并转成 base64 字符串。

    上代码

     client 建立socket链接

     

    ...
    <script>
      const host = '127.0.0.1';
      const port = 8001;
      const ws = new WebSocket(`ws://${host}:${port}`);
    </script>
    

      

    node端监听 WebSocket 升级请求,返回升级成功的数据:

    server.on('upgrade', (req, socket) => {
      if (req.headers['upgrade'] !== 'websocket') {
        res.end('HTTP/1.1 400 Bad Request');
        return;
      }
      const secWsKey = req.headers['sec-websocket-key'];
      const secWsAccept = generateSecWsAccept(secWsKey);
      const responseHeaders = [
        'HTTP/1.1 101 Web Socket Protocol Handshake',
        'Upgrade: WebSocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' + secWsAccept
      ];
      socket.write(responseHeaders.join('
    ') + '
    
    ');
    }
    

      

    Sec-WebSocket-Accept值生成函数

    function generateSecWsAccept (secWsKey) {
      return crypto
        .createHash('SHA1')
        .update(secWsKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
        .digest('base64');
    }
    

     

    开浏览器调试工具,可以看到 WebSocket 通信确实建立起来了。

     

     前端发送消息

    <script>
      ...
      ws.addEventListener('open', () => {
        ws.send('I am client.');
      });
    </script>

    服务端监听消息

    server.on('upgrade', (req, socket) => {
      ...
      socket.on('data', (data) => {
        console.log(data.toString());
      })
    });
    

    数据帧解析

    此时发现服务端监听打印的信息是乱码,因为这里接收的 data 并不完全等同于消息的信息,拿到的是 WebSocket 的数据帧。

    WebSocket 通信的最小信息单位就是帧,一个或多个帧构成一条完成的消息。
     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 ...                |
    +---------------------------------------------------------------+

      

     假定数据的长度一定是小于 126 Byte 的,忽略 Payload len 长度的各种判断,解析成消息数据
    socket.on('data', (data) => {
      // Receive message
      const payloadLen = data[1] & parseInt(1111111, 2); // 假设发送的数据长度小于 125
      const maskingKey = data.slice(2, 6);
      const payloadData = new Buffer(payloadLen);
      for (let i = 0; i < payloadLen; i++) {
        let j = i % 4;
        payloadData[i] = data[6 + i] ^ maskingKey[j];
      }
      console.log(payloadData.toString());
    });
    

     

    向客户端发送消息

    同上,在向客户端发送消息前,需要把要发送的消息数据 包装成一个帧信息。这里为了方便我们设置 MASK 字段为 0,表示发出的数据没有进行加密。

    const dataBuffer = new Buffer('I am Server.');
    const payloadLen = dataBuffer.length;
    const assistData = [];
    assistData.push(129);			// 129: 1000 0001
    assistData.push(payloadLen);
    let assistBuffer = new Buffer(assistData);
    let message = Buffer.concat([assistBuffer, dataBuffer]);
    socket.write(message);
    

    客户端监听

    <script>
      ...
    	ws.addEventListener('message', (event) => {
        console.log('message:' + event.data); // 打印 message 信息
      });
    </script>
    

      

     

  • 相关阅读:
    java图形化Swing教程(一)
    NYOJ467 中缀式变后缀式 【栈】
    火云开发课堂
    Egret项目Typescript的编译报错
    Java学习笔记(八):集合类
    Java学习笔记(七):内部类、静态类和泛型
    认识JavaScript的原型
    Java学习笔记(六):面向对象、接口和抽象类
    Java学习笔记(五):异常处理
    Java学习笔记(四):流程控制
  • 原文地址:https://www.cnblogs.com/breakdown/p/15088414.html
Copyright © 2020-2023  润新知