代码援引阿里大佬(fengjiachun),gitHub https://github.com/fengjiachun/Jupiter
本文主要介绍心跳机制的原理及重连机制(后者对我提醒较为重要),主要是层次上吧,可能是代码阅历与功底的原因。
上文gitHub中已经留存了我的简单心跳测试,主要是使用了 IdleStateHandler 这个Netty自带的心跳机制
/** * @see #IdleStateHandler(boolean, long, long, long, TimeUnit) */ public IdleStateHandler( long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) { this(false, readerIdleTime, writerIdleTime, allIdleTime, unit); }
源码片段中的参数:
(1).readerIdleTime:为读超时间(即测试端一定时间内未接收到被测试端消息);
(2).writerIdleTime:为写超时间(即测试端一定时间内向被测试端发送消息);
(3).allIdeTime:所有类型的超时时间
源码中的状态:常调用
public enum IdleState { READER_IDLE, // 读超时 WRITER_IDLE, // 写超时 ALL_IDLE // 数据交互超时 }
简单的介绍了这个,开始进入主题:
整个心跳测试与重连的思路大体相同,基本是如下6个步骤
1)客户端连接服务端
2)在客户端的的ChannelPipeline中加入一个比较特殊的IdleStateHandler,设置一下客户端的写空闲时间,例如5s
3)当客户端的所有ChannelHandler中4s内没有write事件,则会触发userEventTriggered方法(查看gitHub)
4)我们在客户端的userEventTriggered中对应的触发事件下发送一个心跳包给服务端,检测服务端是否还存活,防止服务端已经宕机,客户端还不知道
5)同样,服务端要对心跳包做出响应,其实给客户端最好的回复就是“不回复”,这样可以服务端的压力,假如有10w个空闲Idle的连接,那么服务端光发送心跳回复,则也是费事的事情,那么怎么才能告诉客户端它还活着呢,其实很简单,因为5s服务端都会收到来自客户端的心跳信息,那么如果10秒内收不到,服务端可以认为客户端挂了,可以close链路
6)加入服务端因为什么因素导致宕机的话,就会关闭所有的链路链接,所以作为客户端要做的事情就是短线重连
老规矩,先上代码,后进行解析
===========================客户端=================================
客户端处理类接口 ChannelHandlerHolder.java
/** * * 客户端的ChannelHandler集合,由子类实现,这样做的好处: * 继承这个接口的所有子类可以很方便地获取ChannelPipeline中的Handlers * 获取到handlers之后方便ChannelPipeline中的handler的初始化和在重连的时候也能很方便 * 地获取所有的handlers */ public interface ChannelHandlerHolder { ChannelHandler[] handlers(); }
自动重连处理类 ConnectionWatchdog.java
@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("重连成功"); } } }); } }
心跳检测状态处理类 ConnectorIdleStateTrigger.java
@Sharable public class ConnectorIdleStateTrigger extends ChannelInboundHandlerAdapter { private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat", CharsetUtil.UTF_8)); //不释放资源,读取后 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleState state = ((IdleStateEvent) evt).state(); if (state == IdleState.WRITER_IDLE) { ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()); } } else { super.userEventTriggered(ctx, evt); } } }
心跳检测业务类 HeartBeatClientHandler.java
public class HeartBeatClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("激活时间是:"+new Date()); System.out.println("HeartBeatClientHandler channelActive"); ctx.fireChannelActive(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("停止时间是:"+new Date()); System.out.println("HeartBeatClientHandler channelInactive"); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String message = (String) msg; System.out.println(message); if (message.equals("Heartbeat")) { ctx.write("has read message from server"); ctx.flush(); } ReferenceCountUtil.release(msg); } }
客户端启动类 HeartBeatsClient.java
public class HeartBeatsClient { protected final HashedWheelTimer timer = new HashedWheelTimer(); private Bootstrap boot; private final ConnectorIdleStateTrigger idleStateTrigger = new ConnectorIdleStateTrigger(); public void connect(int port, String host) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); boot = new Bootstrap(); boot.group(group).channel(NioSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)); final ConnectionWatchdog watchdog = new ConnectionWatchdog(boot, timer, port,host, true) { public ChannelHandler[] handlers() { return new ChannelHandler[] { this, new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS), idleStateTrigger, new StringDecoder(), new StringEncoder(), new HeartBeatClientHandler() }; } }; ChannelFuture future; //进行连接 try { synchronized (boot) { boot.handler(new ChannelInitializer<Channel>() { //初始化channel @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(watchdog.handlers()); } }); future = boot.connect(host,port); } // 以下代码在synchronized同步块外面是安全的 future.sync(); } catch (Throwable t) { throw new Exception("connects to fails", t); } } /** * @param args * @throws Exception */ public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { // 采用默认值 } } new HeartBeatsClient().connect(port, "127.0.0.1"); } }
=====================服务端============================
心跳检测状态处理类 AcceptorIdleStateTrigger.java
@ChannelHandler.Sharable public class AcceptorIdleStateTrigger extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleState state = ((IdleStateEvent) evt).state(); if (state == IdleState.READER_IDLE) { throw new Exception("idle exception"); } } else { super.userEventTriggered(ctx, evt); } } }
心跳检测业务处理类 HeartBeatServerHandler.java
public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("server channelRead.."); System.out.println(ctx.channel().remoteAddress() + "->Server :" + msg.toString()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
服务端启动类 HeartBeatServer.java
public class HeartBeatServer { private final AcceptorIdleStateTrigger idleStateTrigger = new AcceptorIdleStateTrigger(); private int port; public HeartBeatServer(int port) { this.port = port; } public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap sbs = new ServerBootstrap().group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)) .localAddress(new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS)); ch.pipeline().addLast(idleStateTrigger); ch.pipeline().addLast("decoder", new StringDecoder()); ch.pipeline().addLast("encoder", new StringEncoder()); ch.pipeline().addLast(new HeartBeatServerHandler()); }; }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true); // 绑定端口,开始接收进来的连接 ChannelFuture future = sbs.bind(port).sync(); System.out.println("Server start listen at " + port); future.channel().closeFuture().sync(); } catch (Exception e) { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port; if (args.length > 0) { port = Integer.parseInt(args[0]); } else { port = 8080; } new HeartBeatServer(port).start(); } }
启动测试---
1、先启动服务端、再启动客户端
2、关闭服务端,重启
3、会自动重连
=====================================================Demo例子结束,开始分析==============================================
1、@sharable 应该什么时候用
一般每一个链接创建handler实例,来避免多个线程竞争同一资源以至于未认证的客户端可以获得机密的信息,但有可能你不想创建许多实例,就可以添加。
如果一个ChannelHandler被注解为@Sharable,那意味着你可以只创建一个handler实例,并把它添加到一个或多个ChannelPipeline中多次,并不用考虑竞态的情况。
如果这个注解没有指定,你就只能为每次需要添加到pipeline中的handler,每次创建一个新的实例。因为它有非共享的状态,比如:成员变量。
2、ConnectorIdleStateTrigger.java 中初始化变量 HEARTBEAT_SEQUENCE 的意思
详情参考: https://www.jianshu.com/p/ae8010b06ac2
持久化一个以utf-8的固定字符串 以ByteBuf的形式,
内部会以这个字符串的大小为它创建固定的容器,以UTF-8 Charset 来编码
3、 什么时候使用 ReferenceCountUtil.release(msg);
ReferenceCountUtil.release()其实是ByteBuf.release()方法(从ReferenceCounted接口继承而来)的包装。netty4中的ByteBuf使用了引用计数(netty4实现了一个可选的ByteBuf池),每一个新分配的ByteBuf的引用计数值为1,每对这个ByteBuf对象增加一个引用,需要调用ByteBuf.retain()方法,而每减少一个引用,需要调用ByteBuf.release()方法。当这个ByteBuf对象的引用计数值为0时,表示此对象可回收。我这只是用ByteBuf说明,还有其他对象实现了ReferenceCounted接口,此时同理。
简单而言:从InBound里读取的ByteBuf要手动释放,还有自己创建的ByteBuf要自己负责释放。这两处要调用这个release方法。
4、什么时候使用 ctx.fireChannelActive();
1. 当socket建立连接时,Netty触发一个inbound事件channelActive,然后提交一个read()请求给本身(参考DefaultChannelPipeline.fireChannelActive())
2. 接收到read()请求后,Netty从socket读取消息。
3. 当读取到消息时,Netty触发channelRead()。
4. 当读取不到消息后,Netty触发ChannelReadCompleted().
5. Netty提交另外一个read()请求来继续从socket中读取消息。