H5数据实时方案「WebSocket」

作者: 果汁凉茶丶 | 来源:发表于2019-11-06 19:00 被阅读0次

    数据实时:即数据库中的数据得到更新,页面立刻就想得到更新并展示最新的数据状态。通常使用在大数据可视化分析,运营数据监控等场景。

    # 数据实时方案

    Web想要更新页面,通常都是客户端发起Http异步请求,主动向服务端索取数据,方案有:
    (1)Ajax轮询,又称 Ajax短连接:即启动一个定时器隔一定时间(如1s)发送一个请求,服务端收到请求无论如何都直接返回当前数据库状态数据。缺点是实时性不够,产生很多不必要的请求。可用于刷新频率不是很高的场景。
    (2)Ajax长连接:客户端发起Http请求,并设置一个长超时时间,服务端收到请求后,检查数据库如果没有更新则阻塞请求,直到有更新或超时为止。客户端每次收到响应后,立即再发一个请求,Comet就是这种方式。缺点是服务器的处理线程长时间挂起,极大浪费资源,且网络链路可能被网关关闭,需要如ping数据来维持链接。

      以上两种机制都治标不治本,是否能有一种机制,由服务端自己检测数据状态,有更新主动告知客户端。好在,HTML5推出了 WebSocket 协议,解决了这个问题

    # WebSocket是什么

    WebSocket(以下简称 ws)是HTML5提供的一种在单个 TCP 连接上进行全双工通讯的网络技术,目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,让双方都可以主动给对方发消息
      虽说ws是H5下新的协议,但其实也不是全新的。它属于应用层协议,复用了HTTP的握手通道。ws协议与HTTP协议都是基于TCP的,因此都是可靠的协议。ws客户端和服务器只需要做一个握手的动作,两者之间就形成了一条快速通道。在建立握手连接时,数据是通过http进行传输的,但建立之后,真正的数据传输阶段就不需要http参与了

    图片来自菜鸟教程

    # WebSocket的优点

      ws协议相比于HTTP协议,它具有以下优势:

    • 全双工通信能力:支持客户端和服务端主动给对方发送消息
    • 高实时性:Ajax轮询只是不断的请求,而服务端检测到更新主动推送才是真正意义上的实时。
    • 高效节能:HTTP协议请求一般都会有较长的头部,而需要实时更新的数据可能就一点点,这就造成了带宽很多不必要的消耗。而ws协议控制数据包的头部比较小,一般只有十个字节左右。
    • 支持扩展: ws协议定义了扩展,用户可以扩展协议,或实现自定义子协议。
    • 没有跨域限制:不是xhr请求,没有同源策略的限制

    # WebSocket的第一次握手

      虽说ws支持双向通讯能力,但请求必须是由客户发起。由于发起时是一个http握手,因此格式如下

    GET ws://localhost:3000/ws/chat HTTP/1.1
    Host: localhost
    Upgrade: websocket
    Connection: Upgrade
    Origin: http://localhost:3000
    Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== // 客户端随机串
    Sec-WebSocket-Version: 13
    

    值得注意的是:
    (1)其只能发GET请求,且不再是 http://... 而是换成了 ws://... 开头的地址
    (2)请求头Upgrade: websocketConnection: Upgrade表示该连接将要被升级为WebSocket连接;
    (3)Sec-WebSocket-Key 标识连接的Key串(下方有更多解释)
    (4)Sec-WebSocket-Version 指定了WebSocket的协议版本。

    如果服务器识别key正确,会接收这个请求,就会响应如下:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=  // 服务端随机串
    

    服务端Accept串是根据客户端随机串计算出来的,计算规则为:(1)与固定串拼接,(2)执行sha1算法,(3)转为base64字符串。这对Key/Accept需ws客户端和服务端提前约定,目的是为了避免非法ws请求等一些常见的意外情况。并不能确保数据安全性,毕竟算法公开且简单。公式如下:

    toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
    

    响应码101表示将切换协议,更改后的协议就是Upgrade: websocket指定的WebSocket协议。当连接建立成功后,双方就可以自由通讯消息了。消息一般分两种:(1)文本,(2)二进制数据。开发中会使用JSON文本数据比较直观。

    # ws为什么能实现全双工通讯

      前文多次遇到 全双工通信 字眼,意思就是客户端和服务端能随时给对方发送消息。好像理解了但又朦朦胧胧。这里解释一下:

    • 单工: 数据传输只支持在一个方向上的传输,同时只能有一方发送或接收消息。
    • 半双工:数据允许在两个方向上传输,但任一时刻,只允许有一方在传输,是一种切换方向的单工通信
    • 全双工:任何时刻都允许两个方向进行数据传输,不受对方限制。

      HTTP 和WebSocket 都是基于TCP传输协议的,其实TCP本身是支持全双工通讯的,而HTTP协议的请求,因为其应答机制限制了全双工通信。当第一次握手完成后,协议由HTTP切换成了WebSocket,ws连接建立,其实只是简单规定了下:后续通讯不再使用http协议,双发可以互相发送数据了。

    http、WebSocket及TCP的关系(图片摘自网络)

    # 安全的WebSocket通讯

      与 HTTPS 类似,安全的ws连接使用的是wss://...开头的请求,它首先会通过https创建安全的连接,升级协议后,底层通信依然走的 SSL/TLS 协议

    # 连接保持 - 心跳

      WebSocket为了保持客户端与服务端的实时双向通讯,需保持TCP通道链接没有断开。然而长时间没有数据往来的连接,会浪费一些连接资源,网络链路同样可能被网关关闭,毕竟网关不是我们能控制的。因此链路链接就需要提示说明还在使用周期内,这个提示就是心跳来实现的。

    • 发送方 --> 接收方: ping
    • 接收方 --> 发送方: pong
      举例,ws服务端向客户端发送ping,代码如下
    ws.ping('', false, true)
    

    $ WebSocket API

      理解了WebSocket的概念及相应的特征后,来看看怎么上手编写

    # 创建WebSocket实例

      ws提供了WebSocket(url[, protocals])构造函数来返回实例化ws对象。参数一表示要连接的URL,参数二表示可接受的子协议。

    let socket= new WebSocket('http://localhost:8080')
    

      执行以上代码,浏览器就开始尝试创建连接,与 xhr 的readystatechange 类似的是,ws连接也有一个表示当前状态的属性readyState

    # 连接状态-readyState 只读

      用于返回当前WebSocket连接的状态,其值即含义如下

    状态含义
    0 WebSocket.CONNECTING
    1 WebSocket.OPEN
    2 WebSocket.CLOSING
    3 WebSocket.CLOSED

    一个ws连接各个状态的执行时刻如下

    let socket = new WebSocket('http://localhost:8080')
    // 正在创建连接
    console.log('[readyState]:', socket.readyState) // 0
    
    // 连接建立成功后触发onopen回调
    socket.onopen = function() {
      console.log('connected,[readyState]:', socket.readyState) // 1
      // 发送消息
      socket.send('from client: Hello')
    }
    
    // 从服务端收到信息触发onmessage回调
    socket.onmessage = function() {
      console.log('received,[readyState]:', socket.readyState) // 1
      // 发送消息
      socket.send('from client: Hello')
    }
    
    // 连接失败触发onerror回调
    socket.onerror = function() {
      console.log('connect error, [readyState]:', socket.readyState)  // 3
    }
    
    // 调用关闭连接,状态立刻变成2(正在关闭)。关闭成功触发onclose变成3
    socket.close()
    
    // 连接关闭触发onclose回调,有回调参数
    socket.onclose = function(event) {
      const { code, reason, wasClean } = event
      console.log('connect closed, [readyState]:', socket.readyState) // 3
      console.log(code, reason, wasClean) // wasClean表示连接是否已经关闭。boolean
    }
    

      当readyState的值从 0 变成 1 后,客户端和服务端就可以通讯了。

    # 方法

    - 发送数据 send()

      发送数据一定是伴随在连接已经打开的情况下

    socket.addEventListener('open', function(event) {
      sokcet.send('hello server')
    })
    
    - 关闭连接 close()

      关闭当前连接。可以传 0/1/2 个参数。code解释关闭原因的状态码。reason解释关闭原因的描述(限制123个字节)。

    sokcet.close([code[, reason]])
    

    如果未传参数,会默认code1005,意为:无参数,未提供关闭原因状态码。查看 状态码详情。如果提供一个无效的状态码,会抛出异常INVALID_ACCESS_ERR

    # 事件

    - 连接已建立 onopen
    socket.addEventListener('open', function(event)  {
      // TODO: send message
    });
    
    - 接收服务端消息回调 onmessage

      当服务器向客户端发来消息时,WebSocket对象会触发message事件。这个message事件与其他传递消息的协议类似,也是把返回的数据保存在event.data属性中

    socket.addEventListener('message', function(event)  {
      var data = event.data;
      // TODO:
    });
    
    - 关闭连接的回调 onclose
    socket.addEventListener('close', function(event)  {
      const { code, reason, wasClean } = event
      // TODO:
    });
    
    - 连接失败的回调 onerror
    socket.addEventListener('error', function(event)  {
      console.error("WebSocket error observed:", event)
    });
    

    # 属性

    - 当前剩余未发送数据 bufferedAmount 只读

      用于返回已经被send()方法放入队列但还没有被发送到网络中的数据的字节数,只有发送完成它才会被重置为0。如果发送过程中连接被关闭不会重置,不断的调用send()该值会不断增长。

    if (ws.bufferedAmount === 0){
        console.log("发送已完成");
    } else {
        console.log("还有", ws.bufferedAmount, "数据没有发送");
    }
    
    - 连接二进制类型 binaryType 只读

      返回websocket连接所传输二进制数据的类型

    const binaryType = socket.binaryType
    
    - 已选择的扩展值 extensions 只读

      返回服务器已选择的扩展值

    const extensions = socket.extensions 
    
    - 子协议 protocol 只读

      返回服务器端选中的子协议的名字;也就是在实例化WebSocket对象时,在参数protocols中指定的字符串

    const protocol = socket.protocol  
    
    - 子协议 url 只读

      返回值为当构造函数创建WebSocket实例对象时URL的绝对路径。

    const url = socket.url 
    

    $ 一个服务端实例

    这里提供一个简单的例子,引入了ws库实现。也可以使用socket.io

    var app = require('express')();
    var server = require('http').Server(app);
    var WebSocket = require('ws');
    
    var wss = new WebSocket.Server({ port: 8080 });
    
    wss.on('connection', function connection(ws) {
        console.log('server: receive connection.');
        
        ws.on('message', function incoming(message) {
            console.log('server: received: %s', message);
        });
    
        ws.send('world');
    });
    
    app.get('/', function (req, res) {
      res.sendfile(__dirname + '/index.html');
    });
    
    app.listen(3000);
    

    结束语

    参考文献
    WebSocket-菜鸟教程
    WebSocket-MDN
    WebSocket-廖雪峰的官方网站

    相关文章

      网友评论

        本文标题:H5数据实时方案「WebSocket」

        本文链接:https://www.haomeiwen.com/subject/xbtubctx.html