Android開發之使用Netty進行Socket編程(二)

Android開發之使用Netty進行Socket編程(一) 概括了一些SocketNIO的基本概念,下面正式介紹開發中使用到的Netty API以及在Android客戶端中如何使用Netty通過Socket與服務器交互。

1 Channel

A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind.A channel provides a user:

  1. the current state of the channel (e.g. is it open? is it connected?),
  2. the configuration parameters of the channel (e.g. receive buffer size),
  3. the I/O operations that the channel supports (e.g. read, write, connect, and bind), and
  4. the ChannelPipeline which handles all I/O events and requests associated with the channel.

Java NIO中,Channel的作用類似于Java IO的Stream(實際上有所不同,參考上一篇文章)。文檔里也說的很清楚,在客戶端與服務端建立連接后,網絡IO操作是在Channel對象上進行的。

2 ChannelHandler

public interface ChannelHandler

Handles an I/O event or intercepts an I/O operation, and forwards it to its next handler in its ChannelPipeline
.

當客戶端與服務器建立起連接后,ChannelHandler的方法是被網絡event(這里的event是廣義的)觸發的,由ChannelHandler直接處理輸入輸出數據,并傳遞到管道中的下一個ChannelHandler中。
通過Channel或者ChannelHandlerContext發生的請求/響應event 就是在管道中ChannelHandler傳遞。

ChannelInboundHandler對從客戶端發往服務器的報文進行處理,一般用來執行解碼、讀取客戶端數據、進行業務處理等;ChannelOutboundHandler對從服務器發往客戶端的報文進行處理,一般用來進行編碼、發送報文到客戶端。
一般就是繼承ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter,因為Adapter把定制(custom) ChannelHandler的麻煩減小到了最低,Adapter本身已經實現了基礎的數據處理邏輯(例如將event轉發到下一個handler),你可以只重寫那些你想要特別實現的方法。

示例:

/**
 * Handles a server-side channel.
 */
public class NettyClientHandler extends ChannelInboundHandlerAdapter{ // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);// (3)
        String body = new String(req, "UTF-8");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

具體使用:

  1. 一般做法是繼承ChannelInboundHandlerAdapter,這個類實現了ChannelHandler接口,ChannelHandler提供了許多事件處理的接口方法,然后你可以覆蓋這些方法。現在僅僅只需要繼承ChannelHandlerAdapter類而不是你自己去實現接口方法。
  2. 以上我們覆蓋了channelRead()事件處理方法。每當從客戶端收到新的數據時,這個方法會在收到消息時被調用,這個例子中,收到的消息的類型是ByteBuf
  3. 和NIO一樣,讀取數據時,它是直接讀到緩沖區中;在寫入數據時,它也是寫入到緩沖區中。在TCP/IP中,NETTY會把讀到的數據放到ByteBuf的數據結構中。所以這里讀取在ByteBuf的信息,得到服務器返回的內容。

3 ChannelPipeline

public interface ChannelPipeline extends Iterable < Map.Entry < String , ChannelHandler >>

A list of ChannelHandler
s which handles or intercepts inbound events and outbound operations of a Channel
. ChannelPipeline
implements an advanced form of the Intercepting Filter pattern to give a user full control over how an event is handled and how the ChannelHandler
s in a pipeline interact with each other.

ChannelPipeline作為放置ChannelHandler的容器,采用了J2EE的 攔截過濾模式,用戶可以定義管道中的ChannelHandler以哪種規則去攔截并處理事件以及在管道中的ChannelHandler之間如何通信。每個Channel都有它自己的Pipeline,當一個新的Channel被創建時會自動被分配到一個Pipeline中。
ChannelHandler按如下步驟安裝到ChannelPipeline中:

  1. 一個ChannelInitializer接口實現被注冊到一個Bootstrap上:
bootstrap.handler(new ChannelInitializer<SocketChannel>() {//(1)
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
                            pipeline.addLast(nettyClientHandler);//自定義的ChannelInboundHandlerAdapter子類
                        }
                    });

注意:上面編碼器(encoders),解碼器(decoders),和ChannelInboundHandlerAdapter的子類都屬于ChannelHandler

  1. ChannelInitializer.initChannel()被調用時,這個ChannelInitializer會往管道(pipeline)中安裝定制的一組ChannelHandler
  2. 然后這個ChannelInitializer把自己從ChannelPipeline中移除

4 NioEventLoopGroup

MultithreadEventLoopGroup
implementations which is used for NIO Selector
based Channel
s.

NioEventLoopGroup繼承了MultithreadEventLoopGroup,是用來處理NIO操作的多線程事件循環器,Netty提供了許多不同的EventLoopGroup的實現用來處理不同傳輸協議。
NioEventLoopGroup實際上就是個線程池,NioEventLoopGroup在后臺啟動了n個IO線程(NioEventLoop)來處理Channel事件,每一個NioEventLoop負責處理m個Channel,NioEventLoopGroup從NioEventLoop數組里挨個取出NioEventLoop來處理Channel(詳見《NioEventLoopGroup繼承層次結構》)
相比于服務端,客戶端只需要創建一個EventLoopGroup,因為它不需要獨立的線程去監聽客戶端連接,而且Netty是異步事件驅動的NIO框架,它的連接和所有IO操作都是異步的,因此不需要創建單獨的連接線程。

5 Bootstrap

Bootstrap以及 ServerBootstrap類都繼承自 AbstractBootstrap。官方API文檔中的解釋是:

AbstractBootstrap
is a helper class that makes it easy to bootstrap a Channel
. It support method-chaining to provide an easy way to configure the AbstractBootstrap
.

Bootstrap中文翻譯就是引導程序,就是作為管理Channel的一個輔助類。可以通過“方法鏈”的代碼形式(類似Builder模式)去配置一個Bootstrap,創建Channel并發起請求。Bootstrap類為一個應用的網絡層配置提供了容器,客戶端通過它來jianjie。
示例:

EventLoopGroup group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class)
              .group(group);
              .handler(new ChannelInitializer<SocketChannel>() {//(1)
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
                            pipeline.addLast(nettyClientHandler);//(2)
                        }
                    });
  1. ChannelInitializer是一個特殊的ChannelHandler類,作用是幫助使用者配置一個新的Channel。也許你想通過增加一些新的ChannelHandler子類來操作一個新的Channel或者通過其對應的ChannelPipeline來實現你的網絡程序。當你的程序變的復雜時,可能會增加更多的ChannelHandler子類到pipeline上。
  2. 每個Channel都有ChannelPipeline,在Channel的pipeline中加入handler,這里的ChannelHandler類經常會被用來處理一個最近的已經接收的Channel。所以這里的Channel已經不是NIO中的Channel了,她是netty的Channel。

6 建立連接并發起請求

public class NettyClient {
    private Channel channel ;

    public void connect(int port,String host){
        EventLoopGroup group = new NioEventLoopGroup();
        try {//配置Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<SocketChannel>() {

                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
                            pipeline.addLast(new NettyClientHandler () );
                }
            });

            //發起異步連接操作
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            
             channel = channelFuture.channel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            //關閉,釋放線程資源
            group.shutdownGracefully();
        }
    }


  public void sendMessage(String msg){//連接成功后,通過Channel提供的接口進行IO操作
   try {
         if (channel != null && channel.isOpen()) {
            channel.writeAndFlush(sendMsg).sync();     //(1)
            Log.d(TAG, "send succeed " + sendMsg);
                        } else {
                            throw new Exception("channel is null | closed");
                        }
                    } catch (Exception e) {
                        sendReconnectMessage();
                        e.printStackTrace();
                    }  
  } 
    @Test
    public void nettyClient(){
        new NettyClient().connect(8989, "localhost");
    }
    
}
  1. Channel 及ChannelHandlerContext對象提供了許多操作,能夠觸發各種各樣的I/O事件和操作。
    write(Object)方法不會使消息寫入到Channel上,他被緩沖在了內部,你需要調用flush()方法來把緩沖區中數據強行輸出。或者可以用更簡潔的writeAndFlush(msg)以達到同樣的目的。

7 總結

簡而言之,在客戶端上使用Netty,業務流程如下:

  1. 構建Bootstrap,其中包括設置好ChannelHandler來處理將來接收到的數據。
  2. 由Boostrap發起連接。
  3. 連接成功建立后,得到一個ChannelFuture對象,代表了一個還沒有發生的I/O操作。這意味著任何一個請求操作都不會馬上被執行,因為在Netty里所有的操作都是異步的。
  4. 通過Channel對象的writeAndFlush(Object msg)方法往服務端發送數據,接收到的數據 會在ChannelHandler的實現類中的channelRead(ChannelHandlerContext ctx, Object msg)中獲取到被讀到緩沖區的數據——(ByteBuf) msg

8 問題

  1. TCP連接中,NETTY會把讀到的數據放到ByteBuf的數據結構中。基于流的傳輸并不是一個數據包隊列,而是一個字節隊列。即使服務器發送了2個獨立的消息,客戶端也不會作為2次消息處理而僅僅是作為一連串的字節進行讀取。因此這是不能保證你遠程寫入的數據就會準確地讀取。尤其是 服務器發回的消息長度 過長的時候,一次消息將有可能被拆分到不同的ByteBuf數據段中,很有可能需要多次讀取ByteBuf,才能把一個消息完整拿到。
    解決方案:
    ByteToMessageDecoderChannelHandler的一個實現類,他可以在處理數據拆分的問題上變得很簡單。

相關鏈接

  1. Netty 5用戶指南
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容