讀古今文學網 > Netty實戰 > 第2章 你的第一款Netty應用程序 >

第2章 你的第一款Netty應用程序

本章主要內容

  • 設置開發環境
  • 編寫Echo服務器和客戶端
  • 構建並測試應用程序

在本章中,我們將展示如何構建一個基於Netty的客戶端和服務器。應用程序很簡單:客戶端將消息發送給服務器,而服務器再將消息回送給客戶端。但是這個練習很重要,原因有兩個。

首先,它會提供一個測試台,用於設置和驗證你的開發工具和環境,如果你打算通過對本書的示例代碼的練習來為自己將來的開發工作做準備,那麼它將是必不可少的。

其次,你將獲得關於Netty的一個關鍵方面的實踐經驗,即在前一章中提到過的:通過ChannelHandler來構建應用程序的邏輯。這能讓你對在第3章中開始的對Netty API的深入學習做好準備。

2.1 設置開發環境

要編譯和運行本書的示例,只需要JDK和Apache Maven這兩樣工具,它們都是可以免費下載的。

我們將假設,你想要搗鼓示例代碼,並且想很快就開始編寫自己的代碼。雖然你可以使用純文本編輯器,但是我們仍然強烈地建議你使用用於Java的集成開發環境(IDE)。

2.1.1 獲取並安裝Java開發工具包

你的操作系統可能已經安裝了JDK。為了找到答案,可以在命令行輸入:

javac -version  

如果得到的是javac 1.7……或者1.8……,則說明已經設置好了並且可以略過此步[1]。

否則,請從http://java.com/en/download/manual.jsp處獲取JDK第8版。請留心,需要下載的是JDK,而不是Java運行時環境(JRE),其只可以運行Java應用程序,但是不能夠編譯它們。該網站為每個平台都提供了可執行的安裝程序。如果需要安裝說明,可以在同一個網站上找到相關的信息。

建議執行以下操作:

  • 將環境變量JAVA_HOME設置為你的JDK安裝位置(在Windows上,默認值將類似於C:\Program Files\Java\jdk1.8.0_121);
  • %JAVA_HOME%\bin(在Linux上為${JAVA_HOME}/bin)添加到你的執行路徑。

2.1.2 下載並安裝IDE

下面是使用最廣泛的Java IDE,都可以免費獲取:

  • Eclipse—— www.eclipse.org;
  • NetBeans—— www.netbeans.org;
  • Intellij IDEA Community Edition—— www.jetbrains.com。

所有這3種對我們將使用的構建工具Apache Maven都擁有完整的支持。NetBeans和Intellij IDEA都通過可執行的安裝程序進行分發。Eclipse通常使用Zip歸檔文件進行分發,當然也有一些自定義的版本包含了自安裝程序。

2.1.3 下載和安裝Apache Maven

即使你已經熟悉Maven了,我們仍然建議你至少大致瀏覽一下這一節。

Maven是一款廣泛使用的由Apache軟件基金會(ASF)開發的構建管理工具。Netty項目以及本書的示例都使用了它。構建和運行這些示例並不需要你成為一個Maven專家,但是如果你想要對其進行擴展,我們推薦你閱讀附錄中的Maven簡介。

你需要安裝Maven嗎

Eclipse和NetBeans[2]自帶了一個內置的Maven安裝包,對於我們的目的來說開箱即可工作得良好。如果你將要在一個擁有它自己的Maven存儲庫的環境中工作,那麼你的配置管理員可能就有一個預先配置好的能配合它使用的Maven安裝包。

在本書中文版出版時,Maven 的最新版本是3.3.9。你可以從http://maven.apache.org/ download.cgi下載適用於你的操作系統的tar.gz或者zip歸檔文件[3]。安裝很簡單:將歸檔文件的所有內容解壓到你所選擇的任意的文件夾(我們將其稱為<安裝目錄>)。這將創建目錄<安裝目錄>\apache-maven-3.3.9。

和設置Java環境一樣:

  • 將環境變量M2_HOME設置為指向<安裝目錄>\apache-maven-3.3.9;
  • %M2_HOME%\bin(或者在Linux上為${M2_HOME}/bin)添加到你的執行路徑。

這將使得你可以通過在命令行執行mvn.bat(或者mvn)來運行Maven。

2.1.4 配置工具集

如果你已經按照推薦設置好了環境變量JAVA_HOMEM2_HOME,那麼你可能會發現,當你啟動自己的IDE時,它已經發現了你的Java和Maven的安裝位置。如果你需要進行手動配置,我們所列舉的所有的IDE版本在Preferences或者Settings下都有設置這些變量的菜單項。相關的細節請查閱文檔。

這就完成了開發環境的配置。在接下來的各節中,我們將介紹你要構建的第一個Netty應用程序的詳細信息,同時我們將更加深入地瞭解該框架的API。之後,你就能使用剛剛設置好的工具來構建和運行Echo服務器和客戶端了。

2.2 Netty客戶端/服務器概覽

圖2-1從高層次上展示了一個你將要編寫的Echo客戶端和服務器應用程序。雖然你的主要關注點可能是編寫基於Web的用於被瀏覽器訪問的應用程序,但是通過同時實現客戶端和服務器,你一定能更加全面地理解Netty的API。

圖2-1 Echo客戶端和服務器

雖然我們已經談及到了客戶端,但是該圖展示的是多個客戶端同時連接到一台服務器。所能夠支持的客戶端數量,在理論上,僅受限於系統的可用資源(以及所使用的JDK版本可能會施加的限制)。

Echo客戶端和服務器之間的交互是非常簡單的;在客戶端建立一個連接之後,它會向服務器發送一個或多個消息,反過來,服務器又會將每個消息回送給客戶端。雖然它本身看起來好像用處不大,但它充分地體現了客戶端/服務器系統中典型的請求-響應交互模式。

我們將從考察服務器端代碼開始這個項目。

2.3 編寫Echo服務器

所有的Netty服務器都需要以下兩部分。

  • 至少一個ChannelHandler——該組件實現了服務器對從客戶端接收的數據的處理,即它的業務邏輯。
  • 引導——這是配置服務器的啟動代碼。至少,它會將服務器綁定到它要監聽連接請求的端口上。

在本小節的剩下部分,我們將描述Echo服務器的業務邏輯以及引導代碼。

2.3.1 ChannelHandler和業務邏輯

在第1章中,我們介紹了Future和回調,並且闡述了它們在事件驅動設計中的應用。我們還討論了ChannelHandler,它是一個接口族的父接口,它的實現負責接收並響應事件通知。在Netty應用程序中,所有的數據處理邏輯都包含在這些核心抽像的實現中。

因為你的Echo服務器會響應傳入的消息,所以它需要實現ChannelInboundHandler接口,用來定義響應入站事件的方法。這個簡單的應用程序只需要用到少量的這些方法,所以繼承Channel-InboundHandlerAdapter類也就足夠了,它提供了ChannelInboundHandler的默認實現。

我們感興趣的方法是:

  • channelRead——對於每個傳入的消息都要調用;
  • channelReadComplete——通知ChannelInboundHandler最後一次對channel-Read的調用是當前批量讀取中的最後一條消息;
  • exceptionCaught——在讀取操作期間,有異常拋出時會調用。

該Echo服務器的ChannelHandler實現是EchoServerHandler,如代碼清單2-1所示。

代碼清單2-1 EchoServerHandler

@Sharable   ← --  標示一個Channel- Handler可以被多個Channel安全地共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println(
            "Server received: " + in.toString(CharsetUtil.UTF_8));      ← --    將消息記錄到控制台       

        ctx.write(in);    ← --  將接收到的消息寫給發送者,而不沖刷出站消息

    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
            .addListener(ChannelFutureListener.CLOSE);   ←-- 將未決消息[4]沖刷到遠程節點,並且關閉該Channel
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
        Throwable cause) {
        cause.printStackTrace;     ←--  打印異常棧跟蹤
        ctx.close;  ←-- 關閉該Channel
    }
}  

ChannelInboundHandlerAdapter有一個直觀的API,並且它的每個方法都可以被重寫以掛鉤到事件生命週期的恰當點上。因為需要處理所有接收到的數據,所以你重寫了channelRead方法。在這個服務器應用程序中,你將數據簡單地回送給了遠程節點。

重寫exceptionCaught方法允許你對Throwable的任何子類型做出反應,在這裡你記錄了異常並關閉了連接。雖然一個更加完善的應用程序也許會嘗試從異常中恢復,但在這個場景下,只是通過簡單地關閉連接來通知遠程節點發生了錯誤。

如果不捕獲異常,會發生什麼呢

每個Channel都擁有一個與之相關聯的ChannelPipeline,其持有一個ChannelHandler的實例鏈。在默認的情況下,ChannelHandler會把對它的方法的調用轉發給鏈中的下一個Channel-Handler。因此,如果exceptionCaught方法沒有被該鏈中的某處實現,那麼所接收的異常將會被傳遞到ChannelPipeline的尾端並被記錄。為此,你的應用程序應該提供至少有一個實現了exceptionCaught方法的ChannelHandler。(6.4節詳細地討論了異常處理)。

除了ChannelInboundHandlerAdapter之外,還有很多需要學習的ChannelHandler的子類型和實現,我們將在第6章和第7章中對它們進行詳細的闡述。目前,請記住下面這些關鍵點:

  • 針對不同類型的事件來調用ChannelHandler
  • 應用程序通過實現或者擴展ChannelHandler來掛鉤到事件的生命週期,並且提供自定義的應用程序邏輯;
  • 在架構上,ChannelHandler有助於保持業務邏輯與網絡處理代碼的分離。這簡化了開發過程,因為代碼必須不斷地演化以響應不斷變化的需求。

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到子Channel的ChannelPipeline
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception {
                         ch.pipeline.addLast(serverHandler);[5]   ←--  EchoServerHandler被標注為@Shareable,所以我們可以總是使用同樣的實例
                    }
                 });
            ChannelFuture f = b.bind.sync;    ←--   ❻ 異步地綁定服務器;調用sync方法阻塞等待直到綁定完成
            f.channel.closeFuture.sync;  ←--  ❼ 獲取Channel的CloseFuture,並且阻塞當前線程直到它完成
        } 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[6]——當從服務器接收到一條消息時被調用;
  • 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
                .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方法連接到遠程節點;

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

2.5 構建和運行Echo服務器和客戶端

在這一節中,我們將介紹編譯和運行Echo服務器和客戶端所需的所有步驟。

Echo客戶端/服務器的Maven工程

這本書的附錄使用Echo客戶端/服務器工程的配置,詳細地解釋了多模塊Maven工程是如何組織的。這部分內容對於構建和運行該應用程序來說並不是必讀的,之所以推薦閱讀這部分內容,是因為它能幫助你更好地理解本書的示例以及Netty項目本身。

2.5.1 運行構建

要構建Echo客戶端和服務器,請進入到代碼示例根目錄下的chapter2目錄執行以下命令:

mvn clean package  

這將產生非常類似於代碼清單2-5所示的輸出(我們已經編輯忽略了幾個構建過程中的非必要步驟)。

代碼清單2-5 構建Echo客戶端和服務器

[INFO] Scanning for projects...
[INFO] -------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Chapter 2. Your First Netty Application - Echo App
[INFO] Chapter 2. Echo Client
[INFO] Chapter 2. Echo Server
[INFO]
[INFO] -------------------------------------------------------------------
[INFO] Building Chapter 2. Your First Netty Application - 2.0-SNAPSHOT
[INFO] -------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ chapter2 ---
[INFO]
[INFO] -------------------------------------------------------------------
[INFO] Building Chapter 2. Echo Client 2.0-SNAPSHOT
[INFO] -------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.6.1:clean (default-clean)
    @ echo-client ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources)
    @ echo-client ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.3:compile (default-compile)
    @ echo-client ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to
    \netty-in-action\chapter2\Client\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources)
    @ echo-client ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory
    \netty-in-action\chapter2\Client\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile)
    @ echo-client ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.18.1:test (default-test)
    @ echo-client ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ echo-client ---
[INFO] Building jar:
    \netty-in-action\chapter2\Client\target\echo-client-2.0-SNAPSHOT.jar
[INFO]
[INFO] -------------------------------------------------------------------
[INFO] Building Chapter 2. Echo Server 2.0-SNAPSHOT
[INFO] -------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.6.1:clean (default-clean)
    @ echo-server ---
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources)
    @ echo-server ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.3:compile (default-compile)
    @ echo-server ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to
    \netty-in-action\chapter2\Server\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources)
    @ echo-server ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory
    \netty-in-action\chapter2\Server\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile)
    @ echo-server ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.18.1:test (default-test)
    @ echo-server ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ echo-server ---
[INFO] Building jar:
    \netty-in-action\chapter2\Server\target\echo-server-2.0-SNAPSHOT.jar
[INFO] -------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Chapter 2. Your First Netty Application ... SUCCESS [ 0.134 s]
[INFO] Chapter 2. Echo Client .................... SUCCESS [ 1.509 s]
[INFO] Chapter 2. Echo Ser........................ SUCCESS [ 0.139 s]
[INFO] -------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -------------------------------------------------------------------
[INFO] Total time: 1.886 s
[INFO] Finished at: 2015-11-18T17:14:10-05:00
[INFO] Final Memory: 18M/216M
[INFO] -------------------------------------------------------------------  

下面是前面的構建日誌中記錄的主要步驟:

  • Maven確定了構建順序:首先是父pom.xml,然後是各個模塊(子工程);
  • 如果在用戶的本地存儲庫中沒有找到Netty構件,Maven將從公共的Maven存儲庫中下載它們(此處未顯示);
  • 運行了構建生命週期中的cleancompile階段;
  • 最後執行了maven-jar-plugin

Maven Reactor的摘要顯示所有的項目都已經被成功地構建。兩個子工程的目標目錄的文件列表現在應該類似於代碼清單2-6。

代碼清單2-6 構建的構件列表

Directory of nia\chapter2\Client\target
03/16/2015  09:45 PM    <DIR>          classes
03/16/2015  09:45 PM             5,614 echo-client-1.0-SNAPSHOT.jar
03/16/2015  09:45 PM    <DIR>          generated-sources
03/16/2015  09:45 PM    <DIR>          maven-archiver
03/16/2015  09:45 PM    <DIR>          maven-status

Directory of nia\chapter2\Server/target
03/16/2015  09:45 PM    <DIR>          classes
03/16/2015  09:45 PM             5,629 echo-server-1.0-SNAPSHOT.jar
03/16/2015  09:45 PM    <DIR>          generated-sources
03/16/2015  09:45 PM    <DIR>          maven-archiver
03/16/2015  09:45 PM    <DIR>          maven-status  

2.5.2 運行Echo服務器和客戶端

要運行這些應用程序組件,可以直接使用Java命令。但是在POM文件中,已經為你配置好了exec-maven-plugin來做這個(參見附錄以獲取詳細信息)。

並排打開兩個控制台窗口,一個進到chapter2\Server目錄中,另外一個進到chapter2\Client目錄中。

在服務器的控制台中執行這個命令:

mvn exec:java  

應該會看到類似於下面的內容:

[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------------------------------------------------------
[INFO] Building Echo Server 1.0-SNAPSHOT
[INFO] ----------------------------------------------------------------------
[INFO]
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) >
    validate @ echo-server >>>
[INFO]
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) <
    validate @ echo-server <<<
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-server ---
    nia.chapter2.echoserver.EchoServer
    started and listening for connections on /0:0:0:0:0:0:0:0:9999  

服務器現在已經啟動並準備好接受連接。現在在客戶端的控制台中執行同樣的命令:

mvn exec:java  

應該會看到下面的內容:

[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------------------------------------------------
[INFO] Building Echo Client 1.0-SNAPSHOT
[INFO] -------------------------------------------------------------------
[INFO]
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) >
    validate @ echo-client >>>
[INFO]
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) <
    validate @ echo-client <<<
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-client ---
    Client received: Netty rocks!
[INFO] -------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -------------------------------------------------------------------
[INFO] Total time: 2.833 s
[INFO] Finished at: 2015-03-16T22:03:54-04:00
[INFO] Final Memory: 10M/309M
[INFO] -------------------------------------------------------------------  

同時在服務器的控制台中,應該會看到這個:

Server received: Netty rocks!  

每次運行客戶端時,在服務器的控制台中你都能看到這條日誌語句。

下面是發生的事:

(1)一旦客戶端建立連接,它就發送它的消息——Netty rocks!

(2)服務器報告接收到的消息,並將其回送給客戶端;

(3)客戶端報告返回的消息並退出。

你所看到的都是預期的行為,現在讓我們看看故障是如何被處理的。服務器應該還在運行,所以在服務器的控制台中按下Ctrl+C來停止該進程。一旦它停止,就再次使用下面的命令啟動客戶端:

mvn exec:java  

代碼清單2-7展示了你應該會從客戶端的控制台中看到的當它不能連接到服務器時的輸出。

代碼清單2-7 Echo客戶端的異常處理

[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------------------------------------------------
[INFO] Building Echo Client 1.0-SNAPSHOT
[INFO] --------------------------------------------------------------------
[INFO]
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) >
    validate @ echo-client >>>
[INFO]
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) <
    validate @ echo-client <<<
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-client ---
[WARNING]
java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    . . .
    Caused by: java.net.ConnectException: Connection refused:
    no further information: localhost/127.0.0.1:9999
        at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
        at sun.nio.ch.SocketChannelImpl
        .finishConnect(SocketChannelImpl.java:739)
        at io.netty.channel.socket.nio.NioSocketChannel
        .doFinishConnect(NioSocketChannel.java:208)
        at io.netty.channel.nio
        .AbstractNioChannel$AbstractNioUnsafe
        .finishConnect(AbstractNioChannel.java:281)
        at io.netty.channel.nio.NioEventLoop
        .processSelectedKey(NioEventLoop.java:528)
        at io.netty.channel.nio.NioEventLoop.
        processSelectedKeysOptimized(NioEventLoop.java:468)
        at io.netty.channel.nio.NioEventLoop
        .processSelectedKeys(NioEventLoop.java:382)
        at io.netty.channel.nio.NioEventLoop
        .run(NioEventLoop.java:354)
        at io.netty.util.concurrent.SingleThreadEventExecutor$2
        .run(SingleThreadEventExecutor.java:116)
        at io.netty.util.concurrent.DefaultThreadFactory
        $DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
    . . .
[INFO] --------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] --------------------------------------------------------------------
[INFO] Total time: 3.801 s
[INFO] Finished at: 2015-03-16T22:11:16-04:00
[INFO] Final Memory: 10M/309M
[INFO] --------------------------------------------------------------------
[ERROR] Failed to execute goal org.codehaus.mojo:
    exec-maven-plugin:1.2.1:java (default-cli) on project echo-client:
        An exception occured while executing the Java class. null:
        InvocationTargetException: Connection refused:
        no further information: localhost/127.0.0.1:9999 -> [Help 1]  

發生了什麼?客戶端試圖連接服務器,其預期運行在localhost:9999上。但是連接失敗了(和預期的一樣),因為服務器在這之前就已經停止了,所以在客戶端導致了一個java.net.ConnectException。這個異常觸發了EchoClientHandlerexceptionCaught方法,打印出了棧跟蹤並關閉了Channel(見代碼清單2-3)。

2.6 小結

在本章中,你設置好了開發環境,並且構建和運行了你的第一款Netty客戶端和服務器。雖然這只是一個簡單的應用程序,但是它可以伸縮到支持數千個並發連接——每秒可以比普通的基於套接字的Java應用程序處理多得多的消息。

在接下來的幾章中,你將看到更多關於Netty如何簡化可伸縮性和並發性的例子。我們也將更加深入地瞭解Netty對於關注點分離的架構原則的支持。通過提供正確的抽像來解耦業務邏輯和網絡編程邏輯,Netty使得可以很容易地跟上快速演化的需求,而又不危及系統的穩定性。

在下一章中,我們將提供對Netty體系架構的概述。這將為你在後續的章節中對Netty的內部進行深入而全面的學習提供上下文。


[1] Netty的一組受限特性可以運行於JDK 1.6,但是JDK 8或者更高版本則是編譯時必需的,包括運行最新版本的Maven。

[2] 包括Intellij IDEA。——譯者注

[3] 也可以通過HomeBrew或者Scoop來安裝Maven,更加簡單方便。——譯者注

[4] 未決消息(pending message)是指目前暫存於ChannelOutboundBuffer中的消息,在下一次調用flush或者writeAndFlush方法時將會嘗試寫出到套接字。——譯者注——譯者注

[5] 這裡對於所有的客戶端連接來說,都會使用同一個EchoServerHandler,因為其被標注為@Sharable,這將在後面的章節中講到。——譯者注

[6] SimpleChannelInboundHandlerchannelRead0方法的相關討論參見https://github.com/netty/netty/ wiki/New-and-noteworthy-in-5.0#channelread0--messagereceived,其中Netty5的開發工作已經關閉。——譯者注