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

30次阅读

共计 9347 个字符,预计需要花费 24 分钟才能阅读完成。

1、引言

说道“心跳”这个词大家都不陌生,当然不是指男女之间的心跳,而是和长连接相关的。顾名思义就是证明是否还活着的依据。

什么场景下需要心跳呢?目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。

由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态,所以需要发送一段很小的报文告诉对方“我还活着”。

同时还有另外几个目的:

1)服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线;
2)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。

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

本文配套的 CIM 源码地址:

主要镜像:[https://github.com/crossoverJ…
备用镜像:https://github.com/52im/cim

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

有关网络编程基础知识,请阅读以下资料:

《TCP/IP 详解 – 第 11 章·UDP:用户数据报协议》
《TCP/IP 详解 – 第 17 章·TCP:传输控制协议》
《TCP/IP 详解 – 第 18 章·TCP 连接的建立与终止》
《TCP/IP 详解 – 第 21 章·TCP 的超时与重传》
《通俗易懂 - 深入理解 TCP 协议(上):理论基础》(推荐)
《网络编程懒人入门(一):快速理解网络通信协议(上篇)》
《网络编程懒人入门(二):快速理解网络通信协议(下篇)》

有关 Netty 框架方面的知识,请阅读以下资料:

《Netty 源码在线阅读版》(推荐)
《Netty API 文档在线版》(推荐)
《新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析》
《写给初学者:Java 高性能 NIO 框架 Netty 的学习方法和进阶策略》
《少啰嗦!一分钟带你读懂 Java 的 NIO 和经典 IO 的区别》
《史上最强 Java NIO 入门:担心从入门到放弃的,请读这篇!》

学习交流:

– 即时通讯 / 推送技术开发交流 5 群:215477170[推荐]
– 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端 IM》

(本文同步发布于:http://www.52im.net/thread-2799-1-1.html)

2、关于作者

crossoverJie(陈杰): 90 后,毕业于重庆信息工程学院,现供职于重庆猪八戒网络有限公司。
作者的博客:https://crossoverjie.top
作者的 Github:https://github.com/crossoverJie

本文作者的其它文章:

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

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

3、相关文章

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

《为何基于 TCP 协议的移动端 IM 仍然需要心跳保活机制?》
《微信团队原创分享:Android 版微信后台保活实战分享(网络保活篇)》
《移动端 IM 实践:实现 Android 版微信的智能心跳机制》
《移动端 IM 实践:WhatsApp、Line、微信的心跳策略分析》
《一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等》
《融云技术分享:融云安卓端 IM 产品的网络链路保活技术实践》

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

《MobileIMSDK——一套开源的原创移动端即时通讯框架(有完整的心跳保活逻辑和代码实现)》
《一种 Android 端 IM 智能心跳算法的设计与实现探讨(含样例代码)》
《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》
《适合新手:从零开发一个 IM 服务端(基于 Netty,有完整源码)》
《拿起键盘就是干:跟我一起徒手开发一套分布式 IM 系统》
《自已开发 IM 有那么难吗?手把手教你自撸一个 Andriod 版简易 IM (有源码)》

4、心跳实现方式

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

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

2)应用层自己实现。

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

所以我们这里所讨论的都是应用层的实现:

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

5、客户端自动重连

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

常规的实现应当是:

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

6、IdleStateHandler 误区

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

我们假设下面的场景:

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

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

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

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

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

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

\

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

7、靠谱实现

因此我们得有一个单独的线程来判断是否需要重连,不依赖于 IdleStateHandler。

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

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

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

同时来验证一下效果:

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

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

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

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

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

1kill-9 2142

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

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

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

有关消息发送误区:

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

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

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

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

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

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

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

于是来做个试验:正常通信的客户端和服务端,当我把客户端直接断网时,服务端会自动剔除客户端。

9、本文小结

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

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

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

同时也踩了两个误区,坑一个人踩就可以了,希望看过本文的都有所收获避免踩坑。

本文所有相关代码都在此处,感兴趣的可以自行查看:

主要镜像:[https://github.com/crossoverJ…

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

附录:更多参考资料汇总

[1] IM 代码实践 (适合新手):
《自已开发 IM 有那么难吗?手把手教你自撸一个 Andriod 版简易 IM (有源码)》
《一种 Android 端 IM 智能心跳算法的设计与实现探讨(含样例代码)》
《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》
《详解 Netty 的安全性:原理介绍、代码演示(上篇)》
《详解 Netty 的安全性:原理介绍、代码演示(下篇)》
《微信本地数据库破解版(含 iOS、Android),仅供学习研究 [附件下载]》
《Java NIO 基础视频教程、MINA 视频教程、Netty 快速入门视频 [有源码]》
《轻量级即时通讯框架 MobileIMSDK 的 iOS 源码(开源版)[附件下载]》
《开源 IM 工程“蘑菇街 TeamTalk”2015 年 5 月前未删减版完整代码 [附件下载]》
《微信本地数据库破解版(含 iOS、Android),仅供学习研究 [附件下载]》
《NIO 框架入门(一):服务端基于 Netty4 的 UDP 双向通信 Demo 演示 [附件下载]》
《NIO 框架入门(二):服务端基于 MINA2 的 UDP 双向通信 Demo 演示 [附件下载]》
《NIO 框架入门(三):iOS 与 MINA2、Netty4 的跨平台 UDP 双向通信实战 [附件下载]》
《NIO 框架入门(四):Android 与 MINA2、Netty4 的跨平台 UDP 双向通信实战 [附件下载]》
《用于 IM 中图片压缩的 Android 工具类源码,效果可媲美微信 [附件下载]》
《高仿 Android 版手机 QQ 可拖拽未读数小气泡源码 [附件下载]》
《一个 WebSocket 实时聊天室 Demo:基于 node.js+socket.io [附件下载]》
《Android 聊天界面源码:实现了聊天气泡、表情图标(可翻页) [附件下载]》
《高仿 Android 版手机 QQ 首页侧滑菜单源码 [附件下载]》
《开源 libco 库:单机千万连接、支撑微信 8 亿用户的后台框架基石 [源码下载]》
《分享 java AMR 音频文件合并源码,全网最全》
《微信团队原创 Android 资源混淆工具:AndResGuard [有源码]》
《一个基于 MQTT 通信协议的完整 Android 推送 Demo [附件下载]》
《Android 版高仿微信聊天界面源码 [附件下载]》
《高仿手机 QQ 的 Android 版锁屏聊天消息提醒功能 [附件下载]》
《高仿 iOS 版手机 QQ 录音及振幅动画完整实现 [源码下载]》
《Android 端社交应用中的评论和回复功能实战分享[图文 + 源码]》
《Android 端 IM 应用中的 @人功能实现:仿微博、QQ、微信,零入侵、高可扩展[图文 + 源码]》
《仿微信的 IM 聊天时间显示格式(含 iOS/Android/Web 实现)[图文 + 源码]》
《Android 版仿微信朋友圈图片拖拽返回效果 [源码下载]》
《适合新手:从零开发一个 IM 服务端(基于 Netty,有完整源码)》
《拿起键盘就是干:跟我一起徒手开发一套分布式 IM 系统》
《正确理解 IM 长连接的心跳及重连机制,并动手实现(有完整 IM 源码)》
>> 更多同类文章 …… 
[2] 网络编程基础资料:
《TCP/IP 详解 – 第 11 章·UDP:用户数据报协议》
《TCP/IP 详解 – 第 17 章·TCP:传输控制协议》
《TCP/IP 详解 – 第 18 章·TCP 连接的建立与终止》
《TCP/IP 详解 – 第 21 章·TCP 的超时与重传》
《技术往事:改变世界的 TCP/IP 协议(珍贵多图、手机慎点)》
《通俗易懂 - 深入理解 TCP 协议(上):理论基础》
《通俗易懂 - 深入理解 TCP 协议(下):RTT、滑动窗口、拥塞处理》
《理论经典:TCP 协议的 3 次握手与 4 次挥手过程详解》
《理论联系实际:Wireshark 抓包分析 TCP 3 次握手、4 次挥手过程》
《计算机网络通讯协议关系图(中文珍藏版)》
《UDP 中一个包的大小最大能多大?》
《P2P 技术详解(一):NAT 详解——详细原理、P2P 简介》
《P2P 技术详解(二):P2P 中的 NAT 穿越(打洞) 方案详解》
《P2P 技术详解(三):P2P 技术之 STUN、TURN、ICE 详解》
《通俗易懂:快速理解 P2P 技术中的 NAT 穿透原理》
《高性能网络编程(一):单台服务器并发 TCP 连接数到底可以有多少》
《高性能网络编程(二):上一个 10 年,著名的 C10K 并发连接问题》
《高性能网络编程(三):下一个 10 年,是时候考虑 C10M 并发问题了》
《高性能网络编程(四):从 C10K 到 C10M 高性能网络应用的理论探索》
《高性能网络编程(五):一文读懂高性能网络编程中的 I / O 模型》
《高性能网络编程(六):一文读懂高性能网络编程中的线程模型》
《不为人知的网络编程(一):浅析 TCP 协议中的疑难杂症(上篇)》
《不为人知的网络编程(二):浅析 TCP 协议中的疑难杂症(下篇)》
《不为人知的网络编程(三):关闭 TCP 连接时为什么会 TIME_WAIT、CLOSE_WAIT》
《不为人知的网络编程(四):深入研究分析 TCP 的异常关闭》
《不为人知的网络编程(五):UDP 的连接性和负载均衡》
《不为人知的网络编程(六):深入地理解 UDP 协议并用好它》
《不为人知的网络编程(七):如何让不可靠的 UDP 变的可靠?》
《不为人知的网络编程(八):从数据传输层深度解密 HTTP》
《不为人知的网络编程(九):理论联系实际,全方位深入理解 DNS》
《网络编程懒人入门(一):快速理解网络通信协议(上篇)》
《网络编程懒人入门(二):快速理解网络通信协议(下篇)》
《网络编程懒人入门(三):快速理解 TCP 协议一篇就够》
《网络编程懒人入门(四):快速理解 TCP 和 UDP 的差异》
《网络编程懒人入门(五):快速理解为什么说 UDP 有时比 TCP 更有优势》
《网络编程懒人入门(六):史上最通俗的集线器、交换机、路由器功能原理入门》
《网络编程懒人入门(七):深入浅出,全面理解 HTTP 协议》
《网络编程懒人入门(八):手把手教你写基于 TCP 的 Socket 长连接》
《网络编程懒人入门(九):通俗讲解,有了 IP 地址,为何还要用 MAC 地址?》
《技术扫盲:新一代基于 UDP 的低延时网络传输层协议——QUIC 详解》
《让互联网更快:新一代 QUIC 协议在腾讯的技术实践分享》
《现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障》
《聊聊 iOS 中网络编程长连接的那些事》
《移动端 IM 开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”》
《移动端 IM 开发者必读(二):史上最全移动弱网络优化方法总结》
《IPv6 技术详解:基本概念、应用现状、技术实践(上篇)》
《IPv6 技术详解:基本概念、应用现状、技术实践(下篇)》
《从 HTTP/0.9 到 HTTP/2:一文读懂 HTTP 协议的历史演变和设计思路》
《脑残式网络编程入门(一):跟着动画来学 TCP 三次握手和四次挥手》
《脑残式网络编程入门(二):我们在读写 Socket 时,究竟在读写什么?》
《脑残式网络编程入门(三):HTTP 协议必知必会的一些知识》
《脑残式网络编程入门(四):快速理解 HTTP/ 2 的服务器推送(Server Push)》
《脑残式网络编程入门(五):每天都在用的 Ping 命令,它到底是什么?》
《脑残式网络编程入门(六):什么是公网 IP 和内网 IP?NAT 转换又是什么鬼?》
《以网游服务端的网络接入层设计为例,理解实时通信的技术挑战》
《迈向高阶:优秀 Android 程序员必知必会的网络基础》
《全面了解移动端 DNS 域名劫持等杂症:技术原理、问题根源、解决方案等》
《美图 App 的移动端 DNS 优化实践:HTTPS 请求耗时减小近半》
《Android 程序员必知必会的网络通信传输层协议——UDP 和 TCP》
《IM 开发者的零基础通信技术入门(一):通信交换技术的百年发展史(上)》
《IM 开发者的零基础通信技术入门(二):通信交换技术的百年发展史(下)》
《IM 开发者的零基础通信技术入门(三):国人通信方式的百年变迁》
《IM 开发者的零基础通信技术入门(四):手机的演进,史上最全移动终端发展史》
《IM 开发者的零基础通信技术入门(五):1G 到 5G,30 年移动通信技术演进史》
《IM 开发者的零基础通信技术入门(六):移动终端的接头人——“基站”技术》
《IM 开发者的零基础通信技术入门(七):移动终端的千里马——“电磁波”》
《IM 开发者的零基础通信技术入门(八):零基础,史上最强“天线”原理扫盲》
《IM 开发者的零基础通信技术入门(九):无线通信网络的中枢——“核心网”》
《IM 开发者的零基础通信技术入门(十):零基础,史上最强 5G 技术扫盲》
《IM 开发者的零基础通信技术入门(十一):为什么 WiFi 信号差?一文即懂!》
《IM 开发者的零基础通信技术入门(十二):上网卡顿?网络掉线?一文即懂!》
《IM 开发者的零基础通信技术入门(十三):为什么手机信号差?一文即懂!》
《IM 开发者的零基础通信技术入门(十四):高铁上无线上网有多难?一文即懂!》
《IM 开发者的零基础通信技术入门(十五):理解定位技术,一篇就够》
《百度 APP 移动端网络深度优化实践分享(一):DNS 优化篇》
《百度 APP 移动端网络深度优化实践分享(二):网络连接优化篇》
《百度 APP 移动端网络深度优化实践分享(三):移动端弱网优化篇》
《技术大牛陈硕的分享:由浅入深,网络编程学习经验干货总结》
《可能会搞砸你的面试:你知道一个 TCP 连接上能发起多少个 HTTP 请求吗?》
《知乎技术分享:知乎千万级并发的高性能长连接网关技术实践》
>> 更多同类文章 ……
[3] NIO 异步网络编程资料:
《Java 新一代网络编程模型 AIO 原理及 Linux 系统 AIO 介绍》
《有关“为何选择 Netty”的 11 个疑问及解答》
《开源 NIO 框架八卦——到底是先有 MINA 还是先有 Netty?》
《选 Netty 还是 Mina:深入研究与对比(一)》
《选 Netty 还是 Mina:深入研究与对比(二)》
《NIO 框架入门(一):服务端基于 Netty4 的 UDP 双向通信 Demo 演示》
《NIO 框架入门(二):服务端基于 MINA2 的 UDP 双向通信 Demo 演示》
《NIO 框架入门(三):iOS 与 MINA2、Netty4 的跨平台 UDP 双向通信实战》
《NIO 框架入门(四):Android 与 MINA2、Netty4 的跨平台 UDP 双向通信实战》
《Netty 4.x 学习(一):ByteBuf 详解》
《Netty 4.x 学习(二):Channel 和 Pipeline 详解》
《Netty 4.x 学习(三):线程模型详解》
《Apache Mina 框架高级篇(一):IoFilter 详解》
《Apache Mina 框架高级篇(二):IoHandler 详解》
《MINA2 线程原理总结(含简单测试实例)》
《Apache MINA2.0 开发指南(中文版)[附件下载]》
《MINA、Netty 的源代码(在线阅读版)已整理发布》
《解决 MINA 数据传输中 TCP 的粘包、缺包问题(有源码)》
《解决 Mina 中多个同类型 Filter 实例共存的问题》
《实践总结:Netty3.x 升级 Netty4.x 遇到的那些坑(线程篇)》
《实践总结:Netty3.x VS Netty4.x 的线程模型》
《详解 Netty 的安全性:原理介绍、代码演示(上篇)》
《详解 Netty 的安全性:原理介绍、代码演示(下篇)》
《详解 Netty 的优雅退出机制和原理》
《NIO 框架详解:Netty 的高性能之道》
《Twitter:如何使用 Netty 4 来减少 JVM 的 GC 开销(译文)》
《绝对干货:基于 Netty 实现海量接入的推送服务技术要点》
《Netty 干货分享:京东京麦的生产级 TCP 网关技术实践总结》
《新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析》
《写给初学者:Java 高性能 NIO 框架 Netty 的学习方法和进阶策略》
《少啰嗦!一分钟带你读懂 Java 的 NIO 和经典 IO 的区别》
《史上最强 Java NIO 入门:担心从入门到放弃的,请读这篇!》
《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》
>> 更多同类文章 ……

(本文同步发布于:http://www.52im.net/thread-2799-1-1.html)

正文完
 0