聊聊websocket的那些事儿🤩

2,523 阅读5分钟

我正在参加「掘金·启航计划」

这篇文章 中,我们聊了如何使用 SSE 的方式实现服务器的单向推送,但如果我们想实现与服务器的双向通信,就必须使用本文的主角了,即 websocket

废话不多说,直接进入正题吧~

ppx2.jpg

什么是websocket

websocket 翻译过来就是 web套接字,用于给浏览器提供与服务器进行 双向通信 的能力,与HTTP不同,它以 ws://或wss:// 开头,它是一个有状态协议,这意味着客户端和服务器之间的连接将保持活动状态,即 长连接,连接的断开可以由浏览器或者服务器发起

浏览器里提供了 WebSocket 构造函数,用于创建WebSocket连接,当我们使用 new WebSocket(url,[protocol]) 后,便可以得到一个WebSocket实例,该实例上有如下的属性、方法和监听事件

  • protocol:是个只读属性,用于返回服务器端选中的子协议的名字,这是一个在创建WebSocket对象时,在参数[protocols]中指定的字符串,当没有已建立的链接时为空串
  • readyState:返回当前WebSocket实例的连接状态,是个只读属性,分别有如下取值
    • 0:正在连接中
    • 1:已经连接并且可以通讯
    • 2:连接正在关闭
    • 3:连接已关闭或者没有连接成功
  • bufferedAmount:只读属性,用于返回已经被send方法放入队列中但还没有被发送到网络中的数据的字节数,一旦队列中的所有数据被发送至网络,则该属性值将被重置为 0,但是,若在发送过程中连接被关闭,则属性值不会重置为0
  • url:只读属性,返回值为构造函数创建WebSocket实例时传入的url
  • close方法:关闭当前连接
  • send方法:通过该连接发送数据,该方法将需要通过 WebSocket 连接传输至服务器的数据 排入队列,并根据所需要传输的 data bytes 的大小来增加 bufferedAmount 的值。若数据无法传输(例如数据需要缓存而缓冲区已满)时,套接字会自行关闭,需要注意的是,发送的数据格式只支持如下几种
    • USVString:文本字符串,字符串将以 UTF-8 格式添加到缓冲区,并且 bufferedAmount 将加上该字符串以 UTF-8 格式编码时的字节数的值
    • ArrayBuffer:使用有类型的数组对象发送底层二进制数据;其二进制数据内存将被缓存于缓冲区,bufferedAmount 将加上所需字节数的值
    • Blob:将队列 blob 中的原始数据以二进制传输,bufferedAmount 将加上原始数据的字节数的值
    • ArrayBufferView:以二进制帧的形式发送任何JavaScript类数组对象,其二进制数据内容将被队列于缓冲区中,bufferedAmount 将加上必要字节数的值
  • close事件:用于指定连接关闭后的回调函数
  • error事件:用于指定连接失败后的回调函数
  • message事件:用于指定当从服务器接收到数据时的回调函数
  • open事件:用于指定连接成功后的回调函数

下面是一个简单的websocket使用示例

const socket = new WebSocket('ws://localhost:8080');

socket.addEventListener('open', function (event) {
    socket.send('Hello Server!');
});

socket.addEventListener('message', function (event) {
    console.log('Message from server ', event.data);
});

实战经验

当然如果我们想要实际在项目中去使用websocket,光是知道上述的一些基础概念是不够的,所以本节会给出一些在实战中的使用经验

666.jpg

心跳检测

我们知道了websocket是一个 长连接,因此就必须要有 鉴活机制,来保证通信信道的健康,整体的鉴活流程大致如下图

Untitled Diagram.drawio (2).png

代码示例如下

    let socket,heartBeatTimeout

    const sendHeatBeat = ()=>{
        //如果超过十秒未收到心跳响应消息,则重新建立连接
        socket.send('heartBeat-request')
        heartBeatTimeout = setTimeout(()=>{
            initWs()
        },10000)
    }
    const onOpen = ()=>{
        sendHeatBeat()
    }
    const onMessage = e => {
        if(e.data === 'heartBeat-response') {
            //收到心跳响应消息,则重新计时
            clearTimeout(heartBeatTimeout)
            heartBeatTimeout = null
            sendHeatBeat()
        }
    }
    const onError = () => {
        //连接失败后,需要重新建立连接
        initWs()
    }
    const destroyWs = ()=>{
        if(socket) {
            socket.close()
            socket.removeEventListener('open',onOpen)
            socket.removeEventListener('message',onMessage)
            socket.removeEventListener('error',onError)
            clearTimeout(heartBeatTimeout)
            heartBeatTimeout = null
        }
    }
    const initWs = ()=>{
     destroyWs()
     socket = new WebSocket('ws://localhost:8080');
     socket.addEventListener('open',onOpen)
     socket.addEventListener('message',onMessage)
     socket.addEventListener('error',onError)
    }
    
    initWs()

消息处理

在使用 http 的场景下,我们的交互产生的请求与响应是一一对应的,因此我们是能知道响应的数据作用于界面哪一部分的,而在websocket的场景下,从服务器推送过来的消息,默认情况下我们并不知道是对应哪一次请求或者说哪一次交互,从而没法知道返回的数据是需要如何使用的

因此,在websocket的场景下,响应消息的格式 尤为重要,在进行前后端联调时,一定要规范好数据格式,这样才能为后续的交互铺好路,于我看来,一个理想的消息格式如下

    {
        type // 用于标识该消息是用来干啥的
        parmas // 请求携带的参数,通过响应返回过来,可用于一些业务操作
        response // 业务数据 
    }

其中跟http响应最大的不同是 websocket 的消息里必须要有一个类似type的字段,用于告诉我们这个消息是用来干啥的,从而我们才能正确地使用这个消息来做页面更新或其他处理,本质上是借助类似type字段来实现 请求与消息的绑定,即实现一对一的对应关系

niubi.jpg

结语

虽然websocket给我们提供了双向通信的能力,但是如果我们不能正确处理由此带来的一些副作用,那么肯定会出问题的,因此如果要想使用它,程序的复杂度也会随之提升,但我相信通过本文的阅读,你一定可以熟练运用websocket了😉,加油!

都看到这里啦,如果本篇文章对你有帮助,希望能 点个赞👍 支持下啦,你们的支持才是我最大的动力!😘

R-C.gif