当前位置:   article > 正文

为什么WebSocket需要前端心跳检测,有没有原生的检测机制?_websocket心跳检测

websocket心跳检测

本文代码 githubgiteenpm

在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。

设置心跳检测,一是让通讯双方确认对方依旧活跃,二是浏览器端及时检测当前网络线路可用性,保证消息推送的及时性。

你可能会想,WebSocket那么简陋的吗,居然不能自己判断连接状态?在了解前先来回顾一下计算机网络知识。

相关的网络知识

TCP/IP协议族四层结构:

  • 应用层:决定了向用户提供应用服务时通信的活动。HTTP、FTP、WebSocket都是应用层协议

  • (TCP)传输控制层:控制网络中两台主机的数据传输:将应用层数据(有必要时对应用层报文分段,例如一个完整的HTTP报文进行分段)发送到目标主机的特定端口的应用程序。给每个数据标记源端口、目标端口、分段后的序号。

  • (IP)网络层:将IP地址映射为目标主机的MAC地址,然后将TCP数据包(有必要时对数据分片)加入源IP、目标IP等信息后经过链路层扔到网络上让其找到目标主机。

  • 链路层:为IP网络层进行发送、接收数据报。将二进制数据包与在网线传输的网络电信号进行相互转换。

TCP是可靠的连接,握手建立连接后,发送方每发送一个TCP报文(对应用层报文分段后形成多个TCP报文),都会期望对方在指定时间里返回已收到的确认消息,如果超时没有回应,会重复发送,确保所有TCP报文可以到达对方,被对方按顺序拼接成应用层需要的完整报文。

WebSocket协议支持在TCP 上层引入 TLS 层,建立加密通信。

WebSocket与HTTP的异同:

  • WebSocket和HTTP一样是应用层协议,在传输层使用了TCP协议,都是可靠的连接。WebSocket在建立连接时,可以使用已有的HTTP的GET请求进行握手:客户端在请求头中将WebSocket协议版本等信息发生到服务器,服务器同意的话,会响应一个101的状态码。就是说一次HTTP请求和响应,即可轻松转换协议到WebSocket。

  • WebSocket可以互相发起请求。当有新消息时,服务器主动通知客户端,无需客户端主动向服务器询问。客户端也可以向后端发送消息。而HTTP中请求只能由客户端发起。

  • WebSocket是HTML5的内容,HTTP则是超文本传输协议,比HTML5诞生更早。

  • 在应用层,WebSocket的每个报文(在WebSocket中叫数据帧)会比HTTP报文(必须包含请求行、请求头、请求数据)更轻量。

    • WebSocket每个数据帧只有固定的头信息,不会有cookie等或者自定义的头信息。建立通讯后是一对一的,不需要携带验证信息。使用HTTP握手时,的握手请求会自动携带cookie。
    • WebSocket在应用层就会将大的数据进行分拆,而HTTP不会。

WebSocket与与WebRTC的异同:

  • WebRTC是一种通讯技术,由谷歌发起,被广大浏览器实现。用来建立浏览器和浏览器间的通讯,如视频通话等。而WebSocket是一种经过抽象的协议,可以实现为通讯技术。用来建立浏览器和服务器间的通讯。

协议中的心跳检测机制

从网上检索的答案,WebSocket大概有两种从协议角度出发的,检测对方存活的方式:

  1. WebSocket只是一个应用层协议规范,其传输层是TCP,而TCP为长连接提供KeepAlive机制,可以定时发送心跳报文确认对方的存活,但一般是服务器端使用。因为是TCP传输控制层的机制,具体的实现要看操作系统,也就是说应用层接收到的连接状态是操作系统通知的,不同操作系统的资源调度是不一样的,例如何时发送探测报文(不包含有效数据的TCP报文)检测对方的存活,频率是多久,在不同的系统配置下存在差异。可能是2小时进行一次心跳检测,或许更短。如果连续没有收到对方的应答包,才会通知应用层已经断开连接。这就带来了不确定性。同时也意味着其它依赖该机制的应用层协议也会被影响。也就是说要利用这个过程进行检测,客户端要修改操作系统的TCP配置才行,在浏览器环境显然不行。参考1参考2

  2. WebSocket协议也有自身的保活机制,但需要通讯双方的实现。WebSocket通讯的数据帧会有一个4位的OPCODE,标记当前传输的数据帧类型,例如:0x8表示关闭帧、0x9表示ping帧、0xA表示pong帧、0x1普通文本数据帧等。www.rfc-editor.org

    • 关闭数据帧,在任意一方要关闭通道时,发送给对方。例如浏览器的WebSocket实例调用close时,就会发送一个OPCODE为连接关闭的数据帧给服务器端,服务器端接收到后同样需要返回一个关闭数据帧,然后关闭底层的TCP连接。
    • ping数据帧,用于发送方询问对方是否存活,也就是心跳检测包。目前只有后端可以控制ping数据帧的发送。但浏览器端的WebSocket实例上没有对应的api可用。
    • pong数据帧,当WebSocket通讯一方接收到对方发送的ping数据帧后,需要及时回复一个内容一致,且OPCODE标记为pong的数据帧,告诉对方我还在。但目前回复pong是浏览器的自动行为,意味着不同浏览器会有差异。而且在js中没有相关api可以控制。

综上所述,探测对方存活的方式都是服务器主动进行心跳检测。浏览器并没有提供相关能力。为了能够在浏览器端实时探测后端的存活,或者说连接依旧可用,只能自己实现心跳检测。

浏览器端心跳检测的必要性

首先我们先了解一下,目前的浏览器端的WebSocket何时会自动关闭WebSocket,并触发close事件呢?

  • 握手时的WebSocket地址不可用。
  • 其它未知错误。
  • 正常连接状态下,接收到服务器端的关闭帧就会触发关闭回调。

也就是说建立正常连接后,中途浏览器端断网了,或者服务器没有发送关闭帧就关了连接,总之就是在连接无法再使用的情况下,浏览器没有接收到关闭帧,浏览器则会长时间保持连接状态。此时业务代码不去主动探测的话,是无法感知的。

另外通讯双方保持连接意味着需要长时间占用对方的资源。对于服务器端来说资源是非常宝贵的。长时间不活跃的连接,可能会被服务器应用层框架"优化"释放掉。

前端实现心跳检测

实例化一个WebSocket:

function connectWS() {
    const WS = new WebSocket("ws://127.0.0.1:7070/ws/?name=greaclar");
    // WebSocket实例上的事件

    // 当连接成功打开
    WS.addEventListener('open', () => {
        console.log('ws连接成功');
    });
    // 监听后端的推送消息
    WS.addEventListener('message', (event) => {
        console.log('ws收到消息', event.data);
    });
    // 监听后端的关闭消息,如果发送意外错误,这里也会触发
    WS.addEventListener('close', () => {
        console.log('ws连接关闭');
    });
    // 监听WS的意外错误消息
    WS.addEventListener('error', (error) => {
        console.log('ws出错', error);
    });
    return WS;
}

let WS = connectWS();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

心跳检测需要用到的实例方法:

// 发送消息,用来发送心跳包
WS.send('hello'); 
// 关闭连接,当发送心跳包不响应,需要重连时,最好先关闭
WS.close();
  • 1
  • 2
  • 3
  • 4

定义发送心跳包的逻辑:

准备

  • 申请一个变量heartbeatStatus,记录当前心跳检测状态,有三个状态:等待中,已收到应答、超时。
  • 监听WS实例的message事件,监听到就将heartbeatStatus改为:已收到应答。
  • 监听WS实例的open事件,打开后启动心跳检测。

检测

  • 启动一个定时器A。

  • 定时器A执行,1.修改当前状态heartbeatStatus为等待中;2.发送心跳包;3.启动一个定时器B。

    • 发送心跳包后,后端需要立刻推送一个内容一样的心跳应答包给前端,触发前端WS实例的message事件,继而将heartbeatStatus改为已收到应答。
  • 定时器B执行,检测当前heartbeatStatus状态:

    • 如果是已收到应答,证明定时器A执行后,服务器可以及时响应数据。继续启动定时器A,然后不断循环。

    • 如果是等待中,证明连接出现问题了,走关闭或者检测流程。

let WS = connectWS();
let heartbeatStatus = 'waiting';

WS.addEventListener('open', () => {
    // 启动成功后开启心跳检测
    startHeartbeat()
})

WS.addEventListener('message', (event) => {
    const { data } = event;
    console.log('心跳应答了,要把状态改为已收到应答', data);
    if (data === '"heartbeat"') {
        heartbeatStatus = 'received';
    }
})

function startHeartbeat() {
    setTimeout(() => {
        // 将状态改为等待应答,并发送心跳包
        heartbeatStatus = 'waiting';
        WS.send('heartbeat');
        // 启动定时任务来检测刚才服务器有没有应答
        waitHeartbeat();
    }, 1500)
}

function waitHeartbeat() {
    setTimeout(() => {
        console.log('检测服务器有没有应答过心跳包,当前状态', heartbeatStatus);
        if (heartbeatStatus === 'waiting') {
            // 心跳应答超时
            WS.close();
        } else {
            // 启动下一轮心跳检测
            startHeartbeat();
        }
    }, 1500)
}
  • 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
  • 37
  • 38

优化心跳检测

心跳检测异常,但close事件没有触发,大概率是双方之间的网络线路不佳,如果立马进行重连,会挤兑更多的网络资源,重连的失败概率更大,也可能阻塞用户的其它操作。

但也不排除确实是连接的问题,如服务器宕机、意外重启,同时没有告知浏览器需要把旧连接关闭。

所以一发生心跳不应答,个人推荐的做法是,发生延迟后,提醒用户网络异常正在修复中,让用户有个心理准备。然后多发一两个心跳包,连续不应答再提示用户掉线了,是否重连。如果中途正常了,就不需要重连,用户体验更好,对服务器的压力也更小。

// 以上代码需要修改的地方

// 添加一个变量来记录连续不应答次数
let retryCount = 0WS.addEventListener('message', (event) => {
    const { data } = event;
    console.log('心跳应答了,要把状态改为已收到应答', data);
    if (data === '"heartbeat"') {
        // 复位连续不应答次数
        retryCount = 0;
        heartbeatStatus = 'received';
    }
})

// 在等待应答的函数中添加重试的逻辑
function waitHeartbeat() {
    setTimeout(() => {
        // 心跳应答正常,启动下一轮心跳检测
        if (heartbeatStatus === 'received') {
            return startHeartbeat();
        }
        // 更新超时次数
        retryCount ++;
        // 心跳应答超时,但没有连续超过三次
        if (retryCount < 3) {
            alert('ws线路异常,正在检测中。')
            return startHeartbeat();
        }
        
        // 超时次数超过三次
        WS.close();
    }, 1500)
}
  • 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

最后,为了方便大家共同进步,本文已经把相关的逻辑封装为一个类,并且在npm中可下载玩一下,也已经开源到github上。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/242094
推荐阅读
相关标签
  

闽ICP备14008679号