Netty断线重连实现

netty断线重连实现

学习文章

浅析 Netty 实现心跳机制与断线重连

心跳机制

心跳是TCP长连接中,c-s之间发送的一种特殊的数据包,用来通知对方还在线,以确保TCP连接的有效性。

原理是:当在一段时间Idle后,c或者s会发送一个特殊的数据包,也就是ping包给对方,当对方收到一个ping包时,会返回一个pong包以证明自己还在线。这样就确保了TCP连接的有效性。

在netty中使用心跳机制

有两种方式:

  1. 使用TCP协议自带的keepalive机制
  2. 自己在应用层上实现自定义的心跳机制

方式2相对于方式1来说,多费些流量,而且方式1默认关闭,TCP的keepalive机制依赖于操作系统的实现,默认两个小时,并且对于keepalive的修改需要系统调用,不够灵活。当连接换成UDP时,keepalive就失效了。所以大部分都使用方式2。

在netty中使用自定义的心跳机制,重要的两点:IdleStateHandler和userEventTriggered()。

我们在启动前配置一个IdleStateHandler,设置好读空闲时间,写空闲时间,读写空闲时间,netty会有一个定时器,在相应的时间出发相应的时间,我们在userEventTriggered()方法中,可以对触发的事件的类型进行判断,而做出相应的操作。

实现思路

  1. 注册IdleStateHandler
  2. 客户端负责发送PING消息,因此客户端关注ALL_IDLE事件,在这个事件触发后,客户端需要向服务端发送PING消息,告诉其“我还存活着”。
  3. 服务端负责接收PING消息,并返回PONG消息。因此服务端关注READER_IDLE事件,并且服务器的READER_IDLE的时间要比客户端的ALL_IDLE大(通常是两倍),这样就可以实现客户端每到读写空闲时就给服务器发送一个PING消息,服务端响应一个PONG消息,这样客户端与服务端的读写状态都会被刷新,从而进入下一次心跳。因为服务端的READER_IDLE时间是客户端ALL_IDLE的两倍,所以如果在某一次服务端读到READER_IDLE时,证明客户端挂掉了(因为如果客户端没挂的话,它早就应该发送一个PING过来,而这个PING不管怎么着在两倍的时间内也应该到了,所以如果两倍的时间内没有读的话证明客户端就挂了),这时候服务端关闭连接即可。

自定义通用HeartbeatHandler

public abstract class CommonHeartbeatHandler extends SimpleChannelInboundHandler {
    public static final String PING_MSG = "ping";
    public static final String PONG_MSG = "pong";
//    public static final byte CUSTOM_MSG = 3;
    protected String name;
    private int heartbeatCount = 0;

    public CommonHeartbeatHandler(String name) {
        this.name = name;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext context, Object msg) throws Exception {
        String msgStr = msg.toString();
        if (msgStr.equals(PING_MSG)) {
            sendPongMsg(context);
        } else if (msgStr.equals(PONG_MSG)){
            System.out.println(name + " get pong msg from " + context.channel().remoteAddress());
        } else {
            handleData(context, msgStr);
        }
    }

    protected void sendPingMsg(ChannelHandlerContext context) {
        context.writeAndFlush(PING_MSG + "\r\n");
        heartbeatCount++;
        System.out.println(name + " sent ping msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);
    }

    private void sendPongMsg(ChannelHandlerContext context) {
        context.writeAndFlush(PONG_MSG + "\r\n");
        heartbeatCount++;
        System.out.println(name + " sent pong msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);
    }

    protected abstract void handleData(ChannelHandlerContext channelHandlerContext, String msg);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // IdleStateHandler 所产生的 IdleStateEvent 的处理逻辑.
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case READER_IDLE:
                    handleReaderIdle(ctx);
                    break;
                case WRITER_IDLE:
                    handleWriterIdle(ctx);
                    break;
                case ALL_IDLE:
                    handleAllIdle(ctx);
                    break;
                default:
                    break;
            }
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("---" + ctx.channel().remoteAddress() + " is active---");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("---" + ctx.channel().remoteAddress() + " is inactive---");
    }

    protected void handleReaderIdle(ChannelHandlerContext ctx) {
        System.err.println("---READER_IDLE---");
    }

    protected void handleWriterIdle(ChannelHandlerContext ctx) {
        System.err.println("---WRITER_IDLE---");
    }

    protected void handleAllIdle(ChannelHandlerContext ctx) {
        System.err.println("---ALL_IDLE---");
    }
    
}

客户端Handler

protected void doConnect() {
        if (channel != null && channel.isActive()) {
            return;
        }

        ChannelFuture future = bootstrap.connect("127.0.0.1", 12345);

        future.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture futureListener) throws Exception {
                if (futureListener.isSuccess()) {
                    channel = futureListener.channel();
                    System.out.println("Connect to server successfully!");
                } else {
                    System.out.println("Failed to connect to server, try connect after 10s");

                    futureListener.channel().eventLoop().schedule(new Runnable() {
                        @Override
                        public void run() {
                            doConnect();
                        }
                    }, 10, TimeUnit.SECONDS);
                }
            }
        });
    }

连接的时候,注册一个listener,当操作完成后,如果成功,设置好连接,如果失败,通过futureListener.channel().eventLoop().schedule()方法运行一个新的线程,10s后在线程中继续执行doConnect()方法。

那如何在断线的时候触发doConnect()方法呢?答案就是handler的channelInactive方法,当 TCP 连接断开时, 会回调 channelInactive 方法,我们只需要在该方法里重新调用doConnect方法即可。

@Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        client.doConnect();
    }

你可能感兴趣的:(netty)