Netty 系列六 实现心跳机制 超时重连

 Netty实现心跳机制有一个专门的类 IdleStateHandler

public class IdleStateHandler extends ChannelDuplexHandler {

// 构造方法
   public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int         allIdleTimeSeconds) {
        this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);


    }
参数说明:
readerIdleTimeSeconds 读超时,规定时间内没有读到数据会触发读超时事件 
writerIdleTimeSeconds 写超时,规定时间内没有写数据就会触发写超时事件
allIdleTimeSeconds 读写超时,规定时间内没有读写操作就会触发读写超时事件

IdleStateHandler 触发的事件对象 IdleStateEvent

 
public class IdleStateEvent {
    public static final IdleStateEvent FIRST_READER_IDLE_STATE_EVENT;
    public static final IdleStateEvent READER_IDLE_STATE_EVENT;
    public static final IdleStateEvent FIRST_WRITER_IDLE_STATE_EVENT;
    public static final IdleStateEvent WRITER_IDLE_STATE_EVENT;
    public static final IdleStateEvent FIRST_ALL_IDLE_STATE_EVENT;
    public static final IdleStateEvent ALL_IDLE_STATE_EVENT;
    private final IdleState state;
    private final boolean first;

    protected IdleStateEvent(IdleState state, boolean first) {
        this.state = (IdleState)ObjectUtil.checkNotNull(state, "state");
        this.first = first;
    }

每个事件对应一个枚举

public enum IdleState {
    READER_IDLE,
    WRITER_IDLE,
    ALL_IDLE;

    private IdleState() {
    }
}

基于以上的机制 开始代码实现  首先实现心跳

客户端发送心跳的handler

package com.bowei.netty.heartbeat;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.ScheduledFuture;

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class SendHeartHandler extends ChannelInboundHandlerAdapter {
    private Random random= new Random();
    private int baseRandom = 8;
    private Channel channel;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        this.channel=ctx.channel();
        send(channel);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    public void send(Channel channel){
        int seaconds= Math.max(1,random.nextInt(baseRandom));
        System.out.println("下次心跳将在"+seaconds+"秒后发送");
        //定时向服务端发送心跳
        ScheduledFuture<?> future = channel.eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                if (channel.isActive()) {
                    System.out.println("通道活跃可以发送心跳");
                    channel.writeAndFlush("hello");
                } else {
                    System.out.println("通道断开不能发送心跳");
                    channel.closeFuture();
                    throw new RuntimeException();
                }
            }
        }, seaconds, TimeUnit.SECONDS);

        //心态发送成功后继续发送
        future.addListener(new GenericFutureListener() {
            @Override
            public void operationComplete(Future future) throws Exception {
                if(future.isSuccess()){
                    send(channel);
                }
            }
        });
    }
}

客户端的ChannelInitializer

package com.bowei.netty.heartbeat;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import org.springframework.util.Assert;

public class ClientHandlersInitializer extends ChannelInitializer<SocketChannel> {
    private ReconnectHandler reconnectHandler;

    public ClientHandlersInitializer(TcpClient tcpClient){
        Assert.notNull(tcpClient,"tcpClient can not be null");
        // ReconnectHandler 只在初始化客户端的时候new一个新的,每个客户端都对应一个自己的 每个客户端的多个channel共享一个ReconnectHandler
        this.reconnectHandler=new ReconnectHandler(tcpClient);
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(this.reconnectHandler);
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
        pipeline.addLast(new LengthFieldPrepender(4));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new SendHeartHandler());
    }

}
客户端的启动类
package com.bowei.netty.heartbeat;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TcpClient {
   private String host;
    private int port;
    private Bootstrap bootstrap;
    private Channel channel;
    private RetryPolicy retryPolicy;
    public TcpClient(String host, int port) {
        //这个地方不能new new创造了一个新的TcpClient 其他的方法在用到这个类的属性的时候是没有初始化的
       // new TcpClient(host,port,new ExponentialBackOffRetry(2,20,10));
       this(host,port,new ExponentialBackOffRetry(1000, Integer.MAX_VALUE, 60 * 1000) );
    }
    public TcpClient(String host, int port,RetryPolicy retryPolicy){
        this.host=host;
        this.port=port;
        this.retryPolicy=retryPolicy;
        init();
    }

    private void init() {
        EventLoopGroup loopGroup = new NioEventLoopGroup();
        bootstrap = new Bootstrap();
        bootstrap.group(loopGroup)
                .channel(NioSocketChannel.class)
                .handler(new ClientHandlersInitializer(TcpClient.this));
    }
    private ChannelFutureListener getConnectionListener() {
        return new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    future.channel().pipeline().fireChannelInactive();
                }
            }
        };
    }

    public void connect() {
        synchronized (bootstrap) {
            // bootstrap可以公用
            ChannelFuture channelFuture = bootstrap.connect(host, port);
            channelFuture.addListener(getConnectionListener());
            this.channel = channelFuture.channel();
        }
    }

    public RetryPolicy getRetryPolicy() {
        return retryPolicy;
    }

    public static void main(String[] args) {
        TcpClient tcpClient = new TcpClient("localhost", 6667);
        tcpClient.connect();

    }

}

服务器端处理心跳的handler

package com.bowei.netty.heartbeat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

public class ServerIdleStateTrigger 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){
                // 在规定时间内没有受到客户端的心跳 主动断开链接
                ctx.disconnect();
            }else {
                super.userEventTriggered(ctx, evt);
            }
        }
    }
}

 

服务器端的ChannelInitializer

package com.bowei.netty.heartbeat;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;

public class ServerHandlerInitializer extends ChannelInitializer {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        // 设置5s读超时
        ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0));
        ch.pipeline().addLast("serverIdleStateTrigger", new ServerIdleStateTrigger());
        ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
        ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(4));
        ch.pipeline().addLast("decoder", new StringDecoder());
        ch.pipeline().addLast("encoder", new StringEncoder());
        ch.pipeline().addLast("bizHandler", new ServerHandler());
    }
}

服务端的启动类

package com.bowei.netty.heartbeat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class TcpServer {
    private int port;
    private ServerHandlerInitializer serverHandlerInitializer;

    public TcpServer(int port) {
        this.port = port;
        this.serverHandlerInitializer = new ServerHandlerInitializer();
    }

    public void start() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(this.serverHandlerInitializer);
            // 绑定端口,开始接收进来的连接
            ChannelFuture future = bootstrap.bind(port).sync();

            System.out.println("服务端启动 开始监听端口 " + port);
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 6667;
        new TcpServer(port).start();
    }
}

先启动服务端然后在启动客户端 打印日志如下

 

 

 

下面我们实现超时后的重新链接。超时后服务端主动断开客户端(或者客户端链接不上服务端的时候),会触发客户端的的

channelInactive 所以我们需要在这个地方实现的我们的重连

重连的接口,该接口定义了重连的各种策略,跟随业务变化,一般的重连都应该有 重连的最大次数,不应该是每次都是立即重连

可能这个时候服务器端繁忙,应等待一段时间然后在重连,一般随着重连次数的增加,等待的时间应该越来越长

package com.bowei.netty.heartbeat;

public interface RetryPolicy {
    //是否可以重连
    boolean allowRetry(int retryCount);
    //获取重连需要等待的时间
    long getSleepTimeMs(int retryCount);
}
package com.bowei.netty.heartbeat;

import java.util.Random;

public class ExponentialBackOffRetry implements RetryPolicy {
    /**
     * 最大可以重连的次数
     */
    private static final int MAX_RETRY_LIMIT = 30;
    /**
     * 默认重连最长的等待时间
     */
    private static final int DEFAULT_MAX_SLEEP_MS = Integer.MAX_VALUE;
     private final Random random = new Random();

    private final long baseSleepTimeMs;
    private final int maxRetries;
    private final int maxSleepMs;


    public ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries) {
        this(baseSleepTimeMs, maxRetries, DEFAULT_MAX_SLEEP_MS);
    }

    public ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) {
        this.maxRetries = maxRetries;
        this.baseSleepTimeMs = baseSleepTimeMs;
        this.maxSleepMs = maxSleepMs;
    }


    @Override
    public boolean allowRetry(int retryCount) {
        if (retryCount < maxRetries) {
            return true;
        }
        return false;
    }

    @Override
    public long getSleepTimeMs(int retryCount) {
        if (retryCount < 0) {
            throw new IllegalArgumentException("重试次数必须大于0");
        }
        if(retryCount>MAX_RETRY_LIMIT){
            System.out.println("重试次数已达上限");
            retryCount = MAX_RETRY_LIMIT;
        }
        long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << retryCount));
        if(sleepMs>maxSleepMs){
            System.out.println("睡眠时间太长");
            sleepMs=maxSleepMs;
        }

        return sleepMs;
    }
}

 

 

 

 

 

 

重连的handler

package com.bowei.netty.heartbeat;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.EventLoop;

import java.util.concurrent.TimeUnit;

/*
 *  @Sharable 注解用来说明ChannelHandler可以在多个channel直接共享使用
 * 每次失败重连都是一个新的管道Channel 多个管道要共享这个ReconnectHandler
 */
@ChannelHandler.Sharable
public class ReconnectHandler extends ChannelInboundHandlerAdapter {
    private int retries=0;
    private RetryPolicy retryPolicy;
    private TcpClient tcpClient;

    public ReconnectHandler(TcpClient tcpClient){
        this.tcpClient=tcpClient;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端链接成功");
        retries=0;
        ctx.fireChannelActive();//作用:触发事件告知Inbound ChannelHandler:ChannelHandlerContext的Channel现在处于活动状态,调用ChannelInboundHandler的channelActive
    }

    private  RetryPolicy getRetryPolicy(){
        if(this.retryPolicy==null){
            this.retryPolicy = tcpClient.getRetryPolicy();
        }
        return this.retryPolicy;
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if(retries==0){
            System.out.println("未链接到服务端");
            ctx.close();
        }
        boolean allowRetry = getRetryPolicy().allowRetry(retries);
        if(allowRetry) {
            long sleepTimeMs = getRetryPolicy().getSleepTimeMs(retries);
            System.out.println("尝试去重试 重试次数" + ++retries + " 等待时间" + sleepTimeMs);
            final EventLoop eventLoop = ctx.channel().eventLoop();
            eventLoop.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Reconnecting ...");
                    tcpClient.connect();
                }
            },sleepTimeMs,TimeUnit.MILLISECONDS);

        }
    //现在处于不活动状态,调用ChannelInboundHandler的channelInactive
        ctx.fireChannelInactive();
    }

}

 

 

 

先启动我们的客户端 服务端这时还咩有启动 客户端会不断的重试连接 然后在启动服务端

 

Netty实现双向心跳机制,通常涉及到两个方向的交互:服务器向客户端发送心跳以及客户端响应服务器。以下是基本的步骤: 1. **服务器向客户端发送心跳**: - 在`HeartbeatHandler`中,除了接收心跳外,还应该有一个发送心跳的功能,比如每间隔一段时间(比如60秒): ```java class HeartbeatHandler extends ChannelInboundHandlerAdapter { private final int heartbeatInterval; // 定义心跳间隔 @Scheduled(cron = "*/60 * * * *") // 每分钟发送一次心跳 void sendHeartbeat(Channel channel) { ByteBuf content = Unpooled.copiedBuffer("Heartbeat", StandardCharsets.UTF_8); channel.writeAndFlush(content).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } } ``` 2. **客户端响应心跳**: - 客户端需要监听心跳包,一旦收到就回复确认,同时维持一个心跳计时器,防止超时后关闭连接: ```java class ClientHandler extends SimpleChannelInboundHandler<Object> { private boolean alive = true; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof String && msg.equals("Heartbeat")) { ByteBuf response = Unpooled.copiedBuffer("Received heartbeat", StandardCharsets.UTF_8); ctx.writeAndFlush(response); // 回应心跳 ctx.channel().eventLoop().schedule(() -> sendHeartbeat(), heartbeatInterval, TimeUnit.SECONDS); } } private void sendHeartbeat() { if (!alive) return; ByteBuf content = Unpooled.copiedBuffer("Heartbeat ACK", StandardCharsets.UTF_8); ctx.writeAndFlush(content); } } ``` 3. **异常处理和超时管理**: - 在上述代码中,已经设置了关闭连接的监听器。当某个方向无法正常通信时,可以设置超时机制,比如超时后主动关闭连接。 这样,你就实现了双向的心跳机制,双方都会定期交换心跳以验证连接是否正常。注意实际应用中可能还需要考虑错误处理和异常恢复。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值