任何有关TCL、UDP的话题,都逃不过心跳包处理的命。
比如nginx或者自己写的nio框架都需要处理。
笔者就曾经自己写过基于nio的框架,心跳是这样处理的:服务端会启动一个特定的线程处理所有合法登陆的用户对象,并且指定时间内扫描客户端对象(向每一个客户端发送心跳包,客户端收到之后需要回复一个心跳),如果在指定时间内客户端没有返回任何数据,服务端会认为该客户端已经死掉了,然后踢掉它。
nginx的心跳处理呢?
下面我们来看看一下示例:
upstream name {
server 10.1.1.110:8080 max_fails=1 fail_timeout=10s;
server 10.1.1.122:8080 max_fails=1 fail_timeout=10s;
}
fail_timeout表示判断失去链接的时间,max_fails表示失去链接多少次后归为链接无效(即:timeout),简单来说上面配置的意思是,如果10秒钟内还是连不上则统计失效链接数+1,因为max_fails配置是1,所以失效一次就任务心跳失败。
现在重点来了,netty又是怎么处理的呢?
netty使用的是IdleStateHandler,配置参考如下:
//TODO 心跳包
//服务端如果长期没有收到客户端信息,就给客户端发送心跳包”ok”保持连接;如果服务器未收到客户端的反馈数据就主动断开客户端连接
//这个就表示 如果60秒未收到客户端信息 ,服务端就主动断掉客户端; 如果15秒没有信息,服务器就向客户端 发送心跳信息
//第一个参数 表示读操作空闲时间
//第二个参数 表示写操作空闲时间
//第三个参数 表示读写操作空闲时间
//第四个参数 单位
ch.pipeline().addLast("ping", new IdleStateHandler(60, 20, 15, TimeUnit.SECONDS));
注释写得很清楚,这里我就不再解释了,对应的Handler需要重写userEventTriggered方法,这里我用服务端配置作为示例:
NioSocketServerHandler server=new NioSocketServerHandler();
pipeline.addLast(server);
NioSocketServerHandler的userEventTriggered:
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
throws Exception {
//TODO 重写心跳包
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state().equals(IdleState.READER_IDLE)) {
log.info("READER_IDLE");
// 超时关闭channel
ctx.close();
} else if (event.state().equals(IdleState.WRITER_IDLE)) {
log.info("WRITER_IDLE");
} else if (event.state().equals(IdleState.ALL_IDLE)) {
log.info("ALL_IDLE");
// 发送心跳
Message msg=new Message("ok",ModuleID.HEART_BEAT,(byte)0xAF);
ctx.channel().writeAndFlush(msg);
}
}
super.userEventTriggered(ctx, evt);
}
15秒之内如果还没有收到客户端的任何信息,就发送一个心跳给客户端,然后客户端的处理是:
@Override
public void channelRead(ChannelHandlerContext ctx, Object ob)
throws Exception {
logger.info("client read msg:{}, ", JSON.toJSONString(ob));
//TODO to do something...
if(ob instanceof Message){
Message msg=(Message)ob;
// 收到心跳包
if(msg.getType()==(byte) 0xAF){
// 回复服务端一个心跳
Message rs=new Message("ok",ModuleID.HEART_BEAT,(byte)0xAF);
ctx.channel().writeAndFlush(rs);
}
}
}
对应的控制台信息 ->
服务端:
11:19:21.505 [nioEventLoopGroup-3-1] INFO c.k.s.n.s.NioSocketServerHandler - ALL_IDLE
11:19:21.507 [nioEventLoopGroup-3-1] INFO c.k.s.n.s.NioSocketServerHandler - server read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0}
11:19:36.509 [nioEventLoopGroup-3-1] INFO c.k.s.n.s.NioSocketServerHandler - ALL_IDLE
11:19:36.511 [nioEventLoopGroup-3-1] INFO c.k.s.n.s.NioSocketServerHandler - server read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0}
11:19:51.513 [nioEventLoopGroup-3-1] INFO c.k.s.n.s.NioSocketServerHandler - ALL_IDLE
11:19:51.515 [nioEventLoopGroup-3-1] INFO c.k.s.n.s.NioSocketServerHandler - server read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0}
客户端:
11:21:51.549 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
11:22:06.553 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
11:22:21.556 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
11:22:36.560 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
11:22:51.563 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
11:23:06.567 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
11:23:21.571 [nioEventLoopGroup-2-1] INFO test.nio.NioSocketClientHandler - client read msg:{"cmdId":2,"data":"ok","type":-81,"zip":0},
好了,说到这里基本上完了,网站很多帖子跟论坛上基本到这里就结束了,然而这里还有一个值得思考的地方:
这个配置主动发送的配置是放在服务端好呢(上面示例)?
还是放在客户端好呢?
放在服务端的好处是,失效的链接可以及时剔除,释放资源,当然对应的代价是需要浪费一点点资源(毕竟主动推送给所有客户端),如果客户端链接数量小倒是没关系,但是如果很大的话,就不能不慎重对待做调优了。
放在客户端的好处是,服务端就省去了这部分扫描浪费的资源,坏处是服务端无法自动感知失效的客户端对象,导致客户端资源一直占用。
关于笔者的建议,个人认为可以服务端跟客户端结合,客户端主动发送心跳,然后服务端启动一个独立的线程统计客户端发来的心跳,并且服务端设置判断失去链接的时间。