1. 程式人生 > >《Netty 實戰》Netty In Action中文版 第2章——你的第一款Netty應用程式

《Netty 實戰》Netty In Action中文版 第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);⇽--- 將未決訊息沖刷到遠端節點,並且關閉該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到子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()方法連線到遠端節點;

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

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 tes