为什么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();

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

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

定义发送心跳包的逻辑:

准备

  • 申请一个变量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)
}

优化心跳检测

心跳检测异常,但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)
}

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

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
前端使用 WebSocket 进行心跳检测的方法如下: 1. 建立 WebSocket 连接:在前端代码中使用 WebSocket 对象创建与服务器的连接。例如: ```javascript const socket = new WebSocket('ws://example.com/socket'); socket.onopen = function() { // 连接建立后执行心跳检测 startHeartbeat(); }; socket.onclose = function() { // 连接关闭后停止心跳检测 stopHeartbeat(); }; ``` 2. 发送心跳消息:在心跳检测函数中,定期向服务器发送心跳消息。可以使用 `socket.send()` 方法发送一个特定的心跳消息,通常是一个空字符串或者一个特定的标识符。例如: ```javascript function sendHeartbeat() { socket.send(''); } ``` 3. 接收心跳回复:在服务器端收到心跳消息后,应该回复一个特定的消息给前端,表示服务器正常运行。前端可以通过监听 `socket.onmessage` 事件来接收服务器的回复。 ```javascript socket.onmessage = function(event) { if (event.data === 'heartbeat') { // 收到心跳回复,继续下一次心跳检测 startHeartbeat(); } }; ``` 4. 定时执行心跳检测:使用 `setInterval` 函数定时执行心跳检测函数。通常情况下,心跳间隔为几秒钟到几分钟之间。例如: ```javascript let heartbeatInterval; function startHeartbeat() { heartbeatInterval = setInterval(sendHeartbeat, 5000); // 每 5 秒发送一次心跳消息 } function stopHeartbeat() { clearInterval(heartbeatInterval); } ``` 通过以上步骤,前端可以通过 WebSocket 进行心跳检测,保持与服务器的连接状态。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值