正确理解IM长连接的心跳及重连机制,并动手实现(有完整IM源码)
1、引言
说道“心跳”这个词大家都不陌生,当然不是指男女之间的心跳,而是和长连接相关的。顾名思义就是证明是否还活着的依据。
什么场景下需要心跳呢?目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。
由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态,所以需要发送一段很小的报文告诉对方“我还活着”。
同时还有另外几个目的:
1)服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线;
2)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。
本文正好借着在CIM系统中有这样两个需求(CIM是本文作者从零开发的一个学习性质的IM系统,详见《拿起键盘就是干:跟我一起徒手开发一套分布式IM系统》),正好来聊一聊我是如何理解IM长连接的心跳及重连机制,以及又是怎么踩坑已及填坑的。
本文配套的CIM源码地址:
阅读本文需要一定的网络编程以及Netty方面的知识。
2、关于作者
crossoverJie(陈杰): 90后,毕业于重庆信息工程学院,现供职于重庆猪八戒网络有限公司。
作者的博客:https://crossoverjie.top
作者的Github:https://github.com/crossoverJie
本文作者的其它文章:
3、参考资料
➊ 有关网络心跳保活方面的理论文章:
《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》
《移动端IM实践:WhatsApp、Line、微信的心跳策略分析》
➋ 有关网络心跳保活方面的实践文章:
《MobileIMSDK——一套开源的原创移动端即时通讯框架(有完整的心跳保活逻辑和代码实现)》
《一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)》
《手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制》
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)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。
同时也踩了两个误区,坑一个人踩就可以了,希望看过本文的都有所收获避免踩坑。
本文所有相关代码都在此处,感兴趣的可以自行查看:
正确理解IM长连接的心跳及重连机制,并动手实现(有完整IM源码)的更多相关文章
- IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token
本文引用了简书作者“骑小猪看流星”技术文章“Cookie.Session.Token那点事儿”的部分内容,感谢原作者. 1.前言 众所周之,IM是个典型的快速数据流交换系统,当今主流IM系统(尤其移动 ...
- 理解WebSocket心跳及重连机制(五)
理解WebSocket心跳及重连机制 在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件.这样会有:服务器会继续向客户端发送多余的 ...
- 从零开始实现简单 RPC 框架 9:网络通信之心跳与重连机制
一.心跳 什么是心跳 在 TPC 中,客户端和服务端建立连接之后,需要定期发送数据包,来通知对方自己还在线,以确保 TPC 连接的有效性.如果一个连接长时间没有心跳,需要及时断开,否则服务端会维护很多 ...
- [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...
- 【转】Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153761 记得在前面的文章中,我带大家一起从源码的角度分析了Android中Vi ...
- 基于netty实现的长连接,心跳机制及重连机制
技术:maven3.0.5 + netty4.1.33 + jdk1.8 概述 Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...
- 【Socket】关于socket长连接的心跳包
TCP的socket本身就是长连接的,那么为什么还要心跳包呢? 在smack里有个30s发送一个空消息的线程,同样关于心跳包(keepalive) 据网络搜索到的资料解释如下 内网机器如果不主动向外发 ...
- JAVA实现长连接(含心跳检测)Demo
实现原理: 长连接的维持,是要客户端程序,定时向服务端程序,发送一个维持连接包的. 如果,长时间未发送维持连接包,服务端程序将断开连接. 客户端: Client通过持有Sock ...
- 简易RPC框架-心跳与重连机制
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
随机推荐
- MySQL二进制日志分析-概述篇
MySQL从3.23版本开始引入了二进制日志,用于的数据复制, 二进制日志根据MySQL的版本不同,目前有4个版本: https://dev.mysql.com/doc/internals/en/bi ...
- Qt 模拟一个导航定位系统
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://www.cnblogs.com/lihuidashen/p/115397 ...
- 开发利器JRebel部署SpringBoot项目
不要以为年纪轻轻就跌倒了人生谷底,未来还有更大的下降空间等着你. idea下载和安装JRebel 激活JRebel 访问https://my.jrebel.com/ 使用facebook或twitte ...
- Escape (BFS + 模拟)
Problem Description The students of the HEU are maneuvering for their military training. The red arm ...
- 认识HTML和CSS
1.认识HTML标记 HTML的全称是Hyper text markup language,超文本标记语言,用于定义文档的内容结构.在HTML中,所有的标记都是成对出现的. <html>标 ...
- Promise核心原理解析
作者: HerryLo 本文永久有效链接: https://github.com/AttemptWeb...... Promises对象被用于表示一个异步操作的最终完成 (或失败), 及其结果值.主要 ...
- python爬虫入门新手向实战 - 爬取猫眼电影Top100排行榜
本次主要爬取Top100电影榜单的电影名.主演和上映时间, 同时保存为excel表个形式, 其他相似榜单也都可以依葫芦画瓢 首先打开要爬取的网址https://maoyan.com/board/4, ...
- 基于Linux系统--web环境搭建
上线部署文档 数据库部分1.下载Mysql服务 #yum install mysql-server 2.更改 /etc/my.cnf 3.启动Mysql ...
- 对象实例Vue
var vm = new Vue({ el:'#app', data:{}, //数据 methods:{}, //方法调用 filters:{}, //私有过滤器 directives:{}, // ...
- Python基础(十二)
今日主要内容 推导式 生成器表达式 lambda匿名函数 内置函数介绍 一.推导式 (一)列表推导式 先来看一段代码 建立一个空列表,向空列表中添加元素 lst = list() for i in r ...