【IM】心跳机制

1,904 阅读1分钟

「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战

一、前言

为了让消息通过 “长连接” 实现可靠投递,那就要维护好这个 “长连接”。

“心跳机制” 可解决了以下三方面的问题:

  1. 降低服务端连接维护无效连接的开销
  2. 支持客户端快速识别无效连接,自动断线重连
  3. 连接保活,避免被运营商 NAT 超时断开

IM 系统中心跳机制结合 TCP KeepAlive 和 应用层心跳来完成:

  • TCP KeepAlive : 传输层的心跳探测连接可用性,可用来排除网络不可用
  • 应用层心跳:传输层的检测,可用来排除应用服务不可用



二、心跳检测方式

心跳检测方式有:

  • TCP Keepalive
  • 应用层心跳
  • 智能心跳

(1)TCP Keepalive

TCPKeepalive 作为操作系统的 TCP/IP 协议栈实现的一部分,对于本机的 TCP 连接,会在连接空闲期按一定的频次,自动发送不携带数据的探测报文,来探测对方是否存活。

HTTP 1.1 之后默认开启。

默认的三个配置项:

  • tcp_keepalive_time:心跳周期是 2 小时 (7200s),即每次正常发送心跳的周期。
  • tcp_keepalive_intvlKeepAlive 探测包的发送间隔 75s
  • tcp_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

客户端和服务端,各自通过心跳机制来实现 “断线重连” 和 “资源清理”:

2022-02-1315-41-21.png


(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);