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

public void connect() {
    try {
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap 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();
    }
}

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.connect();
            }
        }, 5, TimeUnit.SECONDS);
    }
}

通過獲得EventLoop,然後創建一個循環任務

3.3 當把客戶端的網絡斷開然後在打開,然後發現瞭如下錯誤

在這裏插入圖片描述
爲什麼拋了個錯呢❌?原因當然是客戶端訪問不到服務端了,自然就拋了個錯誤;這樣也就導致了剛纔設置的5秒重連任務也就終止了。
那麼這種情況要怎麼處理呢?這種情況就需要用到ChannelFutureListener,監聽連接狀態

3.4 在NettyClient中爲連接添加一個ChannelFutureListener監聽連接狀態,如下:

public void connect() {
    try {
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap 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();
    }
}

//添加連接狀態監聽
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.connect();
                }
            }, 5, TimeUnit.SECONDS);
        }
    }

}
  • 現在我們再來看下運行效果
    在這裏插入圖片描述
  • 可以看到程序一直在重連中達到了我們需要的效果,當再次打開網絡的時候就可以連接成功了

四、到這裏這篇文章的內容就說完了,下一篇將說下Netty的粘包和拆包這也是開發中經常遇見的問題

Demo將會在本系列文章中最後一篇給出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章