心跳实现方式
心跳其实有两种实现方式:
由于
TCP
协议过于底层,对于开发者来说维护性、灵活度都比较差同时还依赖于操作系统。
所以我们这里所讨论的都是应用层的实现。
如上图所示,在应用层通常是由客户端发送一个心跳包
ping
到服务端,服务端收到后响应一个
pong
表明双方都活得好好的。
一旦其中一端延迟 N 个时间窗口没有收到消息则进行不同的处理。
客户端自动重连
先拿客户端来说吧,每隔一段时间客户端向服务端发送一个心跳包,同时收到服务端的响应。
常规的实现应当是:
-
再有一个定时任务定期检测这个
“本地时间”
是否超过阈值。
这样确实也能实现心跳,但并不友好。
在正常的客户端和服务端通信的情况下,定时任务依然会发送心跳包;这样就显得没有意义,有些多余。
所以理想的情况应当是客户端收到的写消息空闲时才发送这个心跳包去确认服务端是否健在。
好消息是
Netty
已经为我们考虑到了这点,自带了一个开箱即用的
IdleStateHandler
专门用于心跳处理。
来看看
cim
中的实现:
在
pipeline
中加入了一个 10秒没有收到写消息的
IdleStateHandler
,到时他会回调
ChannelInboundHandler
中的
userEventTriggered
方法。
所以一旦写超时就立马向服务端发送一个心跳(做的更完善应当在心跳发送失败后有一定的重试次数);
这样也就只有在空闲时候才会发送心跳包。
但一旦间隔许久没有收到服务端响应进行重连的逻辑应当写在哪里呢?
先来看这个示例:
当收到服务端响应的 pong 消息时,就在当前 Channel 上记录一个时间,也就是说后续可以在定时任务中取出这个时间和当前时间的差额来判断是否超过阈值。
超过则重连。
同时在每次心跳时候都用当前时间和之前服务端响应绑定到
Channel
上的时间相减判断是否需要重连即可。
也就是
heartBeatHandler.process(ctx);
的执行逻辑。
伪代码如下:
@Override
public void process(ChannelHandlerContext ctx) throws Exception {
long heartBeatTime = appConfiguration.getHeartBeatTime() * 1000;
Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel());
long now = System.currentTimeMillis();
if (lastReadTime != null && now - lastReadTime > heartBeatTime){
reconnect();
}
IdleStateHandler 误区
一切看起来也没毛病,但实际上却没有这样实现重连逻辑。
最主要的问题还是对
IdleStateHandler
理解有误。
我们假设下面的场景:
-
客户端通过登录连上了服务端并保持长连接,一切正常的情况下双方各发心跳包保持连接。
-
这时服务端突入出现 down 机,那么理想情况下应当是客户端迟迟没有收到服务端的响应从而
userEventTriggered
执行定时任务。
-
判断
当前时间 - UpdateWriteTime > 阈值
时进行重连。
但却事与愿违,并不会执行 2、3两步。
因为一旦服务端
down
机、或者是与客户端的网络断开则会回调客户端的
channelInactive
事件。
IdleStateHandler
作为一个
ChannelInbound
也重写了
channelInactive()
方法。
这里的
destroy()
方法会把之前开启的定时任务都给取消掉。
所以就不会再有任何的定时任务执行了,也就不会有机会执行这个重连业务
。
什么场景下需要心跳呢? 目前我们接触到的大多是一些基于长连接的应用需要心跳来“保活”。 由于在长连接的场景下,客户端和服务端并不是一直处于通信状态,如果双方长期没有沟通则双方都不清楚对方目前的状态;所以需要发送一段很小的报文告诉对方“我还活着”。 同时还有另外几个目的: 服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线。 客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。
在C/S模式中,有时我们会长时间保持一个连接,以避免频繁地建立连接,但同时,一般会有一个超时时间,在这个时间内没发起任何请求的连接会被断开,以减少负载,节约资源。并且该机制一般都是在服务端实现,因为client强制关闭或意外断开连接,server端在此刻是感知不到的,如果放到client端实现,在上述情况下,该超时机制就失效了。本来这问题很普通,不太值得一提,但最近在项目中看到了该机制的一种糟糕的实现,故在此深入分析一下。
一般情况下,前端页面连接WebSocket服务的时候都是通过Nginx等负载均衡,然后由Nginx去代理连接后端的socket服务。如果建立连接之后不做一些措施,那么可能会有各种各样的原因会导致socket断开。
近日,在公司中开发一个使用websocket为前端推送消息的功能时,发现一个问题:就是每隔一段时间如果不传送数据的话,与前段的连接就会自动断开;
刚开始以为是session的原因,因为web session 的默认时间是30分钟;但是通过日志发现断开时间间隔时间远远不到30分钟;认真分析发现不操作间隔恰好为90秒
它就会在自动断开;随恍然大悟;原来是我们的使用nginx 代理,nginx配置了访问超时时间为90s;
WebSocket是html5中用来实现长连接的一个协议。