赞
踩
在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与与WebRTC的异同:
从网上检索的答案,WebSocket大概有两种从协议角度出发的,检测对方存活的方式:
WebSocket只是一个应用层协议规范,其传输层是TCP,而TCP为长连接提供KeepAlive机制,可以定时发送心跳报文确认对方的存活,但一般是服务器端使用。因为是TCP传输控制层的机制,具体的实现要看操作系统,也就是说应用层接收到的连接状态是操作系统通知的,不同操作系统的资源调度是不一样的,例如何时发送探测报文(不包含有效数据的TCP报文)检测对方的存活,频率是多久,在不同的系统配置下存在差异。可能是2小时进行一次心跳检测,或许更短。如果连续没有收到对方的应答包,才会通知应用层已经断开连接。这就带来了不确定性。同时也意味着其它依赖该机制的应用层协议也会被影响。也就是说要利用这个过程进行检测,客户端要修改操作系统的TCP配置才行,在浏览器环境显然不行。参考1、参考2
WebSocket协议也有自身的保活机制,但需要通讯双方的实现。WebSocket通讯的数据帧会有一个4位的OPCODE,标记当前传输的数据帧类型,例如:0x8表示关闭帧、0x9表示ping帧、0xA表示pong帧、0x1普通文本数据帧等。www.rfc-editor.org
综上所述,探测对方存活的方式都是服务器主动进行心跳检测。浏览器并没有提供相关能力。为了能够在浏览器端实时探测后端的存活,或者说连接依旧可用,只能自己实现心跳检测。
首先我们先了解一下,目前的浏览器端的WebSocket何时会自动关闭WebSocket,并触发close事件呢?
也就是说建立正常连接后,中途浏览器端断网了,或者服务器没有发送关闭帧就关了连接,总之就是在连接无法再使用的情况下,浏览器没有接收到关闭帧,浏览器则会长时间保持连接状态。此时业务代码不去主动探测的话,是无法感知的。
另外通讯双方保持连接意味着需要长时间占用对方的资源。对于服务器端来说资源是非常宝贵的。长时间不活跃的连接,可能会被服务器应用层框架"优化"释放掉。
实例化一个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();
定义发送心跳包的逻辑:
准备
检测
启动一个定时器A。
定时器A执行,1.修改当前状态heartbeatStatus为等待中;2.发送心跳包;3.启动一个定时器B。
定时器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 = 0; WS.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上。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。