我们知道在TCP长连接或者WebSocket长连接中一般我们都会使用心跳机制–即发送特殊的数据包来通告对方自己的业务还没有办完,不要关闭链接。
那么心跳机制可以用来做什么呢?
我们知道网络的传输是不可靠的,当我们发起一个链接请求的过程之中会发生什么事情谁都无法预料,或者断电,服务器重启,断网线之类。
如果有这种情况的发生对方也无法判断你是否还在线。所以这时候我们引入心跳机制,在长链接中双方没有数据交互的时候互相发送数据(可能是空包,也可能是特殊数据),对方收到该数据之后也回复相应的数据用以确保双方都在线,这样就可以确保当前链接是有效的。
1. 如何实现心跳机制
一般实现心跳机制由两种方式:
- TCP协议自带的心跳机制来实现;
- 在应用层来实现。
但是TCP协议自带的心跳机制系统默认是设置的是2小时的心跳频率。它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。另外该心跳机制是与TCP协议绑定的,那如果我们要是使用UDP协议岂不是用不了?所以一般我们都不用。
而一般我们自己实现呢大致的策略是这样的:
- Client启动一个定时器,不断发送心跳;
- Server收到心跳后,做出回应;
- Server启动一个定时器,判断Client是否存在,这里做判断有两种方法:时间差和简单标识。
时间差:
- 收到一个心跳包之后记录当前时间;
- 判断定时器到达时间,计算多久没收到心跳时间=当前时间-上次收到心跳时间。如果改时间大于设定值则认为超时。
简单标识:
- 收到心跳后设置连接标识为true;
- 判断定时器到达时间,如果未收到心跳则设置连接标识为false;
今天我们来看一下Netty的心跳机制的实现,在Netty中提供了IdleStateHandler类来进行心跳的处理,它可以对一个 Channel 的 读/写设置定时器, 当 Channel 在一定事件间隔内没有数据交互时(即处于 idle 状态), 就会触发指定的事件。
该类可以对三种类型的超时做心跳机制检测:
- public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
- this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
- }
- readerIdleTimeSeconds:设置读超时时间;
- writerIdleTimeSeconds:设置写超时时间;
- allIdleTimeSeconds:同时为读或写设置超时时间;
下面我们还是通过一个例子来讲解IdleStateHandler的使用。
服务端:
1. public class HeartBeatServer {
2. private int port;
3.
4. public HeartBeatServer(int port) {
5. this.port = port;
6. }
7.
8. public void start(){
9. EventLoopGroup bossGroup = new NioEventLoopGroup();
10. EventLoopGroup workGroup = new NioEventLoopGroup();
11.
12. ServerBootstrap server = new ServerBootstrap().group(bossGroup,workGroup)
13. .channel(NioServerSocketChannel.class)
14. .childHandler(new HeartBeatServerChannelInitializer());
15.
16. try {
17. ChannelFuture future = server.bind(port).sync();
18. future.channel().closeFuture().sync();
19. } catch (InterruptedException e) {
20. e.printStackTrace();
21. }finally {
22. bossGroup.shutdownGracefully();
23. workGroup.shutdownGracefully();
24. }
25. }
26.
27. public static void main(String[] args) {
28. HeartBeatServer server = new HeartBeatServer(7788);
29. server.start();
30. }
31. }
服务端Initializer:
1. public class HeartBeatServerChannelInitializer extends ChannelInitializer<SocketChannel> {
2. @Override
3. protected void initChannel(SocketChannel socketChannel) throws Exception {
4. ChannelPipeline pipeline = socketChannel.pipeline();
5.
6. pipeline.addLast("handler",new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
7. pipeline.addLast("decoder", new StringDecoder());
8. pipeline.addLast("encoder", new StringEncoder());
9. pipeline.addLast(new HeartBeatServerHandler());
10. }
11. }
在这里IdleStateHandler也是handler的一种,所以加入addLast。我们分别设置4个参数:读超时时间为3s,写超时和读写超时为0,然后加入时间控制单元。另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 Java 系列面试题和答案,非常齐全。
服务端handler:
1. public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter{
2. private int loss_connect_time = 0;
3.
4. @Override
5. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
6. System.out.println(ctx.channel().remoteAddress() + "Server :" + msg.toString());
7. }
8.
9. @Override
10. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
11. if(evt instanceof IdleStateEvent){
12. 服务端对应着读事件,当为READER_IDLE时触发
13. IdleStateEvent event = (IdleStateEvent)evt;
14. if(event.state() == IdleState.READER_IDLE){
15. loss_connect_time++;
16. 接收消息超时");
17. if(loss_connect_time > 2){
18. 关闭不活动的链接");
19. ctx.channel().close();
20. }
21. }else{
22. super.userEventTriggered(ctx,evt);
23. }
24. }
25. }
26.
27. @Override
28. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
29. ctx.close();
30. }
31. }
我们看到在handler中调用了userEventTriggered方法,IdleStateEvent的state()方法一个有三个值:READER_IDLE,WRITER_IDLE,ALL_IDLE。正好对应读事件写事件和读写事件。
再来写一下客户端:
1. public class HeartBeatsClient {
2. private int port;
3. private String address;
4.
5. public HeartBeatsClient(int port, String address) {
6. this.port = port;
7. this.address = address;
8. }
9.
10. public void start(){
11. EventLoopGroup group = new NioEventLoopGroup();
12.
13. Bootstrap bootstrap = new Bootstrap();
14. bootstrap.group(group)
15. .channel(NioSocketChannel.class)
16. .handler(new HeartBeatsClientChannelInitializer());
17.
18. try {
19. ChannelFuture future = bootstrap.connect(address,port).sync();
20. future.channel().closeFuture().sync();
21. } catch (Exception e) {
22. e.printStackTrace();
23. }finally {
24. group.shutdownGracefully();
25. }
26.
27. }
28.
29. public static void main(String[] args) {
30. HeartBeatsClient client = new HeartBeatsClient(7788,"127.0.0.1");
31. client.start();
32. }
33. }
客户端Initializer:
1. public class HeartBeatsClientChannelInitializer extends ChannelInitializer<SocketChannel> {
2.
3. protected void initChannel(SocketChannel socketChannel) throws Exception {
4. ChannelPipeline pipeline = socketChannel.pipeline();
5.
6. pipeline.addLast("handler", new IdleStateHandler(0, 3, 0, TimeUnit.SECONDS));
7. pipeline.addLast("decoder", new StringDecoder());
8. pipeline.addLast("encoder", new StringEncoder());
9. pipeline.addLast(new HeartBeatClientHandler());
10. }
11. }
这里我们设置了IdleStateHandler的写超时为3秒,客户端执行的动作为写消息到服务端,服务端执行读动作。Spring Boot 学习笔记分享给你看下。
客户端handler:
1. public class HeartBeatClientHandler extends ChannelInboundHandlerAdapter {
2.
3. private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat",
4. CharsetUtil.UTF_8));
5.
6. private static final int TRY_TIMES = 3;
7.
8. private int currentTime = 0;
9.
10. @Override
11. public void channelActive(ChannelHandlerContext ctx) throws Exception {
12. 激活时间是:"+new Date());
13. 链接已经激活");
14. ctx.fireChannelActive();
15. }
16.
17. @Override
18. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
19. 停止时间是:"+new Date());
20. 关闭链接");
21. }
22.
23. @Override
24. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
25. 当前轮询时间:"+new Date());
26. if (evt instanceof IdleStateEvent) {
27. IdleStateEvent event = (IdleStateEvent) evt;
28. if (event.state() == IdleState.WRITER_IDLE) {
29. if(currentTime <= TRY_TIMES){
30. System.out.println("currentTime:"+currentTime);
31. currentTime++;
32. ctx.channel().writeAndFlush(HEARTBEAT_SEQUENCE.duplicate());
33. }
34. }
35. }
36. }
37.
38. @Override
39. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
40. String message = (String) msg;
41. System.out.println(message);
42. if (message.equals("Heartbeat")) {
43. ctx.write("has read message from server");
44. ctx.flush();
45. }
46. ReferenceCountUtil.release(msg);
47. }
48.
49. @Override
50. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
51. ctx.close();
52. }
53. }
启动服务端和客户端我们看到输出为:
我们再来屡一下思路:
- 首先客户端激活channel,因为客户端中并没有发送消息所以会触发客户端的IdleStateHandler,它设置的写超时时间为3s;
- 然后触发客户端的事件机制进入userEventTriggered方法,在触发器中计数并向客户端发送消息;
- 服务端接收消息;
- 客户端触发器继续轮询发送消息,直到计数器满不再向服务端发送消息;
- 服务端在IdleStateHandler设置的读消息超时时间5s内未收到消息,触发了服务端中handler的userEventTriggered方法,于是关闭客户端的链接。
大体我们的简单心跳机制就是这样的思路,通过事件触发机制以及计数器的方式来实现,上面我们的案例中最后客户端没有发送消息的时候我们是强制断开了客户端的链接,那么既然可以关闭,我们是不是也可是重新链接客户端呢?因为万一客户端本身并不想关闭而是由于别的原因导致他无法与服务端通信。下面我们来说一下重连机制。
当我们的服务端在未读到客户端消息超时而关闭客户端的时候我们一般在客户端的finally块中方的是关闭客户端的代码,这时我们可以做一下修改的,finally是一定会被执行新的,所以我们可以在finally块中重新调用一下启动客户端的代码,这样就又重新启动了客户端了,上客户端代码:
1. /**
2. * 本Client为测试netty重连机制
3. * Server端代码都一样,所以不做修改
4. * 只用在client端中做一下判断即可
5. */
6. public class HeartBeatsClient2 {
7.
8. private int port;
9. private String address;
10. ChannelFuture future;
11.
12. public HeartBeatsClient2(int port, String address) {
13. this.port = port;
14. this.address = address;
15. }
16.
17. public void start(){
18. EventLoopGroup group = new NioEventLoopGroup();
19.
20. Bootstrap bootstrap = new Bootstrap();
21. bootstrap.group(group)
22. .channel(NioSocketChannel.class)
23. .handler(new HeartBeatsClientChannelInitializer());
24.
25. try {
26. future = bootstrap.connect(address,port).sync();
27. future.channel().closeFuture().sync();
28. } catch (Exception e) {
29. e.printStackTrace();
30. }finally {
31. //group.shutdownGracefully();
32. if (null != future) {
33. if (future.channel() != null && future.channel().isOpen()) {
34. future.channel().close();
35. }
36. }
37. 准备重连");
38. start();
39. 重连成功");
40. }
41.
42. }
43.
44. public static void main(String[] args) {
45. HeartBeatsClient2 client = new HeartBeatsClient2(7788,"127.0.0.1");
46. client.start();
47. }
48. }
其余部分的代码与上面的实例并无异同,只需改造客户端即可,我们再运行服务端和客户端会看到客户端虽然被关闭了,但是立马又被重启: