今天研究的是,心跳和重连,虽然这次是大神写的代码,但是万变不离其宗,我们先回顾一下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秒才验证是不会断连的,所以我们在启动之后,关闭服务端,然后再次重启服务端
首先启动服务端,控制台如下:
启动客户端,控制台如下:
客户端启动之后,服务端的控制台:
关闭服务端后,客户端控制台:
重启启动服务端:
重连成功~