正确理解IM长链接的心跳及重连机制,并动手实现(有完整IM源码)

一、引言

说道“心跳”这个词你们都不陌生,固然不是指男女之间的心跳,而是和长链接相关的。顾名思义就是证实是否还活着的依据。html

什么场景下须要心跳呢?目前咱们接触到的大可能是一些基于长链接的应用须要心跳来“保活”。git

因为在长链接的场景下,客户端和服务端并非一直处于通讯状态,若是双方长期没有沟通则双方都不清楚对方目前的状态,因此须要发送一段很小的报文告诉对方“我还活着”。github

同时还有另外几个目的:算法

1)服务端检测到某个客户端迟迟没有心跳过来能够主动关闭通道,让它下线;apache

2)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的链接。编程

本文正好借着在CIM系统中有这样两个需求(CIM是本文做者从零开发的一个学习性质的IM系统,详见《拿起键盘就是干:跟我一块儿徒手开发一套分布式IM系统》),正好来聊一聊我是如何理解IM长链接的心跳及重连机制,以及又是怎么踩坑已及填坑的。微信

本文配套的CIM源码地址:网络

主要镜像:https://github.com/crossoverJie/cimapp

备用镜像:https://github.com/52im/cim框架

阅读本文须要必定的网络编程以及Netty方面的知识。

二、关于做者

crossoverJie(陈杰): 90后,毕业于重庆信息工程学院,现供职于重庆猪八戒网络有限公司。

做者的博客:https://crossoverjie.top

做者的Github:https://github.com/crossoverJie

本文做者的其它文章:

拿起键盘就是干:跟我一块儿徒手开发一套分布式IM系统

技术干货:从零开始,教你设计一个百万级的消息推送系统

三、相关文章

➊ 有关网络心跳保活方面的理论文章:

为什么基于TCP协议的移动端IM仍然须要心跳保活机制?

微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)

移动端IM实践:实现Android版微信的智能心跳机制

移动端IM实践:WhatsApp、Line、微信的心跳策略分析

一文读懂即时通信应用中的网络心跳包机制:做用、原理、实现思路等

融云技术分享:融云安卓端IM产品的网络链路保活技术实践

➋ 有关网络心跳保活方面的实践文章:

MobileIMSDK——一套开源的原创移动端即时通信框架(有完整的心跳保活逻辑和代码实现)》

一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)

手把手教你用Netty实现网络通讯程序的心跳机制、断线重连机制

适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

拿起键盘就是干:跟我一块儿徒手开发一套分布式IM系统

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)

四、心跳实现方式

心跳其实有两种实现方式:

1)TCP 协议实现(keepalive 机制,详见《TCP/IP详解 卷1:协议-第23章 TCP的保活定时器》);

2)应用层本身实现。

因为 TCP 协议过于底层,对于开发者来讲维护性、灵活度都比较差同时还依赖于操做系统(详见:《为什么基于TCP协议的移动端IM仍然须要心跳保活机制?》)。

因此咱们这里所讨论的都是应用层的实现:

 

如上图所示,在应用层一般是由客户端发送一个心跳包 ping 到服务端,服务端收到后响应一个 pong 代表双方都活得好好的。一旦其中一端延迟 N 个时间窗口没有收到消息则进行不一样的处理。

五、客户端自动重连

先拿客户端来讲吧,每隔一段时间客户端向服务端发送一个心跳包,同时收到服务端的响应。

常规的实现应当是:

1)开启一个定时任务,按期发送心跳包;

2)收到服务端响应后更新本地时间;

3)再有一个定时任务按期检测这个“本地时间”是否超过阈值;

4)超事后则认为服务端出现故障,须要重连。

这样确实也能实现心跳,但并不友好。

在正常的客户端和服务端通讯的状况下,定时任务依然会发送心跳包;这样就显得没有意义,有些多余。因此理想的状况应当是客户端收到的写消息空闲时才发送这个心跳包去确认服务端是否健在。

好消息是 Netty 已经为咱们考虑到了这点,自带了一个开箱即用的 IdleStateHandler 专门用于心跳处理。

来看看 cim 中的实现:

 

在 pipeline 中加入了一个 10秒没有收到写消息的 IdleStateHandler,到时他会回调 ChannelInboundHandler 中的 userEventTriggered 方法。

 

因此一旦写超时就立马向服务端发送一个心跳(作的更完善应当在心跳发送失败后有必定的重试次数)。

这样也就只有在空闲时候才会发送心跳包。但一旦间隔许久没有收到服务端响应进行重连的逻辑应当写在哪里呢?

先来看这个示例:

当收到服务端响应的 pong 消息时,就在当前 Channel 上记录一个时间,也就是说后续能够在定时任务中取出这个时间和当前时间的差额来判断是否超过阈值。

超过则重连。

 

 

同时在每次心跳时候都用当前时间和以前服务端响应绑定到 Channel 上的时间相减判断是否须要重连便可。

也就是  heartBeatHandler.process(ctx); 的执行逻辑。

伪代码以下:

@Override

public void process(ChannelHandlerContext ctx) throws Exception {

 

    longheartBeatTime = appConfiguration.getHeartBeatTime() * 1000;

 

    Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel());

    longnow = System.currentTimeMillis();

    if(lastReadTime != null&& now - lastReadTime > heartBeatTime){

        reconnect();

    }

}

六、IdleStateHandler 误区

一切看起来也没毛病,但实际上却没有这样实现重连逻辑。最主要的问题仍是对 IdleStateHandler 理解有误。

咱们假设下面的场景:

1)客户端经过登陆连上了服务端并保持长链接,一切正常的状况下双方各发心跳包保持链接;

2)这时服务端突入出现 down 机,那么理想状况下应当是客户端迟迟没有收到服务端的响应从而 userEventTriggered 执行定时任务;

3)判断当前时间 - UpdateWriteTime > 阈值 时进行重连。

但却事与愿违,并不会执行 二、3两步。

由于一旦服务端 down 机、或者是与客户端的网络断开则会回调客户端的 channelInactive 事件。

IdleStateHandler 做为一个 ChannelInbound 也重写了 channelInactive() 方法。

 

\

 

这里的 destroy() 方法会把以前开启的定时任务都给取消掉。因此就不会再有任何的定时任务执行了,也就不会有机会执行这个重连业务。

七、靠谱实现

所以咱们得有一个单独的线程来判断是否须要重连,不依赖于 IdleStateHandler。

因而 cim 在客户端感知到网络断开时就会开启一个定时任务:

 

之因此不在客户端启动就开启,是为了节省一点线程消耗。网络问题虽然不可避免,但在须要的时候开启更能节省资源。

 

 

在这个任务重其实就是执行了重连,限于篇幅具体代码就不贴了,感兴趣的能够自行查阅。

同时来验证一下效果:

启动两个服务端,再启动客户端链接上一台并保持长链接。这时忽然手动关闭一台服务,客户端能够自动重连到可用的那台服务节点。

 

 

启动客户端后服务端也能收到正常的 ping 消息:

利用 :info 命令查看当前客户端的连接状态发现连的是 9000端口。

 

:info 是一个新增命令,能够查看一些客户端信息。

这时我关掉链接上的这台节点:

1kill-9 2142

 

 

这时客户端会自动重连到可用的那台节点。这个节点也收到了上线日志以及心跳包。

八、服务端自动剔除离线客户端

如今来看看服务端,它要实现的效果就是延迟 N 秒没有收到客户端的 ping 包则认为客户端下线了,在 cim 的场景下就须要把他踢掉置于离线状态。

有关消息发送误区:

这里依然有一个误区,在调用 ctx.writeAndFlush() 发送消息获取回调时。

其中是 isSuccess 并不能做为消息发送成功与否的标准:

 

也就是说即使是客户端直接断网,服务端这里发送消息后拿到的 success 依旧是 true。这是由于这里的 success 只是告知咱们消息写入了 TCP 缓冲区成功了而已。

和我以前有着同样错误理解的不在少数,这是 Netty 官方给的回复:

 

相关 issue:https://github.com/netty/netty/issues/4915

因此咱们不能依据此来关闭客户端的链接,而是要像上文同样判断 Channel 上绑定的时间与当前时间只差是否超过了阈值。

 

 

 

以上则是 cim 服务端的实现,逻辑和开头说的一致,也和 Dubbo 的心跳机制有些相似。

因而来作个试验:正常通讯的客户端和服务端,当我把客户端直接断网时,服务端会自动剔除客户端。

 

 

九、本文小结

这样就实现了文初的两个要求:

1)服务端检测到某个客户端迟迟没有心跳过来能够主动关闭通道,让它下线;

2)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的链接。

同时也踩了两个误区,坑一我的踩就能够了,但愿看过本文的都有所收获避免踩坑。

本文全部相关代码都在此处,感兴趣的能够自行查看:

主要镜像:https://github.com/crossoverJie/cim

备用镜像:https://github.com/52im/cim

相关文章
相关标签/搜索