当前位置:   article > 正文

Netty实现心跳机制与断线重连_javascript websocket一直重连netty

javascript websocket一直重连netty

心跳机制

何为心跳

所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性.

注:心跳包还有另一个作用,经常被忽略,即:一个连接如果长时间不用,防火墙或者路由器就会断开该连接

如何实现

核心Handler —— IdleStateHandler

Netty 中, 实现心跳机制的关键是 IdleStateHandler, 那么这个 Handler 如何使用呢? 先看下它的构造器:

 

  1. public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
  2. this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
  3. }

这里解释下三个参数的含义:

  • readerIdleTimeSeconds: 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLEIdleStateEvent 事件.
  • writerIdleTimeSeconds: 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLEIdleStateEvent 事件.
  • allIdleTimeSeconds: 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLEIdleStateEvent 事件.

注:这三个参数默认的时间单位是。若需要指定其他时间单位,可以使用另一个构造方法:IdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)

下面直接上代码,需要注意的地方,会在代码中通过注释进行说明。

使用IdleStateHandler实现心跳

下面将使用IdleStateHandler来实现心跳,Client端连接到Server端后,会循环执行一个任务:随机等待几秒,然后ping一下Server端,即发送一个心跳包。当等待的时间超过规定时间,将会发送失败,以为Server端在此之前已经主动断开连接了。代码如下:

Client端

ClientIdleStateTrigger —— 心跳触发器

ClientIdleStateTrigger也是一个Handler,只是重写了userEventTriggered方法,用于捕获IdleState.WRITER_IDLE事件(未在指定时间内向服务器发送数据),然后向Server端发送一个心跳包。

 

  1. /**
  2. * <p>
  3. * 用于捕获{@link IdleState#WRITER_IDLE}事件(未在指定时间内向服务器发送数据),然后向<code>Server</code>端发送一个心跳包。
  4. * </p>
  5. */
  6. public class ClientIdleStateTrigger extends ChannelInboundHandlerAdapter {
  7. public static final String HEART_BEAT = "heart beat!";
  8. @Override
  9. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  10. if (evt instanceof IdleStateEvent) {
  11. IdleState state = ((IdleStateEvent) evt).state();
  12. if (state == IdleState.WRITER_IDLE) {
  13. // write heartbeat to server
  14. ctx.writeAndFlush(HEART_BEAT);
  15. }
  16. } else {
  17. super.userEventTriggered(ctx, evt);
  18. }
  19. }
  20. }

Pinger —— 心跳发射器

 

  1. /**
  2. * <p>客户端连接到服务器端后,会循环执行一个任务:随机等待几秒,然后ping一下Server端,即发送一个心跳包。</p>
  3. */
  4. public class Pinger extends ChannelInboundHandlerAdapter {
  5. private Random random = new Random();
  6. private int baseRandom = 8;
  7. private Channel channel;
  8. @Override
  9. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  10. super.channelActive(ctx);
  11. this.channel = ctx.channel();
  12. ping(ctx.channel());
  13. }
  14. private void ping(Channel channel) {
  15. int second = Math.max(1, random.nextInt(baseRandom));
  16. System.out.println("next heart beat will send after " + second + "s.");
  17. ScheduledFuture<?> future = channel.eventLoop().schedule(new Runnable() {
  18. @Override
  19. public void run() {
  20. if (channel.isActive()) {
  21. System.out.println("sending heart beat to the server...");
  22. channel.writeAndFlush(ClientIdleStateTrigger.HEART_BEAT);
  23. } else {
  24. System.err.println("The connection had broken, cancel the task that will send a heart beat.");
  25. channel.closeFuture();
  26. throw new RuntimeException();
  27. }
  28. }
  29. }, second, TimeUnit.SECONDS);
  30. future.addListener(new GenericFutureListener() {
  31. @Override
  32. public void operationComplete(Future future) throws Exception {
  33. if (future.isSuccess()) {
  34. ping(channel);
  35. }
  36. }
  37. });
  38. }
  39. @Override
  40. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  41. // 当Channel已经断开的情况下, 仍然发送数据, 会抛异常, 该方法会被调用.
  42. cause.printStackTrace();
  43. ctx.close();
  44. }
  45. }

ClientHandlersInitializer —— 客户端处理器集合的初始化类

 

  1. public class ClientHandlersInitializer extends ChannelInitializer<SocketChannel> {
  2. private ReconnectHandler reconnectHandler;
  3. private EchoHandler echoHandler;
  4. public ClientHandlersInitializer(TcpClient tcpClient) {
  5. Assert.notNull(tcpClient, "TcpClient can not be null.");
  6. this.reconnectHandler = new ReconnectHandler(tcpClient);
  7. this.echoHandler = new EchoHandler();
  8. }
  9. @Override
  10. protected void initChannel(SocketChannel ch) throws Exception {
  11. ChannelPipeline pipeline = ch.pipeline();
  12. pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
  13. pipeline.addLast(new LengthFieldPrepender(4));
  14. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  15. pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
  16. pipeline.addLast(new Pinger());
  17. }
  18. }

注: 上面的Handler集合,除了Pinger,其他都是编解码器和解决粘包,可以忽略。

TcpClient —— TCP连接的客户端

 

  1. public class TcpClient {
  2. private String host;
  3. private int port;
  4. private Bootstrap bootstrap;
  5. /** 将<code>Channel</code>保存起来, 可用于在其他非handler的地方发送数据 */
  6. private Channel channel;
  7. public TcpClient(String host, int port) {
  8. this(host, port, new ExponentialBackOffRetry(1000, Integer.MAX_VALUE, 60 * 1000));
  9. }
  10. public TcpClient(String host, int port, RetryPolicy retryPolicy) {
  11. this.host = host;
  12. this.port = port;
  13. init();
  14. }
  15. /**
  16. * 向远程TCP服务器请求连接
  17. */
  18. public void connect() {
  19. synchronized (bootstrap) {
  20. ChannelFuture future = bootstrap.connect(host, port);
  21. this.channel = future.channel();
  22. }
  23. }
  24. private void init() {
  25. EventLoopGroup group = new NioEventLoopGroup();
  26. // bootstrap 可重用, 只需在TcpClient实例化的时候初始化即可.
  27. bootstrap = new Bootstrap();
  28. bootstrap.group(group)
  29. .channel(NioSocketChannel.class)
  30. .handler(new ClientHandlersInitializer(TcpClient.this));
  31. }
  32. public static void main(String[] args) {
  33. TcpClient tcpClient = new TcpClient("localhost", 2222);
  34. tcpClient.connect();
  35. }
  36. }

Server端

ServerIdleStateTrigger —— 断连触发器

 

  1. /**
  2. * <p>在规定时间内未收到客户端的任何数据包, 将主动断开该连接</p>
  3. */
  4. public class ServerIdleStateTrigger extends ChannelInboundHandlerAdapter {
  5. @Override
  6. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  7. if (evt instanceof IdleStateEvent) {
  8. IdleState state = ((IdleStateEvent) evt).state();
  9. if (state == IdleState.READER_IDLE) {
  10. // 在规定时间内没有收到客户端的上行数据, 主动断开连接
  11. ctx.disconnect();
  12. }
  13. } else {
  14. super.userEventTriggered(ctx, evt);
  15. }
  16. }
  17. }

ServerBizHandler —— 服务器端的业务处理器

 

  1. /**
  2. * <p>收到来自客户端的数据包后, 直接在控制台打印出来.</p>
  3. */
  4. @ChannelHandler.Sharable
  5. public class ServerBizHandler extends SimpleChannelInboundHandler<String> {
  6. private final String REC_HEART_BEAT = "I had received the heart beat!";
  7. @Override
  8. protected void channelRead0(ChannelHandlerContext ctx, String data) throws Exception {
  9. try {
  10. System.out.println("receive data: " + data);
  11. // ctx.writeAndFlush(REC_HEART_BEAT);
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. @Override
  17. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  18. System.out.println("Established connection with the remote client.");
  19. // do something
  20. ctx.fireChannelActive();
  21. }
  22. @Override
  23. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  24. System.out.println("Disconnected with the remote client.");
  25. // do something
  26. ctx.fireChannelInactive();
  27. }
  28. @Override
  29. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  30. cause.printStackTrace();
  31. ctx.close();
  32. }
  33. }

ServerHandlerInitializer —— 服务器端处理器集合的初始化类

 

  1. /**
  2. * <p>用于初始化服务器端涉及到的所有<code>Handler</code></p>
  3. */
  4. public class ServerHandlerInitializer extends ChannelInitializer<SocketChannel> {
  5. protected void initChannel(SocketChannel ch) throws Exception {
  6. ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0));
  7. ch.pipeline().addLast("idleStateTrigger", new ServerIdleStateTrigger());
  8. ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
  9. ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(4));
  10. ch.pipeline().addLast("decoder", new StringDecoder());
  11. ch.pipeline().addLast("encoder", new StringEncoder());
  12. ch.pipeline().addLast("bizHandler", new ServerBizHandler());
  13. }
  14. }

注:new IdleStateHandler(5, 0, 0)handler代表如果在5秒内没有收到来自客户端的任何数据包(包括但不限于心跳包),将会主动断开与该客户端的连接。

TcpServer —— 服务器端

 

  1. public class TcpServer {
  2. private int port;
  3. private ServerHandlerInitializer serverHandlerInitializer;
  4. public TcpServer(int port) {
  5. this.port = port;
  6. this.serverHandlerInitializer = new ServerHandlerInitializer();
  7. }
  8. public void start() {
  9. EventLoopGroup bossGroup = new NioEventLoopGroup(1);
  10. EventLoopGroup workerGroup = new NioEventLoopGroup();
  11. try {
  12. ServerBootstrap bootstrap = new ServerBootstrap();
  13. bootstrap.group(bossGroup, workerGroup)
  14. .channel(NioServerSocketChannel.class)
  15. .childHandler(this.serverHandlerInitializer);
  16. // 绑定端口,开始接收进来的连接
  17. ChannelFuture future = bootstrap.bind(port).sync();
  18. System.out.println("Server start listen at " + port);
  19. future.channel().closeFuture().sync();
  20. } catch (Exception e) {
  21. bossGroup.shutdownGracefully();
  22. workerGroup.shutdownGracefully();
  23. e.printStackTrace();
  24. }
  25. }
  26. public static void main(String[] args) throws Exception {
  27. int port = 2222;
  28. new TcpServer(port).start();
  29. }
  30. }

至此,所有代码已经编写完毕。

测试

首先启动客户端,再启动服务器端。启动完成后,在客户端的控制台上,可以看到打印如下类似日志:

 

在服务器端可以看到控制台输出了类似如下的日志:


可以看到,客户端在发送4个心跳包后,第5个包因为等待时间较长,等到真正发送的时候,发现连接已断开了;

而服务器端收到客户端的4个心跳数据包后,迟迟等不到下一个数据包,所以果断断开该连接。

 

异常情况

在测试过程中,有可能会出现如下情况:

 

 


出现这种情况的原因是:在连接已断开的情况下,仍然向服务器端发送心跳包。虽然在发送心跳包之前会使用channel.isActive()判断连接是否可用,但也有可能上一刻判断结果为可用,但下一刻发送数据包之前,连接就断了。

目前尚未找到优雅处理这种情况的方案,各位看官如果有好的解决方案,还望不吝赐教。拜谢!!!

断线重连

断线重连这里就不过多介绍,相信各位都知道是怎么回事。这里只说大致思路,然后直接上代码。

实现思路

客户端在监测到与服务器端的连接断开后,或者一开始就无法连接的情况下,使用指定的重连策略进行重连操作,直到重新建立连接或重试次数耗尽。

对于如何监测连接是否断开,则是通过重写ChannelInboundHandler#channelInactive来实现,但连接不可用,该方法会被触发,所以只需要在该方法做好重连工作即可。

代码实现

注:以下代码都是在上面心跳机制的基础上修改/添加的。

因为断线重连是客户端的工作,所以只需对客户端代码进行修改。

重试策略

RetryPolicy —— 重试策略接口

 

  1. public interface RetryPolicy {
  2. /**
  3. * Called when an operation has failed for some reason. This method should return
  4. * true to make another attempt.
  5. *
  6. * @param retryCount the number of times retried so far (0 the first time)
  7. * @return true/false
  8. */
  9. boolean allowRetry(int retryCount);
  10. /**
  11. * get sleep time in ms of current retry count.
  12. *
  13. * @param retryCount current retry count
  14. * @return the time to sleep
  15. */
  16. long getSleepTimeMs(int retryCount);
  17. }

ExponentialBackOffRetry —— 重连策略的默认实现

 

  1. /**
  2. * <p>Retry policy that retries a set number of times with increasing sleep time between retries</p>
  3. */
  4. public class ExponentialBackOffRetry implements RetryPolicy {
  5. private static final int MAX_RETRIES_LIMIT = 29;
  6. private static final int DEFAULT_MAX_SLEEP_MS = Integer.MAX_VALUE;
  7. private final Random random = new Random();
  8. private final long baseSleepTimeMs;
  9. private final int maxRetries;
  10. private final int maxSleepMs;
  11. public ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries) {
  12. this(baseSleepTimeMs, maxRetries, DEFAULT_MAX_SLEEP_MS);
  13. }
  14. public ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) {
  15. this.maxRetries = maxRetries;
  16. this.baseSleepTimeMs = baseSleepTimeMs;
  17. this.maxSleepMs = maxSleepMs;
  18. }
  19. @Override
  20. public boolean allowRetry(int retryCount) {
  21. if (retryCount < maxRetries) {
  22. return true;
  23. }
  24. return false;
  25. }
  26. @Override
  27. public long getSleepTimeMs(int retryCount) {
  28. if (retryCount < 0) {
  29. throw new IllegalArgumentException("retries count must greater than 0.");
  30. }
  31. if (retryCount > MAX_RETRIES_LIMIT) {
  32. System.out.println(String.format("maxRetries too large (%d). Pinning to %d", maxRetries, MAX_RETRIES_LIMIT));
  33. retryCount = MAX_RETRIES_LIMIT;
  34. }
  35. long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << retryCount));
  36. if (sleepMs > maxSleepMs) {
  37. System.out.println(String.format("Sleep extension too large (%d). Pinning to %d", sleepMs, maxSleepMs));
  38. sleepMs = maxSleepMs;
  39. }
  40. return sleepMs;
  41. }
  42. }

ReconnectHandler—— 重连处理器

 

  1. @ChannelHandler.Sharable
  2. public class ReconnectHandler extends ChannelInboundHandlerAdapter {
  3. private int retries = 0;
  4. private RetryPolicy retryPolicy;
  5. private TcpClient tcpClient;
  6. public ReconnectHandler(TcpClient tcpClient) {
  7. this.tcpClient = tcpClient;
  8. }
  9. @Override
  10. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  11. System.out.println("Successfully established a connection to the server.");
  12. retries = 0;
  13. ctx.fireChannelActive();
  14. }
  15. @Override
  16. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  17. if (retries == 0) {
  18. System.err.println("Lost the TCP connection with the server.");
  19. ctx.close();
  20. }
  21. boolean allowRetry = getRetryPolicy().allowRetry(retries);
  22. if (allowRetry) {
  23. long sleepTimeMs = getRetryPolicy().getSleepTimeMs(retries);
  24. System.out.println(String.format("Try to reconnect to the server after %dms. Retry count: %d.", sleepTimeMs, ++retries));
  25. final EventLoop eventLoop = ctx.channel().eventLoop();
  26. eventLoop.schedule(() -> {
  27. System.out.println("Reconnecting ...");
  28. tcpClient.connect();
  29. }, sleepTimeMs, TimeUnit.MILLISECONDS);
  30. }
  31. ctx.fireChannelInactive();
  32. }
  33. private RetryPolicy getRetryPolicy() {
  34. if (this.retryPolicy == null) {
  35. this.retryPolicy = tcpClient.getRetryPolicy();
  36. }
  37. return this.retryPolicy;
  38. }
  39. }

ClientHandlersInitializer

在之前的基础上,添加了重连处理器ReconnectHandler

 

  1. public class ClientHandlersInitializer extends ChannelInitializer<SocketChannel> {
  2. private ReconnectHandler reconnectHandler;
  3. private EchoHandler echoHandler;
  4. public ClientHandlersInitializer(TcpClient tcpClient) {
  5. Assert.notNull(tcpClient, "TcpClient can not be null.");
  6. this.reconnectHandler = new ReconnectHandler(tcpClient);
  7. this.echoHandler = new EchoHandler();
  8. }
  9. @Override
  10. protected void initChannel(SocketChannel ch) throws Exception {
  11. ChannelPipeline pipeline = ch.pipeline();
  12. pipeline.addLast(this.reconnectHandler);
  13. pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
  14. pipeline.addLast(new LengthFieldPrepender(4));
  15. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  16. pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
  17. pipeline.addLast(new Pinger());
  18. }
  19. }

TcpClient

在之前的基础上添加重连、重连策略的支持。

 

  1. public class TcpClient {
  2. private String host;
  3. private int port;
  4. private Bootstrap bootstrap;
  5. /** 重连策略 */
  6. private RetryPolicy retryPolicy;
  7. /** 将<code>Channel</code>保存起来, 可用于在其他非handler的地方发送数据 */
  8. private Channel channel;
  9. public TcpClient(String host, int port) {
  10. this(host, port, new ExponentialBackOffRetry(1000, Integer.MAX_VALUE, 60 * 1000));
  11. }
  12. public TcpClient(String host, int port, RetryPolicy retryPolicy) {
  13. this.host = host;
  14. this.port = port;
  15. this.retryPolicy = retryPolicy;
  16. init();
  17. }
  18. /**
  19. * 向远程TCP服务器请求连接
  20. */
  21. public void connect() {
  22. synchronized (bootstrap) {
  23. ChannelFuture future = bootstrap.connect(host, port);
  24. future.addListener(getConnectionListener());
  25. this.channel = future.channel();
  26. }
  27. }
  28. public RetryPolicy getRetryPolicy() {
  29. return retryPolicy;
  30. }
  31. private void init() {
  32. EventLoopGroup group = new NioEventLoopGroup();
  33. // bootstrap 可重用, 只需在TcpClient实例化的时候初始化即可.
  34. bootstrap = new Bootstrap();
  35. bootstrap.group(group)
  36. .channel(NioSocketChannel.class)
  37. .handler(new ClientHandlersInitializer(TcpClient.this));
  38. }
  39. private ChannelFutureListener getConnectionListener() {
  40. return new ChannelFutureListener() {
  41. @Override
  42. public void operationComplete(ChannelFuture future) throws Exception {
  43. if (!future.isSuccess()) {
  44. future.channel().pipeline().fireChannelInactive();
  45. }
  46. }
  47. };
  48. }
  49. public static void main(String[] args) {
  50. TcpClient tcpClient = new TcpClient("localhost", 2222);
  51. tcpClient.connect();
  52. }
  53. }

测试

在测试之前,为了避开 Connection reset by peer 异常,可以稍微修改Pingerping()方法,添加if (second == 5)的条件判断。如下:

 

  1. private void ping(Channel channel) {
  2. int second = Math.max(1, random.nextInt(baseRandom));
  3. if (second == 5) {
  4. second = 6;
  5. }
  6. System.out.println("next heart beat will send after " + second + "s.");
  7. ScheduledFuture<?> future = channel.eventLoop().schedule(new Runnable() {
  8. @Override
  9. public void run() {
  10. if (channel.isActive()) {
  11. System.out.println("sending heart beat to the server...");
  12. channel.writeAndFlush(ClientIdleStateTrigger.HEART_BEAT);
  13. } else {
  14. System.err.println("The connection had broken, cancel the task that will send a heart beat.");
  15. channel.closeFuture();
  16. throw new RuntimeException();
  17. }
  18. }
  19. }, second, TimeUnit.SECONDS);
  20. future.addListener(new GenericFutureListener() {
  21. @Override
  22. public void operationComplete(Future future) throws Exception {
  23. if (future.isSuccess()) {
  24. ping(channel);
  25. }
  26. }
  27. });
  28. }

启动客户端

先只启动客户端,观察控制台输出,可以看到类似如下日志:

 

可以看到,当客户端发现无法连接到服务器端,所以一直尝试重连。随着重试次数增加,重试时间间隔越大,但又不想无限增大下去,所以需要定一个阈值,比如60s。如上图所示,当下一次重试时间超过60s时,会打印Sleep extension too large(*). Pinning to 60000,单位为ms。出现这句话的意思是,计算出来的时间超过阈值(60s),所以把真正睡眠的时间重置为阈值(60s)。

启动服务器端

接着启动服务器端,然后继续观察客户端控制台输出。

 


可以看到,在第9次重试失败后,第10次重试之前,启动的服务器,所以第10次重连的结果为Successfully established a connection to the server.,即成功连接到服务器。接下来因为还是不定时ping服务器,所以出现断线重连、断线重连的循环。

 

扩展

在不同环境,可能会有不同的重连需求。有不同的重连需求的,只需自己实现RetryPolicy接口,然后在创建TcpClient的时候覆盖默认的重连策略即可。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/717217
推荐阅读
相关标签
  

闽ICP备14008679号