什么场景下需要心跳呢?目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。
由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态,所以需要发送一段很小的报文告诉对方“我还活着”。
同时还有另外几个目的:
本文配套的CIM源码地址:
阅读本文需要一定的网络编程以及Netty方面的知识。
有关网络编程基础知识,请阅读以下资料:
有关Netty框架方面的知识,请阅读以下资料:
学习交流:
心跳其实有两种实现方式:
2)应用层自己实现。
所以我们这里所讨论的都是应用层的实现:
常规的实现应当是:
1)开启一个定时任务,定期发送心跳包;
4)超过后则认为服务端出现故障,需要重连。
这样确实也能实现心跳,但并不友好。
在正常的客户端和服务端通信的情况下,定时任务依然会发送心跳包;这样就显得没有意义,有些多余。所以理想的情况应当是客户端收到的写消息空闲时才发送这个心跳包去确认服务端是否健在。
来看看cim中的实现:
在pipeline中加入了一个10秒没有收到写消息的IdleStateHandler,到时他会回调ChannelInboundHandler中的userEventTriggered方法。
所以一旦写超时就立马向服务端发送一个心跳(做的更完善应当在心跳发送失败后有一定的重试次数)。
这样也就只有在空闲时候才会发送心跳包。但一旦间隔许久没有收到服务端响应进行重连的逻辑应当写在哪里呢?
先来看这个示例:
超过则重连。
也就是heartBeatHandler.process(ctx);的执行逻辑。
伪代码如下:
longheartBeatTime=appConfiguration.getHeartBeatTime()*1000;
LonglastReadTime=NettyAttrUtil.getReaderTime(ctx.channel());longnow=System.currentTimeMillis();if(lastReadTime!=null&&now-lastReadTime>heartBeatTime){reconnect();}}
我们假设下面的场景:
2)这时服务端突入出现down机,那么理想情况下应当是客户端迟迟没有收到服务端的响应从而userEventTriggered执行定时任务;
但却事与愿违,并不会执行2、3两步。
因为一旦服务端down机、或者是与客户端的网络断开则会回调客户端的channelInactive事件。
\
这里的destroy()方法会把之前开启的定时任务都给取消掉。所以就不会再有任何的定时任务执行了,也就不会有机会执行这个重连业务。
因此我们得有一个单独的线程来判断是否需要重连,不依赖于IdleStateHandler。
于是cim在客户端感知到网络断开时就会开启一个定时任务:
之所以不在客户端启动就开启,是为了节省一点线程消耗。网络问题虽然不可避免,但在需要的时候开启更能节省资源。
在这个任务重其实就是执行了重连,限于篇幅具体代码就不贴了,感兴趣的可以自行查阅。
同时来验证一下效果:
启动两个服务端,再启动客户端连接上一台并保持长连接。这时突然手动关闭一台服务,客户端可以自动重连到可用的那台服务节点。
启动客户端后服务端也能收到正常的ping消息:
利用:info命令查看当前客户端的链接状态发现连的是9000端口。
:info是一个新增命令,可以查看一些客户端信息。
这时我关掉连接上的这台节点:
这时客户端会自动重连到可用的那台节点。这个节点也收到了上线日志以及心跳包。
现在来看看服务端,它要实现的效果就是延迟N秒没有收到客户端的ping包则认为客户端下线了,在cim的场景下就需要把他踢掉置于离线状态。
有关消息发送误区:
这里依然有一个误区,在调用ctx.writeAndFlush()发送消息获取回调时。
其中是isSuccess并不能作为消息发送成功与否的标准:
也就是说即便是客户端直接断网,服务端这里发送消息后拿到的success依旧是true。这是因为这里的success只是告知我们消息写入了TCP缓冲区成功了而已。
和我之前有着一样错误理解的不在少数,这是Netty官方给的回复:
于是来做个试验:正常通信的客户端和服务端,当我把客户端直接断网时,服务端会自动剔除客户端。
这样就实现了文初的两个要求:
1)服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线;
2)客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。
同时也踩了两个误区,坑一个人踩就可以了,希望看过本文的都有所收获避免踩坑。