Netty 之 Netty生产级的心跳和重连机制

sigh,写这篇博客的时候老脸还是红了一下,心里还是有些唏嘘的,应该算是剽窃吧,每个人的代码功力的确是有差距的,好在文章的标题是“一起学”,而不是开涛大神的“跟我学”系列的文章,我们还是多花点时间学习吧,感叹无用~


最近工作比较忙,但闲暇之余还是看了阿里的冯家春(fengjiachun)的github上的开源代码Jupiter,写的RPC框架让我感叹人外有人,废话不多说,下面的代码全部截取自Jupiter,写了一个比较完整的例子,供大家一起学习分享,再次对@Luca抱拳,Jupiter的Github地址:


https://github.com/fengjiachun/Jupiter


今天研究的是,心跳和重连,虽然这次是大神写的代码,但是万变不离其宗,我们先回顾一下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
  在CODE上查看代码片 派生到我的代码片
  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
  在CODE上查看代码片 派生到我的代码片
  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.         EventLoopGroup bossGroup = new NioEventLoopGroup(1);  
  32.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
  33.         try {  
  34.             ServerBootstrap sbs = new ServerBootstrap().group(bossGroup, workerGroup)  
  35.                     .channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO))  
  36.                     .localAddress(new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() {  
  37.                         protected void initChannel(SocketChannel ch) throws Exception {  
  38.                             ch.pipeline().addLast(new IdleStateHandler(500, TimeUnit.SECONDS));  
  39.                             ch.pipeline().addLast(idleStateTrigger);  
  40.                             ch.pipeline().addLast("decoder"new StringDecoder());  
  41.                             ch.pipeline().addLast("encoder"new StringEncoder());  
  42.                             ch.pipeline().addLast(new HeartBeatServerHandler());  
  43.                         };  
  44.   
  45.                     }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);  
  46.             // 绑定端口,开始接收进来的连接  
  47.             ChannelFuture future = sbs.bind(port).sync();  
  48.   
  49.             System.out.println("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.             port = Integer.parseInt(args[0]);  
  61.         } else {  
  62.             port = 8080;  
  63.         }  
  64.         new HeartBeatServer(port).start();  
  65.     }  
  66.   
  67. }  
单独写一个AcceptorIdleStateTrigger,其实也是继承ChannelInboundHandlerAdapter,重写userEventTriggered方法,因为客户端是write,那么服务端自然是read,设置的状态就是IdleState.READER_IDLE,源码如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  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
  在CODE上查看代码片 派生到我的代码片
  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.         System.out.println("server channelRead..");  
  12.         System.out.println(ctx.channel().remoteAddress() + "->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,顾名思义,链路检测狗,我们先看完整代码:

package netty.test.chapter14;

/**
 * Created by Administrator on 2016/9/22.
 */
/**
 *
 * 重连检测狗,当发现当前的链路不稳定关闭之后,进行12次重连
 */
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;

import java.util.concurrent.TimeUnit;
@Sharable
public abstract class ConnectionWatchdog extends ChannelInboundHandlerAdapter implements TimerTask ,ChannelHandlerHolder{



    private final Bootstrap bootstrap;
    private final Timer timer;
    private final int port;

    private final String host;

    private volatile boolean reconnect = true;
    private int attempts;


    public ConnectionWatchdog(Bootstrap bootstrap, Timer timer, int port,String host, boolean reconnect) {
        this.bootstrap = bootstrap;
        this.timer = timer;
        this.port = port;
        this.host = host;
        this.reconnect = reconnect;
    }

    /**
     * channel链路每次active的时候,将其连接的次数重新☞ 0
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("当前链路已经激活了,重连尝试次数重新置为0");
        attempts = 0;
        ctx.fireChannelActive();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("链接关闭");
        if(reconnect){
            System.out.println("链接关闭,将进行重连");
            if (attempts < 12) {
                attempts++;
            }           //重连的间隔时间会越来越长
            int timeout = 2 << attempts;
            timer.newTimeout(this, timeout, TimeUnit.MILLISECONDS);
        }
        ctx.fireChannelInactive();
    }

    public void run(Timeout timeout) throws Exception {
        ChannelFuture future;
        //bootstrap已经初始化好了,只需要将handler填入就可以了
        synchronized (bootstrap) {
            bootstrap.handler(new ChannelInitializer<Channel>(){
                @Override
                protected void initChannel(Channel ch) throws Exception {
                    ch.pipeline().addLast(handlers());
                }
            });
            future = bootstrap.connect(host,port);
        }
         //future对象
         future.addListener(new ChannelFutureListener() {
             public void operationComplete(ChannelFuture f) throws Exception {
                 boolean succeed = f.isSuccess();
         //如果重连失败,则调用ChannelInactive方法,再次出发重连事件,一直尝试12次,如果失败则不再重连
                 if (!succeed) {
                     System.out.println("重连失败");
                     f.channel().pipeline().fireChannelInactive();
                 }else{
                     System.out.println("重连成功");
                 }
             }
         });
    }

}


 
稍微分析一下: 

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

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

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

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

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

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

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

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  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.         EventLoopGroup group = new NioEventLoopGroup();    
  31.           
  32.         boot = new Bootstrap();  
  33.         boot.group(group).channel(NioSocketChannel.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(040, 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.                     boot.handler(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.                 port = Integer.valueOf(args[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
  在CODE上查看代码片 派生到我的代码片
  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
  在CODE上查看代码片 派生到我的代码片
  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.         System.out.println("激活时间是:"+new Date());  
  17.         System.out.println("HeartBeatClientHandler channelActive");  
  18.         ctx.fireChannelActive();  
  19.     }  
  20.   
  21.     @Override  
  22.     public void channelInactive(ChannelHandlerContext ctx) throws Exception {  
  23.         System.out.println("停止时间是:"+new Date());  
  24.         System.out.println("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.             ctx.write("has read message from server");  
  34.             ctx.flush();  
  35.         }  
  36.         ReferenceCountUtil.release(msg);  
  37.     }  
  38. }  


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

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

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

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

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

重启启动服务端:

重连成功~

  • 3
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值