说道“心跳”这个词大家都不陌生,当然不是指男女之间的心跳,而是和长连接相关的。顾名思义就是证明是否还活着的依据。
什么场景下需要心跳呢?目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。
由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态,所以需要发送一段很小的报文告诉对方“我还活着”。
同时还有另外几个目的:
1)服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线; 2)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。
本文正好借着在CIM系统中有这样两个需求(CIM是本文作者从零开发的一个学习性质的IM系统,详见《 拿起键盘就是干:跟我一起徒手开发一套分布式IM系统 》),正好来聊一聊我是如何理解IM长连接的心跳及重连机制,以及又是怎么踩坑已及填坑的。
本文配套的CIM源码地址:
主要镜像: https://github.com/crossoverJie/cim
备用镜像: 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 )
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 理解有误。
我们假设下面的场景:
1)客户端通过登录连上了服务端并保持长连接,一切正常的情况下双方各发心跳包保持连接;
2)这时服务端突入出现 down 机,那么理想情况下应当是客户端迟迟没有收到服务端的响应从而 userEventTriggered 执行定时任务;
3)判断当前时间 - UpdateWriteTime > 阈值 时进行重连。
但却事与愿违,并不会执行 2、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
[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 )