閱讀704 返回首頁    go 魔獸


《Netty 實戰》Netty In Action中文版 第2章——你的第一款Netty應用程序(二)

2.3.2 引導服務器

在討論過由EchoServerHandler實現的核心業務邏輯之後,我們現在可以探討引導服務器本身的過程了,具體涉及以下內容:

  • 綁定到服務器將在其上監聽並接受傳入連接請求的端口;
  • 配置Channel,以將有關的入站消息通知給EchoServerHandler實例。

傳輸

 

在這一節中,你將遇到術語傳輸。在網絡協議的標準多層視圖中,傳輸層提供了端到端的或者主機到主機的通信服務。

因特網通信是建立在TCP傳輸之上的。除了一些由Java NIO實現提供的服務器端性能增強之外,NIO傳輸大多數時候指的就是TCP傳輸。

我們將在第4章對傳輸進行詳細的討論。

代碼清單2-2展示了EchoServer類的完整代碼。

代碼清單2-2 EchoServer

public class EchoServer {
    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println(
                "Usage: " + EchoServer.class.getSimpleName() +
                " ");
        }
        int port = Integer.parseInt(args[0]);   ⇽--- 設置端口值(如果端口參數的格式不正確,則拋出一個NumberFormatException
        new EchoServer(port).start();    ⇽---  調用服務器的start()方法
    }
    public void start() throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();    ⇽---   創建Event-LoopGroup
        try {
             ServerBootstrap b = new ServerBootstrap();    ⇽---    創建Server-Bootstrap
             b.group(group)
                 .channel(NioServerSocketChannel.class)   ⇽---   指定所使用的NIO傳輸Channel
                 .localAddress(new InetSocketAddress(port))   ⇽---   使用指定的端口設置套接字地址
                .childHandler(new ChannelInitializer(){    ⇽---   添加一個EchoServer-
Handler到子ChannelChannelPipeline
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception {
                         ch.pipeline().addLast(serverHandler);[4]   ⇽---  EchoServerHandler被標注為@Shareable,所以我們可以總是使用同樣的實例
                    }
                 });
            ChannelFuture f = b.bind().sync();    ⇽---    異步地綁定服務器;調用sync()方法阻塞等待直到綁定完成
            f.channel().closeFuture().sync();  ⇽---   獲取ChannelCloseFuture,並且阻塞當前線程直到它完成
        } finally {
            group.shutdownGracefully().sync();    ⇽---    關閉EventLoopGroup,釋放所有的資源
        }
    }
}

在處,你創建了一個ServerBootstrap實例。因為你正在使用的是NIO傳輸,所以你指定了NioEventLoopGroup來接受和處理新的連接,並且將Channel的類型指定為NioServer-SocketChannel。在此之後,你將本地地址設置為一個具有選定端口的InetSocket-Address。服務器將綁定到這個地址以監聽新的連接請求。

在處,你使用了一個特殊的類——ChannelInitializer。這是關鍵。當一個新的連接被接受時,一個新的子Channel將會被創建,而ChannelInitializer將會把一個你的EchoServerHandler的實例添加到該ChannelChannelPipeline中。正如我們之前所解釋的,這個ChannelHandler將會收到有關入站消息的通知。

雖然NIO是可伸縮的,但是其適當的尤其是關於多線程處理的配置並不簡單。Netty的設計封裝了大部分的複雜性,而且我們將在第3章中對相關的抽象(EventLoopGroupSocket-ChannelChannelInitializer)進行詳細的討論。

接下來你綁定了服務器,並等待綁定完成。(對sync()方法的調用將導致當前Thread阻塞,一直到綁定操作完成為止)。在處,該應用程序將會阻塞等待直到服務器的Channel關閉(因為你在ChannelClose Future上調用了sync()方法)。然後,你將可以關閉EventLoopGroup,並釋放所有的資源,包括所有被創建的線程。

這個示例使用了NIO,因為得益於它的可擴展性和徹底的異步性,它是目前使用最廣泛的傳輸。但是也可以使用一個不同的傳輸實現。如果你想要在自己的服務器中使用OIO傳輸,將需要指定OioServerSocketChannelOioEventLoopGroup。我們將在第4章中對傳輸進行更加詳細的探討。

與此同時,讓我們回顧一下你剛完成的服務器實現中的重要步驟。下麵這些是服務器的主要代碼組件:

  • EchoServerHandler實現了業務邏輯;
  • main()方法引導了服務器;

引導過程中所需要的步驟如下:

  • 創建一個ServerBootstrap的實例以引導和綁定服務器;
  • 創建並分配一個NioEventLoopGroup實例以進行事件的處理,如接受新連接以及讀/寫數據;
  • 指定服務器綁定的本地的InetSocketAddress
  • 使用一個EchoServerHandler的實例初始化每一個新的Channel
  • 調用ServerBootstrap.bind()方法以綁定服務器。

在這個時候,服務器已經初始化,並且已經就緒能被使用了。在下一節中,我們將探討對應的客戶端應用程序的代碼。

2.4 編寫Echo客戶端

Echo客戶端將會:

(1)連接到服務器;

(2)發送一個或者多個消息;

(3)對於每個消息,等待並接收從服務器發回的相同的消息;

(4)關閉連接。

編寫客戶端所涉及的兩個主要代碼部分也是業務邏輯和引導,和你在服務器中看到的一樣。

2.4.1 通過ChannelHandler實現客戶端邏輯

如同服務器,客戶端將擁有一個用來處理數據的ChannelInboundHandler。在這個場景下,你將擴展SimpleChannelInboundHandler類以處理所有必須的任務,如代碼清單2-3所示。這要求重寫下麵的方法:

  • channelActive()——在到服務器的連接已經建立之後將被調用;
  • channelRead0()[5]——當從服務器接收到一條消息時被調用;
  • exceptionCaught()——在處理過程中引發異常時被調用。

代碼清單2-3 客戶端的ChannelHandler

@Sharable     ⇽---  標記該類的實例可以被多個Channel共享
public class EchoClientHandler extends
    SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",     ⇽---  當被通知Channel是活躍的時候,發送一條消息
        CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
        System.out.println(    ⇽---  記錄已接收消息的轉儲
            "Client received: " + in.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,     ⇽---  在發生異常時,記錄錯誤並關閉Channel 
        Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

首先,你重寫了channelActive()方法,其將在一個連接建立時被調用。這確保了數據將會被盡可能快地寫入服務器,其在這個場景下是一個編碼了字符串"Netty rocks!"的字節緩衝區。

接下來,你重寫了channelRead0()方法。每當接收數據時,都會調用這個方法。需要注意的是,由服務器發送的消息可能會被分塊接收。也就是說,如果服務器發送了5字節,那麼不能保證這5字節會被一次性接收。即使是對於這麼少量的數據,channelRead0()方法也可能會被調用兩次,第一次使用一個持有3字節的ByteBuf(Netty的字節容器),第二次使用一個持有2字節的ByteBuf。作為一個麵向流的協議,TCP保證了字節數組將會按照服務器發送它們的順序被接收。

重寫的第三個方法是exceptionCaught()。如同在EchoServerHandler(見代碼清單2-2)中所示,記錄Throwable,關閉Channel,在這個場景下,終止到服務器的連接。

SimpleChannelInboundHandler與ChannelInboundHandler

 

你可能會想:為什麼我們在客戶端使用的是SimpleChannelInboundHandler,而不是在Echo- ServerHandler中所使用的ChannelInboundHandlerAdapter呢?這和兩個因素的相互作用有關:業務邏輯如何處理消息以及Netty如何管理資源。

在客戶端,當channelRead0()方法完成時,你已經有了傳入消息,並且已經處理完它了。當該方法返回時,SimpleChannelInboundHandler負責釋放指向保存該消息的ByteBuf的內存引用。

EchoServerHandler中,你仍然需要將傳入消息回送給發送者,而write()操作是異步的,直到channelRead()方法返回後可能仍然沒有完成(如代碼清單2-1所示)。為此,EchoServerHandler擴展了ChannelInboundHandlerAdapter,其在這個時間點上不會釋放消息。

消息在EchoServerHandlerchannelReadComplete()方法中,當writeAndFlush()方法被調用時被釋放(見代碼清單2-1)。

第5章和第6章將對消息的資源管理進行詳細的介紹。

2.4.2 引導客戶端

如同將在代碼清單2-4中所看到的,引導客戶端類似於引導服務器,不同的是,客戶端是使用主機和端口參數來連接遠程地址,也就是這裏的Echo服務器的地址,而不是綁定到一個一直被監聽的端口。

代碼清單2-4 客戶端的主類

public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
       EventLoopGroup group = new NioEventLoopGroup();
        try {    ⇽---  創建Bootstrap
            Bootstrap b = new Bootstrap();     ⇽---  指定EventLoopGroup以處理客戶端事件;需要適用於NIO的實現
            b.group(group)    
                 .channel(NioSocketChannel.class)     ⇽---  適用於NIO傳輸的Channel類型
                 .remoteAddress(new InetSocketAddress(host, port))     ⇽---  設置服務器的InetSocketAddr-ess
![](/api/storage/getbykey/screenshow?key=17043add7e9c14a5d3f7)                .handler(new ChannelInitializer<SocketChannel>() {    ⇽---  在創建Channel時,向ChannelPipeline中添加一個Echo-ClientHandler實例
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception {
                   ch.pipeline().addLast(
                        new EchoClientHandler());
                    }
                });
            ChannelFuture f = b.connect().sync();     ⇽---  連接到遠程節點,阻塞等待直到連接完成
            f.channel().closeFuture().sync();      ⇽---  阻塞,直到Channel關閉
        } finally {
            group.shutdownGracefully().sync();       ⇽---  關閉線程池並且釋放所有的資源
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println(
                "Usage: " + EchoClient.class.getSimpleName() +
                " <host> <port>");
            return;
        }

        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

和之前一樣,使用了NIO傳輸。注意,你可以在客戶端和服務器上分別使用不同的傳輸。例如,在服務器端使用NIO傳輸,而在客戶端使用OIO傳輸。在第4章,我們將探討影響你選擇適用於特定用例的特定傳輸的各種因素和場景。

讓我們回顧一下這一節中所介紹的要點:

  • 為初始化客戶端,創建了一個Bootstrap實例;
  • 為進行事件處理分配了一個NioEventLoopGroup實例,其中事件處理包括創建新的連接以及處理入站和出站數據;
  • 為服務器連接創建了一個InetSocketAddress實例;
  • 當連接被建立時,一個EchoClientHandler實例會被安裝到(該Channel的)ChannelPipeline中;
  • 在一切都設置完成後,調用Bootstrap.connect()方法連接到遠程節點;

完成了客戶端,你便可以著手構建並測試該係統了。

轉載自 並發編程網 - ifeve.comhttps://ifeve.com/

最後更新:2017-05-18 20:36:20

  上一篇:go  Clojure世界:API文檔生成
  下一篇:go  淘寶開源metaq的python客戶端