「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」
一、前言
为了让消息通过 “长连接” 实现可靠投递,那就要维护好这个 “长连接”。
“心跳机制” 可解决了以下三方面的问题:
- 降低服务端连接维护无效连接的开销
- 支持客户端快速识别无效连接,自动断线重连
- 连接保活,避免被运营商
NAT超时断开
IM 系统中心跳机制结合 TCP KeepAlive 和 应用层心跳来完成:
TCP KeepAlive: 传输层的心跳探测连接可用性,可用来排除网络不可用- 应用层心跳:传输层的检测,可用来排除应用服务不可用
二、心跳检测方式
心跳检测方式有:
TCP Keepalive- 应用层心跳
- 智能心跳
(1)TCP Keepalive
TCP 的 Keepalive 作为操作系统的 TCP/IP 协议栈实现的一部分,对于本机的 TCP 连接,会在连接空闲期按一定的频次,自动发送不携带数据的探测报文,来探测对方是否存活。
HTTP 1.1之后默认开启。
默认的三个配置项:
tcp_keepalive_time:心跳周期是 2 小时 (7200s),即每次正常发送心跳的周期。tcp_keepalive_intvl:KeepAlive探测包的发送间隔 75stcp_keepalive_probes:在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为 9 次
# 开发环境查看
$ cat /proc/sys/net/ipv4/tcp_keepalive_time
600
$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
15
$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
3
# 通过修改 etc/sysctl.conf
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
犹如 nginx 中设置:nginx.conf
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 75s;
keepalive_requests 1000;
types_hash_max_size 2048;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
}
TCP Keepalive 的缺陷:无法检测应用服务是否可用。
TCP处于传输层:检测IP和 端口是否有效- 如果应用服务代码死锁、假死等,
TCP Keepalive就无法检测
(2)应用层心跳
为了解决
TCP Keepalive存在的一些不足的问题,很多IM服务使用应用层心跳来提升探测的灵活性和准确性。
应用层心跳相比 TCP Keepalive:由于需要在应用层进行发送和接收的处理,因此更能反映应用的可用性,而不是仅仅代表网络可用。
原理:应用层心跳实际上就是客户端每隔一定时间间隔,向 IM 服务端发送一个业务层的数据包告知自身存活。
灵活可设置的心跳间隔在节省网络流量和保活层面优势更明显:
稍微复杂的策略是客户端在发送数据空闲后才发送心跳包。
WhatApps:应用层心跳间隔为 30s 和 1分钟- 微信:应用层心跳间隔为 4分半
- 微博:应用层心跳间隔为 2分钟
QQ:应用层心跳间隔为 45s
客户端和服务端,各自通过心跳机制来实现 “断线重连” 和 “资源清理”:
(3)智能心跳
在国内移动网络场景下,各个地方运营商在不同的网络类型下
NAT超时的时间差异性很大。
- 采用固定频率的应用层心跳在实现上虽然相对较为简单
- 但为了避免
NAT超时,只能将心跳间隔设置为小于所有网络环境下NAT超时的最短时间 - 虽然也能解决问题,但对于设备
CPU、电量、网络流量的资源无法做到最大程度的节约
优化这现象:采用 “智能心跳” 的方案
- 平衡“NAT 超时”
- “设备资源节约”
智能心跳:就是让心跳间隔能够根据网络环境来自动调整,通过不断自动调整心跳间隔的方式,逐步逼近 NAT 超时临界点,在保证 NAT 不超时的情况下尽量节约设备资源。
三、实战
(1)websocket 中心跳
服务端:
ChannelInitializer<SocketChannel> initializer = new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//先添加websocket相关的编解码器和协议处理器
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
pipeline.addLast(new WebSocketServerProtocolHandler("/", null, true));
//再添加服务端业务消息的总处理器
pipeline.addLast(websocketRouterHandler);
//服务端添加一个idle处理器,如果一段时间socket中没有消息传输,服务端会强制断开
// 读 0s 即不限制,写 0s 即不限制,总间隔
pipeline.addLast(new IdleStateHandler(0, 0, serverConfig.getAllIdleSecond()));
pipeline.addLast(closeIdleChannelHandler);
}
};
前端代码:
function init() {
if (window.WebSocket) {
websocket = new WebSocket("ws://127.0.0.1:8080");
websocket.onmessage = function (event) {
onmsg(event);
};
websocket.onopen = function () {
bind();
// 定时发送心跳
heartBeat.start();
}
websocket.onclose = function () {
reconnect();
};
websocket.onerror = function () {
reconnect();
};
} else {
alert("您的浏览器不支持WebSocket协议!");
}
}
/** 每2分钟发送一次心跳包,接收到消息或者服务端的响应又会重置来重新计时。 */
var heartBeat = {
timeout: 120000,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
start: function () {
var self = this;
this.timeoutObj = setTimeout(function () {
var sender_id = $("#sender_id").val();
var sendMsgJson = '{ "type": 0, "data": {"uid":' + sender_id + ',"timeout": 120000}}';
websocket.send(sendMsgJson);
self.serverTimeoutObj = setTimeout(function () {
websocket.close();
$("#ws_status").text("失去连接!");
}, self.timeout)
}, this.timeout)
},
}
(2)生产实战
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds,
0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);