Netty在Android开发中的应用实战系列(三)——— 心跳处理 | 断线重连

阅读本文建议从第一篇开始往后看

本系列文章

一、客户端添加心跳处理

这里就需要提到一个netty重要的IdleStateHandler,用于处理心跳机制;为当前连接通道设置 读、写、读写 空闲超时时间,当达到了设定的时间那么就会回调ClientHandler中的userEventTriggered(ChannelHandlerContext ctx, Object evt)函数。

这里在解释一下什么叫空闲超时:假设你设置了客户端读超时为10s,如果持续10s内客户端没有收到数据(也就是服务端没有发送数据过来)那么就会回调userEventTriggered函数,如果有一直收到数据那么就不会回调userEventTriggered函数;直到持续10s没有收到数据则继续触发userEventTriggered函数。

  • IdleStateHandler()常用的构造函数,三个参数的释义
public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
        this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
    }
  • readerIdleTimeSeconds 空闲读的时长
  • writerIdleTimeSeconds 空闲写的时长
  • writerIdleTimeSeconds 空闲读写的时长

IdleStateHandler的使用

  • 与之前添加编解码器一样,往ChannelPipeline添加即可
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap()
        // 省略部分代码
        .handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                //添加心跳处理Handler
                pipeline.addLast(new IdleStateHandler(10, 0, 0));
                //省略部分代码
            }
        });
// 连接到服务端
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
  • ClientHandler中进行心跳数据包发送
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    super.userEventTriggered(ctx, evt);
    if (evt instanceof IdleStateEvent) {
        if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) {
            sendHeartPkg(ctx);
        }
    } else {
        super.userEventTriggered(ctx, evt);
    }
}
/**
 * 发送心跳
 */
private void sendHeartPkg(ChannelHandlerContext ctx) {
    PkgDataBean bean = new PkgDataBean();
    bean.setCmd((byte) 0x02);
    bean.setData("心跳数据包");
    bean.setDataLength((byte) bean.getData().getBytes().length);
    ctx.channel().writeAndFlush(bean);
    Log.d(TAG, "客户端发送心跳成功");
}
  • 客户端运行的效果
    在这里插入图片描述
  • 服务端运行的效果
    在这里插入图片描述

二、服务端收到了客户端的心跳,也是需要回应客户端;服务端之需要在ServerHandler中稍微修改一下即可

    /**
     * 当收到数据的回调
     *
     * @param ctx  封装的连接对像
     * @param bean
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, PkgDataBean bean) throws Exception {
        switch (bean.getCmd()) {
            case 0x02:
                //响应客户端心跳
                bean.setCmd((byte) 0x03);
                ctx.channel().writeAndFlush(bean);
                break;
            default:
                break;
        }
        Log.d(TAG, "收到了解码器处理过的数据:" + bean.toString());
    }
  • 我们将收到的数据包,修改下命令字节为0x03然后再发送回给客户端

有了心跳机制就可以很好的判断连接是否是通畅,是否可以正常收发数据了

三、断线重连处理

当我们的网络不稳定,或者服务端一直未响应心跳、又或者切换网络导致连接中断;这个是就需要让它自动重连服务端,恢复链接

3.1 首先需要修改下NettyClient的连接代码,将NettyClient实例传递给ClientHandler

//...
private Bootstrap bootstrap;

public void connect() {
    try {
        NioEventLoopGroup group = new NioEventLoopGroup();
        bootstrap = new Bootstrap()
                // 指定channel类型
                .channel(NioSocketChannel.class)
                // 指定EventLoopGroup
                .group(group)
                // 指定Handler
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new IdleStateHandler(10, 0, 0));
                        //添加发送数据编码器
                        pipeline.addLast(new ClientEncoder());
                        //添加数据处理器
                        pipeline.addLast(new ClientHandler(NettyClient.this));
                    }
                });
        // 连接到服务端
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
        //获取连接通道
        channel = channelFuture.sync().channel();
        handler.obtainMessage(0, "连接成功").sendToTarget();
    } catch (Exception e) {
        handler.obtainMessage(0, "连接失败").sendToTarget();
        Log.e(TAG, "连接失败:" + e.getMessage());
        e.printStackTrace();
    }
}

/**
 * 重连
 */
public void reConnect() {
    try {
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
        channel = channelFuture.sync().channel();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

3.2 客户端与服务端连接断开了,那么会回调我们Handler中的channelInactive函数;所以只需要在这断开的回调启动重连任务就可以

public class ClientHandler extends SimpleChannelInboundHandler<Object> {

    private static final String TAG = "ClientHandler";
    private NettyClient client;

    public ClientHandler(NettyClient nettyClient) {
        this.client = nettyClient;

    }

    /**
     * 与服务端断开的回调
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        Log.d(TAG, "与服务端断开连接:" + ctx.toString());
        //启动重连
        reConnect(ctx);
    }

    /**
     * 5s重连一次服务端
     */
    private void reConnect(final ChannelHandlerContext ctx) {
        EventLoop loop = ctx.channel().eventLoop();
        loop.schedule(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "连接断开,发起重连");
                client.reConnect();
            }
        }, 5, TimeUnit.SECONDS);
    }
}

通过获得EventLoop,然后创建一个循环任务

3.3 当把客户端的网络断开然后在打开,然后发现了如下错误

在这里插入图片描述
为什么抛了个错呢❌?原因当然是客户端访问不到服务端了,自然就抛了个错误;这样也就导致了刚才设置的5秒重连任务也就终止了。
那么这种情况要怎么处理呢?这种情况就需要用到ChannelFutureListener,监听连接状态

3.4 在NettyClient中为连接添加一个ChannelFutureListener监听连接状态,如下:

//...
private Bootstrap bootstrap;

public void connect() {
    try {
        NioEventLoopGroup group = new NioEventLoopGroup();
        bootstrap = new Bootstrap()
                // 指定channel类型
                .channel(NioSocketChannel.class)
                // 指定EventLoopGroup
                .group(group)
                // 指定Handler
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new IdleStateHandler(10, 0, 0));
                        //添加发送数据编码器
                        pipeline.addLast(new ClientEncoder());
                        //添加数据处理器
                        pipeline.addLast(new ClientHandler(NettyClient.this));
                    }
                });
        // 连接到服务端
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
        // 添加连接状态监听
		channelFuture.addListener(new ConnectListener(this));
        //获取连接通道
        channel = channelFuture.sync().channel();
        handler.obtainMessage(0, "连接成功").sendToTarget();
    } catch (Exception e) {
        handler.obtainMessage(0, "连接失败").sendToTarget();
        Log.e(TAG, "连接失败:" + e.getMessage());
        e.printStackTrace();
    }
}

/**
 * 重连
 */
public void reConnect() {
    try {
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
        // 添加连接状态监听
		channelFuture.addListener(new ConnectListener(this));
        channel = channelFuture.sync().channel();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

//添加连接状态监听
channelFuture.addListener(new ConnectListener(this));

  • ConnectListener.java实现ChannelFutureListener来获取连接状态
public class ConnectListener implements ChannelFutureListener {

    private static final String TAG = "ConnectListener";
    private NettyClient nettyClient;

    public ConnectListener(NettyClient nettyClient) {
        this.nettyClient = nettyClient;
    }

    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        //连接失败发起重连
        if (!channelFuture.isSuccess()) {
            final EventLoop loop = channelFuture.channel().eventLoop();
            loop.schedule(new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "连接失败,发起重连");
                    nettyClient.reConnect();
                }
            }, 5, TimeUnit.SECONDS);
        }
    }

}
  • 现在我们再来看下运行效果
    在这里插入图片描述
  • 可以看到程序一直在重连中达到了我们需要的效果,当再次打开网络的时候就可以连接成功了

四、到这里这篇文章的内容就说完了,下一篇将说下Netty的粘包和拆包这也是开发中经常遇见的问题

Demo将会在本系列第四篇文章中给出

发布了140 篇原创文章 · 获赞 546 · 访问量 54万+

猜你喜欢

转载自blog.csdn.net/a_zhon/article/details/100941312