今天研究的是,心跳和重连,虽然这次是大神写的代码,但是万变不离其宗,我们先回顾一下Netty应用心跳和重连的整个过程:

1)客户端连接服务端

2)在客户端的的ChannelPipeline中加入一个比较特殊的IdleStateHandler,设置一下客户端的写空闲时间,例如5s

3)当客户端的所有ChannelHandler中4s内没有write事件,则会触发userEventTriggered方法(上文介绍过)

4)我们在客户端的userEventTriggered中对应的触发事件下发送一个心跳包给服务端,检测服务端是否还存活,防止服务端已经宕机,客户端还不知道

5)同样,服务端要对心跳包做出响应,其实给客户端最好的回复就是“不回复”,这样可以服务端的压力,假如有10w个空闲Idle的连接,那么服务端光发送心跳回复,则也是费事的事情,那么怎么才能告诉客户端它还活着呢,其实很简单,因为5s服务端都会收到来自客户端的心跳信息,那么如果10秒内收不到,服务端可以认为客户端挂了,可以close链路

6)加入服务端因为什么因素导致宕机的话,就会关闭所有的链路链接,所以作为客户端要做的事情就是短线重连

 

以上描述的就是整个心跳和重连的整个过程,虽然很简单,上一篇blog也写了一个Demo,简单地做了一下上述功能

 

要写工业级的Netty心跳重连的代码,需要解决一下几个问题:

1)ChannelPipeline中的ChannelHandlers的维护,首次连接和重连都需要对ChannelHandlers进行管理

2)重连对象的管理,也就是bootstrap对象的管理

3)重连机制编写

 

完整的代码:https://github.com/BazingaLyn/netty-study/tree/master/src/main/java/com/lyncc/netty/idle

 

下面我们就看大神是如何解决这些问题的,首先先定义一个接口ChannelHandlerHolder,用来保管ChannelPipeline中的Handlers的

 

[java] view plain copy

 

1. package com.lyncc.netty.idle;  
2.   
3. import io.netty.channel.ChannelHandler;  
4.   
5. /** 
6.  *  
7.  * 客户端的ChannelHandler集合,由子类实现,这样做的好处: 
8.  * 继承这个接口的所有子类可以很方便地获取ChannelPipeline中的Handlers 
9.  * 获取到handlers之后方便ChannelPipeline中的handler的初始化和在重连的时候也能很方便 
10.  * 地获取所有的handlers 
11.  */  
12. public interface ChannelHandlerHolder {  
13.   
14.     ChannelHandler[] handlers();  
15. }

我们再来编写我们熟悉的服务端的ServerBootstrap的编写:

 

HeartBeatServer.java

 

[java] view plain copy

 

1. package com.lyncc.netty.idle;  
2.   
3. import io.netty.bootstrap.ServerBootstrap;  
4. import io.netty.channel.ChannelFuture;  
5. import io.netty.channel.ChannelInitializer;  
6. import io.netty.channel.ChannelOption;  
7. import io.netty.channel.EventLoopGroup;  
8. import io.netty.channel.nio.NioEventLoopGroup;  
9. import io.netty.channel.socket.SocketChannel;  
10. import io.netty.channel.socket.nio.NioServerSocketChannel;  
11. import io.netty.handler.codec.string.StringDecoder;  
12. import io.netty.handler.codec.string.StringEncoder;  
13. import io.netty.handler.logging.LogLevel;  
14. import io.netty.handler.logging.LoggingHandler;  
15. import io.netty.handler.timeout.IdleStateHandler;  
16.   
17. import java.net.InetSocketAddress;  
18. import java.util.concurrent.TimeUnit;  
19.   
20. public class HeartBeatServer {  
21.       
22. private final AcceptorIdleStateTrigger idleStateTrigger = new AcceptorIdleStateTrigger();  
23.       
24. private int port;  
25.   
26. public HeartBeatServer(int port) {  
27. this.port = port;  
28.     }  
29.   
30. public void start() {  
31. new NioEventLoopGroup(1);  
32. new NioEventLoopGroup();  
33. try {  
34. new ServerBootstrap().group(bossGroup, workerGroup)  
35. class).handler(new LoggingHandler(LogLevel.INFO))  
36. new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() {  
37. protected void initChannel(SocketChannel ch) throws Exception {  
38. new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));  
39.                             ch.pipeline().addLast(idleStateTrigger);  
40. "decoder", new StringDecoder());  
41. "encoder", new StringEncoder());  
42. new HeartBeatServerHandler());  
43.                         };  
44.   
45. 128).childOption(ChannelOption.SO_KEEPALIVE, true);  
46. // 绑定端口,开始接收进来的连接  
47.             ChannelFuture future = sbs.bind(port).sync();  
48.   
49. "Server start listen at " + port);  
50.             future.channel().closeFuture().sync();  
51. catch (Exception e) {  
52.             bossGroup.shutdownGracefully();  
53.             workerGroup.shutdownGracefully();  
54.         }  
55.     }  
56.   
57. public static void main(String[] args) throws Exception {  
58. int port;  
59. if (args.length > 0) {  
60. 0]);  
61. else {  
62. 8080;  
63.         }  
64. new HeartBeatServer(port).start();  
65.     }  
66.   
67. }

单独写一个AcceptorIdleStateTrigger,其实也是继承ChannelInboundHandlerAdapter,重写userEventTriggered方法,因为客户端是write,那么服务端自然是read,设置的状态就是IdleState.READER_IDLE,源码如下:

 

 

[java] view plain copy

 

1. package com.lyncc.netty.idle;  
2.   
3. import io.netty.channel.ChannelHandler;  
4. import io.netty.channel.ChannelHandlerContext;  
5. import io.netty.channel.ChannelInboundHandlerAdapter;  
6. import io.netty.handler.timeout.IdleState;  
7. import io.netty.handler.timeout.IdleStateEvent;  
8.   
9.   
10. @ChannelHandler.Sharable  
11. public class AcceptorIdleStateTrigger extends ChannelInboundHandlerAdapter {  
12.   
13. @Override  
14. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {  
15. if (evt instanceof IdleStateEvent) {  
16.             IdleState state = ((IdleStateEvent) evt).state();  
17. if (state == IdleState.READER_IDLE) {  
18. throw new Exception("idle exception");  
19.             }  
20. else {  
21. super.userEventTriggered(ctx, evt);  
22.         }  
23.     }  
24. }

HeartBeatServerHandler就是一个很简单的自定义的Handler,不是重点:

 

 

[java] view plain copy

 

1. package com.lyncc.netty.idle;  
2.   
3. import io.netty.channel.ChannelHandlerContext;  
4. import io.netty.channel.ChannelInboundHandlerAdapter;  
5.   
6. public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {  
7.   
8.   
9. @Override  
10. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
11. "server channelRead..");  
12. "->Server :" + msg.toString());  
13.     }  
14.   
15. @Override  
16. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
17.         cause.printStackTrace();  
18.         ctx.close();  
19.     }  
20.   
21. }

接下来就是重点,我们需要写一个类,这个类可以去观察链路是否断了,如果断了,进行循环的断线重连操作,ConnectionWatchdog,顾名思义,链路检测狗,我们先看完整代码:

 

[java] view plain copy

 

    1. package com.lyncc.netty.idle;  
    2.   
    3. import io.netty.bootstrap.Bootstrap;  
    4. import io.netty.channel.Channel;  
    5. import io.netty.channel.ChannelFuture;  
    6. import io.netty.channel.ChannelFutureListener;  
    7. import io.netty.channel.ChannelHandler.Sharable;  
    8. import io.netty.channel.ChannelHandlerContext;  
    9. import io.netty.channel.ChannelInboundHandlerAdapter;  
    10. import io.netty.channel.ChannelInitializer;  
    11. import io.netty.util.Timeout;  
    12. import io.netty.util.Timer;  
    13. import io.netty.util.TimerTask;  
    14.   
    15. import java.util.concurrent.TimeUnit;  
    16.   
    17. /** 
    18.  *  
    19.  * 重连检测狗,当发现当前的链路不稳定关闭之后,进行12次重连 
    20.  */  
    21. @Sharable  
    22. public abstract class ConnectionWatchdog extends ChannelInboundHandlerAdapter implements TimerTask ,ChannelHandlerHolder{  
    23.       
    24.       
    25.       
    26. private final Bootstrap bootstrap;  
    27. private final Timer timer;  
    28. private final int port;  
    29.       
    30. private final String host;  
    31.   
    32. private volatile boolean reconnect = true;  
    33. private int attempts;  
    34.       
    35.       
    36. public ConnectionWatchdog(Bootstrap bootstrap, Timer timer, int port,String host, boolean reconnect) {  
    37. this.bootstrap = bootstrap;  
    38. this.timer = timer;  
    39. this.port = port;  
    40. this.host = host;  
    41. this.reconnect = reconnect;  
    42.     }  
    43.       
    44. /** 
    45.      * channel链路每次active的时候,将其连接的次数重新☞ 0 
    46.      */  
    47. @Override  
    48. public void channelActive(ChannelHandlerContext ctx) throws Exception {  
    49.           
    50. "当前链路已经激活了,重连尝试次数重新置为0");  
    51.           
    52. 0;  
    53.         ctx.fireChannelActive();  
    54.     }  
    55.       
    56. @Override  
    57. public void channelInactive(ChannelHandlerContext ctx) throws Exception {  
    58. "链接关闭");  
    59. if(reconnect){  
    60. "链接关闭,将进行重连");  
    61. if (attempts < 12) {  
    62.                 attempts++;  
    63. //重连的间隔时间会越来越长  
    64. int timeout = 2 << attempts;  
    65. this, timeout, TimeUnit.MILLISECONDS);  
    66.             }  
    67.         }  
    68.         ctx.fireChannelInactive();  
    69.     }  
    70.       
    71.   
    72. public void run(Timeout timeout) throws Exception {  
    73.           
    74.         ChannelFuture future;  
    75. //bootstrap已经初始化好了,只需要将handler填入就可以了  
    76. synchronized (bootstrap) {  
    77. new ChannelInitializer<Channel>() {  
    78.   
    79. @Override  
    80. protected void initChannel(Channel ch) throws Exception {  
    81.                       
    82.                     ch.pipeline().addLast(handlers());  
    83.                 }  
    84.             });  
    85.             future = bootstrap.connect(host,port);  
    86.         }  
    87. //future对象  
    88. new ChannelFutureListener() {  
    89.   
    90. public void operationComplete(ChannelFuture f) throws Exception {  
    91. boolean succeed = f.isSuccess();  
    92.   
    93. //如果重连失败,则调用ChannelInactive方法,再次出发重连事件,一直尝试12次,如果失败则不再重连  
    94. if (!succeed) {  
    95. "重连失败");  
    96.                     f.channel().pipeline().fireChannelInactive();  
    97. else{  
    98. "重连成功");  
    99.                 }  
    100.             }  
    101.         });  
    102.           
    103.     }  
    104.   
    105. }

    稍微分析一下:

     

    1)继承了ChannelInboundHandlerAdapter,说明它也是Handler,也对,作为一个检测对象,肯定会放在链路中,否则怎么检测

    2)实现了2个接口,TimeTask,ChannelHandlerHolder

       ①TimeTask,我们就要写run方法,这应该是一个定时任务,这个定时任务做的事情应该是重连的工作

       ②ChannelHandlerHolder的接口,这个接口我们刚才说过是维护的所有的Handlers,因为在重连的时候需要获取Handlers

    3)bootstrap对象,重连的时候依旧需要这个对象

    4)当链路断开的时候会触发channelInactive这个方法,也就说触发重连的导火索是从这边开始的

     

    好了,我们这边再写次核心的HeartBeatsClient的代码:

     

    [java] view plain copy

     

    1. package com.lyncc.netty.idle;  
    2.   
    3. import io.netty.bootstrap.Bootstrap;  
    4. import io.netty.channel.Channel;  
    5. import io.netty.channel.ChannelFuture;  
    6. import io.netty.channel.ChannelHandler;  
    7. import io.netty.channel.ChannelInitializer;  
    8. import io.netty.channel.EventLoopGroup;  
    9. import io.netty.channel.nio.NioEventLoopGroup;  
    10. import io.netty.channel.socket.nio.NioSocketChannel;  
    11. import io.netty.handler.codec.string.StringDecoder;  
    12. import io.netty.handler.codec.string.StringEncoder;  
    13. import io.netty.handler.logging.LogLevel;  
    14. import io.netty.handler.logging.LoggingHandler;  
    15. import io.netty.handler.timeout.IdleStateHandler;  
    16. import io.netty.util.HashedWheelTimer;  
    17.   
    18. import java.util.concurrent.TimeUnit;  
    19.   
    20. public class HeartBeatsClient {  
    21.       
    22. protected final HashedWheelTimer timer = new HashedWheelTimer();  
    23.       
    24. private Bootstrap boot;  
    25.       
    26. private final ConnectorIdleStateTrigger idleStateTrigger = new ConnectorIdleStateTrigger();  
    27.   
    28. public void connect(int port, String host) throws Exception {  
    29.           
    30. new NioEventLoopGroup();    
    31.           
    32. new Bootstrap();  
    33. class).handler(new LoggingHandler(LogLevel.INFO));  
    34.               
    35. final ConnectionWatchdog watchdog = new ConnectionWatchdog(boot, timer, port,host, true) {  
    36.   
    37. public ChannelHandler[] handlers() {  
    38. return new ChannelHandler[] {  
    39. this,  
    40. new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS),  
    41.                             idleStateTrigger,  
    42. new StringDecoder(),  
    43. new StringEncoder(),  
    44. new HeartBeatClientHandler()  
    45.                     };  
    46.                 }  
    47.             };  
    48.               
    49.             ChannelFuture future;  
    50. //进行连接  
    51. try {  
    52. synchronized (boot) {  
    53. new ChannelInitializer<Channel>() {  
    54.   
    55. //初始化channel  
    56. @Override  
    57. protected void initChannel(Channel ch) throws Exception {  
    58.                             ch.pipeline().addLast(watchdog.handlers());  
    59.                         }  
    60.                     });  
    61.   
    62.                     future = boot.connect(host,port);  
    63.                 }  
    64.   
    65. // 以下代码在synchronized同步块外面是安全的  
    66.                 future.sync();  
    67. catch (Throwable t) {  
    68. throw new Exception("connects to  fails", t);  
    69.             }  
    70.     }  
    71.   
    72. /** 
    73.      * @param args 
    74.      * @throws Exception 
    75.      */  
    76. public static void main(String[] args) throws Exception {  
    77. int port = 8080;  
    78. if (args != null && args.length > 0) {  
    79. try {  
    80. 0]);  
    81. catch (NumberFormatException e) {  
    82. // 采用默认值  
    83.             }  
    84.         }  
    85. new HeartBeatsClient().connect(port, "127.0.0.1");  
    86.     }  
    87.   
    88. }

    也稍微说明一下:

     

    1)创建了ConnectionWatchdog对象,自然要实现handlers方法

    2)初始化好bootstrap对象

    3)4秒内没有写操作,进行心跳触发,也就是IdleStateHandler这个方法

     

    最后ConnectorIdleStateTrigger这个类

     

    [java] view plain copy

    1. package com.lyncc.netty.idle;  
    2.   
    3. import io.netty.buffer.ByteBuf;  
    4. import io.netty.buffer.Unpooled;  
    5. import io.netty.channel.ChannelHandler.Sharable;  
    6. import io.netty.channel.ChannelHandlerContext;  
    7. import io.netty.channel.ChannelInboundHandlerAdapter;  
    8. import io.netty.handler.timeout.IdleState;  
    9. import io.netty.handler.timeout.IdleStateEvent;  
    10. import io.netty.util.CharsetUtil;  
    11.   
    12. @Sharable  
    13. public class ConnectorIdleStateTrigger extends ChannelInboundHandlerAdapter {  
    14.       
    15. private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat",  
    16.             CharsetUtil.UTF_8));  
    17.   
    18. @Override  
    19. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {  
    20. if (evt instanceof IdleStateEvent) {  
    21.             IdleState state = ((IdleStateEvent) evt).state();  
    22. if (state == IdleState.WRITER_IDLE) {  
    23. // write heartbeat to server  
    24.                 ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate());  
    25.             }  
    26. else {  
    27. super.userEventTriggered(ctx, evt);  
    28.         }  
    29.     }  
    30. }

    HeartBeatClientHandler.java(不是重点)

     

     

    [java] view plain copy

     

      1. package com.lyncc.netty.idle;  
      2.   
      3. import io.netty.channel.ChannelHandler.Sharable;  
      4. import io.netty.channel.ChannelHandlerContext;  
      5. import io.netty.channel.ChannelInboundHandlerAdapter;  
      6. import io.netty.util.ReferenceCountUtil;  
      7.   
      8. import java.util.Date;  
      9.   
      10. @Sharable  
      11. public class HeartBeatClientHandler extends ChannelInboundHandlerAdapter {  
      12.   
      13.       
      14. @Override  
      15. public void channelActive(ChannelHandlerContext ctx) throws Exception {  
      16. "激活时间是:"+new Date());  
      17. "HeartBeatClientHandler channelActive");  
      18.         ctx.fireChannelActive();  
      19.     }  
      20.   
      21. @Override  
      22. public void channelInactive(ChannelHandlerContext ctx) throws Exception {  
      23. "停止时间是:"+new Date());  
      24. "HeartBeatClientHandler channelInactive");  
      25.     }  
      26.   
      27.   
      28. @Override  
      29. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
      30.         String message = (String) msg;  
      31.         System.out.println(message);  
      32. if (message.equals("Heartbeat")) {  
      33. "has read message from server");  
      34.             ctx.flush();  
      35.         }  
      36.         ReferenceCountUtil.release(msg);  
      37.     }  
      38. }

      好了,到此为止,所有的代码都贴完了,我们做一个简单的测试,按照常理,如果不出任何状况的话,客户端4秒发送心跳,服务端5秒才验证是不会断连的,所以我们在启动之后,关闭服务端,然后再次重启服务端

      首先启动服务端,控制台如下:

      心跳接口是nginx netty心跳客户端重连_.net

      启动客户端,控制台如下:

      心跳接口是nginx netty心跳客户端重连_客户端_02

      客户端启动之后,服务端的控制台:

      心跳接口是nginx netty心跳客户端重连_.net_03

      关闭服务端后,客户端控制台:

      心跳接口是nginx netty心跳客户端重连_.net_04

      重启启动服务端:

      心跳接口是nginx netty心跳客户端重连_心跳接口是nginx_05

      重连成功~