Netty在Android中使用

引用netty介紹文檔中的一句話:Netty適合用來開發高性能長連接的客戶端或服務器
其次netty基於NIO,並做了封裝與優化以及一些高級算法,使得使用起來相比原生NIO更加容易,性能更好

1.使用方法

  • 1.1建立連接
        //進行初始化
        NioEventLoopGroup  nioEventLoopGroup = new NioEventLoopGroup(); //初始化線程組
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.channel(NioSocketChannel.class).group(nioEventLoopGroup);
        bootstrap.option(ChannelOption.TCP_NODELAY, true); //無阻塞
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true); //長連接
        bootstrap.option(ChannelOption.SO_TIMEOUT, SLEEP_TIME); //收發超時
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                        .addLast(new ByteArrayDecoder())  //接收解碼方式
                        .addLast(new ByteArrayEncoder())  //發送編碼方式
                        .addLast(new ChannelHandle(ConnectService.this)); //處理數據接收
            }
        });

//開始建立連接並監聽返回
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(AppInfo.serverIp, AppInfo.serverPort));
                channelFuture.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) {
                        AppInfo.isConnected = future.isSuccess();
                        if (future.isSuccess()) {
                            Log.d(TAG, "connect success !");                 
                        } else {
                            Log.d(TAG, "connect failed !");   
                        }
                    }
                });
  • 1.2數據發送
private void sendDataToServer(byte[] sendBytes) {
        if (sendBytes != null && sendBytes.length > 0) {
            if (channelFuture != null && channelFuture.channel().isActive()) {
                channelFuture.channel().writeAndFlush(sendBytes);
            }
        }
    }
  • 1.3實現數據接收類:
public static class ChannelHandle extends SimpleChannelInboundHandler<SimpleProtocol> {

        private ConnectService connectService;

        public ChannelHandle(ConnectService connectService) {
            this.connectService = connectService;
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            Log.e(TAG, "channelInactive 連接失敗");
        }

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            super.userEventTriggered(ctx, evt);
            if (evt instanceof IdleStateEvent) {
                IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
                if (idleStateEvent.state().equals(IdleState.WRITER_IDLE)) {
                    Log.d(TAG, "userEventTriggered write idle");
                    if (connectService == null){
                        return;
                    }
                }else if (idleStateEvent.state().equals(IdleState.READER_IDLE)){
                    Log.d(TAG, "userEventTriggered read idle");
                    ctx.channel().close();
                }
            }
        }
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, byte[] data) throws Exception {
            if (simpleProtocol == null){
                return;
            }
           //處理接收到的數據data
        }

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

2.常見問題處理及通信優化

  • 2.1心跳
    在初始化時添加:.addLast(new IdleStateHandler(30, 10, 0)) //參數1:代表讀套接字超時的時間,例如30秒沒收到數據會觸發讀超時回調;參數2:代表寫套接字超時時間,例如10秒沒有進行寫會觸發寫超時回調;參數3:將在未執行讀取或寫入時觸發超時回調,0代表不處理;讀超時儘量設置大於寫超時代表多次寫超時時寫心跳包,多次寫了心跳數據仍然讀超時代表當前連接錯誤,即可斷開連接重新連接
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                        .addLast(new ByteArrayDecoder())  //接收解碼方式
                        .addLast(new ByteArrayEncoder())  //發送編碼方式
                        .addLast(new ChannelHandle(ConnectService.this)); //處理數據接收
                        .addLast(new IdleStateHandler(30, 10, 0))
            }
        });

在添加初始化.addLast(new ChannelHandle(ConnectService.this));的ChannelHandle 類中重寫超時回調方法

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            super.userEventTriggered(ctx, evt);
            if (evt instanceof IdleStateEvent) {
                IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
                if (idleStateEvent.state().equals(IdleState.WRITER_IDLE)) {
                    //寫超時,此時可以發送心跳數據給服務器
                    Log.d(TAG, "userEventTriggered write idle");
                    if (connectService == null){
                        return;
                    }
                }else if (idleStateEvent.state().equals(IdleState.READER_IDLE)){
                    //讀超時,此時代表沒有收到心跳返回可以關閉當前連接進行重連
                    Log.d(TAG, "userEventTriggered read idle");
                    ctx.channel().close();
                }
            }
        }
  • 2.2重連接
private void reConnect(){
if (channelFuture != null && channelFuture.channel() != null && channelFuture.channel().isActive()){
                        channelFuture.channel().close();//已經連接時先關閉當前連接,關閉時回調exceptionCaught進行重新連接
                    }else {
                        connect(); //當前未連接,直接連接即可
                    }
}

連接失敗回調

@Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            Log.e(TAG, "channelInactive 連接失敗");      
            if (connectService != null) {
                connectService.connect(); //重新連接
            }
        }

連接錯誤回調

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
            Log.i(TAG, "exceptionCaught");
            cause.printStackTrace();
            ctx.close();  //關閉連接後回調channelInactive會重新調用connectService.connect();
        }
  • 2.3數據接收不全
    出現問題的原因1:接收數據的緩衝區太小 解決辦法:
bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(5000, 5000, 8000)); //接收緩衝區 最小值太小時數據接收不全

出現問題的原因2:沒有處理粘包拆包問題 解決方法:
添加包的接收處理:addLast(new SimpleProtocolDecoder()) 解包處理避免粘包的主要步驟是:1.解協議頭 2.解包長 3.讀指定的包長後返回

public class SimpleProtocolDecoder extends ByteToMessageDecoder{

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
        if (buffer.readableBytes() >= SimpleProtocol.BASE_LENGTH){
            int beginReader;
            while (true){
                beginReader = buffer.readerIndex();
                buffer.markReaderIndex();
                if (buffer.readShort() == AppInfo.HEAD_TAG){
                    buffer.readByte();
                    break;
                }
                buffer.resetReaderIndex();
                buffer.readByte();
                if (buffer.readableBytes() < SimpleProtocol.BASE_LENGTH){
                    return;
                }
            }

            int length = MyUtil.shortToBig(buffer.readShort()) - SimpleProtocol.BASE_LENGTH;
            //buffer.readerIndex(beginReader);
            //Log.e("TAG","length:"+buffer.readableBytes());
            if (buffer.readableBytes() < length){
                buffer.readerIndex(beginReader);
                return;
            }
            //byte[] data = new byte[length];
            byte[] data = new byte[length + SimpleProtocol.BASE_LENGTH];
            int pos = buffer.readerIndex() - SimpleProtocol.BASE_LENGTH;
            if (pos < 0){
                return;
            }
            buffer.readerIndex(pos);
            buffer.readBytes(data);

            SimpleProtocol simpleProtocol = new SimpleProtocol((short) data.length,data);
            out.add(simpleProtocol);
        }
    }

}

3.注意細節

  • 把連接,讀寫都放到service中,模塊封裝方便各個組件調用
  • 每次連接時使用線程池,避免ANR,節省開銷
  • 重新連接時不要對netty重複初始化,同時可以使用handler控制重連間隔避免短時間內死循環重複調用
  • 重連時close當前channel的同時需要調用以下代碼:
if (channelFuture != null && channelFutureListener != null){
                    channelFuture.removeListener(channelFutureListener);         
                    channelFuture.cancel(true);
                }

4.其他

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