Netty 5用戶指南

原文地址http://netty.io/wiki/user-guide-for-5.x.html    譯者:光輝勇士      校對:郭蕾

前言

問題

現如今我們使用通用的應用程序或者類庫來實現系統之間地互相訪問,比如我們經常使用一個HTTP客戶端來從web服務器上獲取信息,或者通過web service來執行一個遠程的調用。

然而,有時候一個通用的協議和他的實現並沒有覆蓋一些場景。比如我們無法使用一個通用的HTTP服務器來處理大文件、電子郵件、近實時消息比如財務信息和多人遊戲數據。我們需要一個合適的協議來處理一些特殊的場景。例如你可以實現一個優化的Ajax的聊天應用、媒體流傳輸或者是大文件傳輸的HTTP服務器,你甚至可以自己設計和實現一個新的協議來準確地實現你的需求。

另外不可避免的事情是你不得不處理這些私有協議來確保和原有系統的互通。這個例子將會展示如何快速實現一個不影響應用程序穩定性和性能的協議。

解決方案

Netty是一個提供異步事件驅動的網絡應用框架,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

換句話說,Netty是一個NIO框架,使用它可以簡單快速地開發網絡應用程序,比如客戶端和服務端的協議。Netty大大簡化了網絡程序的開發過程比如TCP和UDP的 Socket的開發。

“快速和簡單”並不意味着應用程序會有難維護和性能低的問題,Netty是一個精心設計的框架,它從許多協議的實現中吸收了很多的經驗比如FTP、SMTP、HTTP、許多二進制和基於文本的傳統協議,Netty在不降低開發效率、性能、穩定性、靈活性情況下,成功地找到了解決方案。

有一些用戶可能已經發現其他的一些網絡框架也聲稱自己有同樣的優勢,所以你可能會問是Netty和它們的不同之處。答案就是Netty的哲學設計理念。Netty從第一天開始就爲用戶提供了用戶體驗最好的API以及實現設計。正是因爲Netty的設計理念,才讓我們得以輕鬆地閱讀本指南並使用Netty。

入門指南

這個章節會介紹Netty核心的結構,並通過一些簡單的例子來幫助你快速入門。當你讀完本章節你馬上就可以用Netty寫出一個客戶端和服務端。

如果你在學習的時候喜歡“自頂向下(top-down)”的方法,那你可能需要要從第二章《架構概述》開始,然後再回到這裏。

開始之前

運行本章節中的兩個例子最低要求是:Netty的最新版本(Netty5)和JDK1.6及以上。最新的Netty版本在項目下載頁面可以找到。爲了下載到正確的JDK版本,請到你喜歡的網站下載。

閱讀本章節過程中,你可能會對相關類有疑惑,關於這些類的詳細的信息請請參考API說明文檔。爲了方便,所有文檔中涉及到的類名字都會被關聯到一個在線的API說明。當然如果有任何錯誤信息、語法錯誤或者你有任何好的建議來改進文檔說明,那麼請聯繫Netty社區

DISCARD服務(丟棄服務,指的是會忽略所有接收的數據的一種協議)

世界上最簡單的協議不是”Hello,World!”,是DISCARD,他是一種丟棄了所有接受到的數據,並不做有任何的響應的協議。

爲了實現DISCARD協議,你唯一需要做的就是忽略所有收到的數據。讓我們從處理器的實現開始,處理器是由Netty生成用來處理I/O事件的。

01package io.netty.example.discard;
02 
03import io.netty.buffer.ByteBuf;
04 
05import io.netty.channel.ChannelHandlerContext;
06import io.netty.channel.ChannelHandlerAdapter;
07 
08/**
09 * Handles a server-side channel.
10 */
11public class DiscardServerHandler extends ChannelHandlerAdapter { // (1)
12 
13    @Override
14    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
15        // Discard the received data silently.
16        ((ByteBuf) msg).release(); // (3)
17    }
18 
19    @Override
20    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {// (4)
21        // Close the connection when an exception is raised.
22        cause.printStackTrace();
23        ctx.close();
24    }
25}
  1. DisCardServerHandler 繼承自 ChannelHandlerAdapter,這個類實現了ChannelHandler接口,ChannelHandler提供了許多事件處理的接口方法,然後你可以覆蓋這些方法。現在僅僅只需要繼承ChannelHandlerAdapter類而不是你自己去實現接口方法。
  2. 這裏我們覆蓋了chanelRead()事件處理方法。每當從客戶端收到新的數據時,這個方法會在收到消息時被調用,這個例子中,收到的消息的類型是ByteBuf
  3. 爲了實現DISCARD協議,處理器不得不忽略所有接受到的消息。ByteBuf是一個引用計數對象,這個對象必須顯示地調用release()方法來釋放。請記住處理器的職責是釋放所有傳遞到處理器的引用計數對象。通常,channelRead()方法的實現就像下面的這段代碼:
    1@Override
    2public void channelRead(ChannelHandlerContext ctx, Object msg) {
    3    try {
    4        // Do something with msg
    5    finally {
    6        ReferenceCountUtil.release(msg);
    7    }
    8}
  4. exceptionCaught()事件處理方法是當出現Throwable對象纔會被調用,即當Netty由於IO錯誤或者處理器在處理事件時拋出的異常時。在大部分情況下,捕獲的異常應該被記錄下來並且把關聯的channel給關閉掉。然而這個方法的處理方式會在遇到不同異常的情況下有不同的實現,比如你可能想在關閉連接之前發送一個錯誤碼的響應消息。

到目前爲止一切都還比較順利,我們已經實現了DISCARD服務的一半功能,剩下的需要編寫一個main()方法來啓動服務端的DiscardServerHandler。

01package io.netty.example.discard;
02 
03import io.netty.bootstrap.ServerBootstrap;
04 
05import io.netty.channel.ChannelFuture;
06import io.netty.channel.ChannelInitializer;
07import io.netty.channel.ChannelOption;
08import io.netty.channel.EventLoopGroup;
09import io.netty.channel.nio.NioEventLoopGroup;
10import io.netty.channel.socket.SocketChannel;
11import io.netty.channel.socket.nio.NioServerSocketChannel;
12 
13/**
14 * Discards any incoming data.
15 */
16public class DiscardServer {
17 
18    private int port;
19 
20    public DiscardServer(int port) {
21        this.port = port;
22    }
23 
24    public void run() throws Exception {
25        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
26        EventLoopGroup workerGroup = new NioEventLoopGroup();
27        try {
28            ServerBootstrap b = new ServerBootstrap(); // (2)
29            b.group(bossGroup, workerGroup)
30             .channel(NioServerSocketChannel.class// (3)
31             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
32                 @Override
33                 public void initChannel(SocketChannel ch) throws Exception {
34                     ch.pipeline().addLast(new DiscardServerHandler());
35                 }
36             })
37             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
38             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
39 
40            // Bind and start to accept incoming connections.
41            ChannelFuture f = b.bind(port).sync(); // (7)
42 
43            // Wait until the server socket is closed.
44            // In this example, this does not happen, but you can do that to gracefully
45            // shut down your server.
46            f.channel().closeFuture().sync();
47        finally {
48            workerGroup.shutdownGracefully();
49            bossGroup.shutdownGracefully();
50        }
51    }
52 
53    public static void main(String[] args) throws Exception {
54        int port;
55        if (args.length > 0) {
56            port = Integer.parseInt(args[0]);
57        else {
58            port = 8080;
59        }
60        new DiscardServer(port).run();
61    }
62}
  1. NioEventLoopGroup 是用來處理I/O操作的多線程事件循環器,Netty提供了許多不同的EventLoopGroup的實現用來處理不同傳輸協議。在這個例子中我們實現了一個服務端的應用,因此會有2個NioEventLoopGroup會被使用。第一個經常被叫做‘boss’,用來接收進來的連接。第二個經常被叫做‘worker’,用來處理已經被接收的連接,一旦‘boss’接收到連接,就會把連接信息註冊到‘worker’上。如何知道多少個線程已經被使用,如何映射到已經創建的Channels上都需要依賴於EventLoopGroup的實現,並且可以通過構造函數來配置他們的關係。
  2. ServerBootstrap 是一個啓動NIO服務的輔助啓動類。你可以在這個服務中直接使用Channel,但是這會是一個複雜的處理過程,在很多情況下你並不需要這樣做。
  3. 這裏我們指定使用NioServerSocketChannel類來舉例說明一個新的Channel如何接收進來的連接。
  4. 這裏的事件處理類經常會被用來處理一個最近的已經接收的ChannelChannelInitializer是一個特殊的處理類,他的目的是幫助使用者配置一個新的Channel。也許你想通過增加一些處理類比如DiscardServerHandle來配置一個新的Channel或者其對應的ChannelPipeline來實現你的網絡程序。當你的程序變的複雜時,可能你會增加更多的處理類到pipline上,然後提取這些匿名類到最頂層的類上。
  5. 你可以設置這裏指定的通道實現的配置參數。我們正在寫一個TCP/IP的服務端,因此我們被允許設置socket的參數選項比如tcpNoDelay和keepAlive。請參考ChannelOption和詳細的ChannelConfig實現的接口文檔以此可以對ChannelOptions的有一個大概的認識。
  6. 你關注過option()和childOption()嗎?option()是提供給NioServerSocketChannel用來接收進來的連接。childOption()是提供給由父管道ServerChannel接收到的連接,在這個例子中也是NioServerSocketChannel
  7. 我們繼續,剩下的就是綁定端口然後啓動服務。這裏我們在機器上綁定了機器所有網卡上的8080端口。當然現在你可以多次調用bind()方法(基於不同綁定地址)。

恭喜!你已經完成熟練地完成了第一個基於Netty的服務端程序。

觀察接收到的數據

現在我們已經編寫出我們第一個服務端,我們需要測試一下他是否真的可以運行。最簡單的測試方法是用telnet 命令。例如,你可以在命令行上輸入telnet localhost 8080或者其他類型參數。

然而我們能說這個服務端是正常運行了嗎?事實上我們也不知道因爲他是一個discard服務,你根本不可能得到任何的響應。爲了證明他仍然是在工作的,讓我們修改服務端的程序來打印出他到底接收到了什麼。

我們已經知道channelRead()方法是在數據被接收的時候調用。讓我們放一些代碼到DiscardServerHandler類的channelRead()方法。

01@Override
02public void channelRead(ChannelHandlerContext ctx, Object msg) {
03    ByteBuf in = (ByteBuf) msg;
04    try {
05        while (in.isReadable()) { // (1)
06            System.out.print((char) in.readByte());
07            System.out.flush();
08        }
09    finally {
10        ReferenceCountUtil.release(msg); // (2)
11    }
12}
  1. 這個低效的循環事實上可以簡化爲:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
  2. 或者,你可以在這裏調用in.release()。

如果你再次運行telnet命令,你將會看到服務端打印出了他所接收到的消息。
完整的discard server代碼放在了io.netty.example.discard包下面。

ECHO服務(響應式協議)

到目前爲止,我們雖然接收到了數據,但沒有做任何的響應。然而一個服務端通常會對一個請求作出響應。讓我們學習怎樣在ECHO協議的實現下編寫一個響應消息給客戶端,這個協議針對任何接收的數據都會返回一個響應。

和discard server唯一不同的是把在此之前我們實現的channelRead()方法,返回所有的數據替代打印接收數據到控制檯上的邏輯。因此,需要把channelRead()方法修改如下:

1@Override
2public void channelRead(ChannelHandlerContext ctx, Object msg) {
3    ctx.write(msg); // (1)
4    ctx.flush(); // (2)
5}

1. ChannelHandlerContext對象提供了許多操作,使你能夠觸發各種各樣的I/O事件和操作。這裏我們調用了write(Object)方法來逐字地把接受到的消息寫入。請注意不同於DISCARD的例子我們並沒有釋放接受到的消息,這是因爲當寫入的時候Netty已經幫我們釋放了。
2. ctx.write(Object)方法不會使消息寫入到通道上,他被緩衝在了內部,你需要調用ctx.flush()方法來把緩衝區中數據強行輸出。或者你可以用更簡潔的cxt.writeAndFlush(msg)以達到同樣的目的。

如果你再一次運行telnet命令,你會看到服務端會發回一個你已經發送的消息。
完整的echo服務的代碼放在了io.netty.example.echo包下面。

TIME服務(時間協議的服務)

在這個部分被實現的協議是TIME協議。和之前的例子不同的是在不接受任何請求時他會發送一個含32位的整數的消息,並且一旦消息發送就會立即關閉連接。在這個例子中,你會學習到如何構建和發送一個消息,然後在完成時主動關閉連接。

因爲我們將會忽略任何接收到的數據,而只是在連接被創建發送一個消息,所以這次我們不能使用channelRead()方法了,代替他的是,我們需要覆蓋channelActive()方法,下面的就是實現的內容:

01package io.netty.example.time;
02 
03public class TimeServerHandler extends ChannelHandlerAdapter {
04 
05    @Override
06    public void channelActive(final ChannelHandlerContext ctx) { // (1)
07        final ByteBuf time = ctx.alloc().buffer(4); // (2)
08        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
09 
10        final ChannelFuture f = ctx.writeAndFlush(time); // (3)
11        f.addListener(new ChannelFutureListener() {
12            @Override
13            public void operationComplete(ChannelFuture future) {
14                assert f == future;
15                ctx.close();
16            }
17        }); // (4)
18    }
19 
20    @Override
21    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
22        cause.printStackTrace();
23        ctx.close();
24    }
25}
  1. channelActive()方法將會在連接被建立並且準備進行通信時被調用。因此讓我們在這個方法裏完成一個代表當前時間的32位整數消息的構建工作。
  2. 爲了發送一個新的消息,我們需要分配一個包含這個消息的新的緩衝。因爲我們需要寫入一個32位的整數,因此我們需要一個至少有4個字節的ByteBuf。通過ChannelHandlerContext.alloc()得到一個當前的ByteBufAllocator,然後分配一個新的緩衝。
  3. 和往常一樣我們需要編寫一個構建好的消息。但是等一等,flip在哪?難道我們使用NIO發送消息時不是調用java.nio.ByteBuffer.flip()嗎?ByteBuf之所以沒有這個方法因爲有兩個指針,一個對應讀操作一個對應寫操作。當你向ByteBuf裏寫入數據的時候寫指針的索引就會增加,同時讀指針的索引沒有變化。讀指針索引和寫指針索引分別代表了消息的開始和結束。比較起來,NIO緩衝並沒有提供一種簡潔的方式來計算出消息內容的開始和結尾,除非你調用flip方法。當你忘記調用flip方法而引起沒有數據或者錯誤數據被髮送時,你會陷入困境。這樣的一個錯誤不會發生在Netty上,因爲我們對於不同的操作類型有不同的指針。你會發現這樣的使用方法會讓你過程變得更加的容易,因爲你已經習慣一種沒有使用flip的方式。另外一個點需要注意的是ChannelHandlerContext.write()(和writeAndFlush())方法會返回一個ChannelFuture對象,一個ChannelFuture代表了一個還沒有發生的I/O操作。這意味着任何一個請求操作都不會馬上被執行,因爲在Netty裏所有的操作都是異步的。舉個例子下面的代碼中在消息被髮送之前可能會先關閉連接。
    1Channel ch = ...;
    2ch.writeAndFlush(message);
    3ch.close();

    因此你需要在write()方法返回的ChannelFuture完成後調用close()方法,然後當他的寫操作已經完成他會通知他的監聽者。請注意,close()方法也可能不會立馬關閉,他也會返回一個ChannelFuture

  4. 當一個寫請求已經完成是如何通知到我們?這個只需要簡單地在返回的ChannelFuture上增加一個ChannelFutureListener。這裏我們構建了一個匿名的ChannelFutureListener類用來在操作完成時關閉Channel。或者,你可以使用簡單的預定義監聽器代碼:
    1f.addListener(ChannelFutureListener.CLOSE);

爲了測試我們的time服務如我們期望的一樣工作,你可以使用UNIX的rdate命令

1$ rdate -o <port> -p <host>

Port是你在main()函數中指定的端口,host使用locahost就可以了。

Time客戶端

不像DISCARD和ECHO的服務端,對於TIME協議我們需要一個客戶端因爲人們不能把一個32位的二進制數據翻譯成一個日期或者日曆。在這一部分,我們將會討論如何確保服務端是正常工作的,並且學習怎樣用Netty編寫一個客戶端。

在Netty中,編寫服務端和客戶端最大的並且唯一不同的使用了不同的BootStrapChannel的實現。請看一下下面的代碼:

01package io.netty.example.time;
02 
03public class TimeClient {
04    public static void main(String[] args) throws Exception {
05        String host = args[0];
06        int port = Integer.parseInt(args[1]);
07        EventLoopGroup workerGroup = new NioEventLoopGroup();
08 
09        try {
10            Bootstrap b = new Bootstrap(); // (1)
11            b.group(workerGroup); // (2)
12            b.channel(NioSocketChannel.class); // (3)
13            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
14            b.handler(new ChannelInitializer&lt;SocketChannel&gt;() {
15                @Override
16                public void initChannel(SocketChannel ch) throws Exception {
17                    ch.pipeline().addLast(new TimeClientHandler());
18                }
19            });
20 
21            // Start the client.
22            ChannelFuture f = b.connect(host, port).sync(); // (5)
23 
24            // Wait until the connection is closed.
25            f.channel().closeFuture().sync();
26        finally {
27            workerGroup.shutdownGracefully();
28        }
29    }
30}
  1. BootStrapServerBootstrap類似,不過他是對非服務端的channel而言,比如客戶端或者無連接傳輸模式的channel。
  2. 如果你只指定了一個EventLoopGroup,那他就會即作爲一個‘boss’線程,也會作爲一個‘workder’線程,儘管客戶端不需要使用到‘boss’線程。
  3. 代替NioServerSocketChannel的是NioSocketChannel,這個類在客戶端channel被創建時使用。
  4. 不像在使用ServerBootstrap時需要用childOption()方法,因爲客戶端的SocketChannel沒有父channel的概念。
  5. 我們用connect()方法代替了bind()方法。

正如你看到的,他和服務端的代碼是不一樣的。ChannelHandler是如何實現的?他應該從服務端接受一個32位的整數消息,把他翻譯成人們能讀懂的格式,並打印翻譯好的時間,最後關閉連接:

01package io.netty.example.time;
02 
03import java.util.Date;
04 
05public class TimeClientHandler extends ChannelHandlerAdapter {
06    @Override
07    public void channelRead(ChannelHandlerContext ctx, Object msg) {
08        ByteBuf m = (ByteBuf) msg; // (1)
09        try {
10            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
11            System.out.println(new Date(currentTimeMillis));
12            ctx.close();
13        finally {
14            m.release();
15        }
16    }
17 
18    @Override
19    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
20        cause.printStackTrace();
21        ctx.close();
22    }
23}
  1. 在TCP/IP中,NETTY會把讀到的數據放到ByteBuf的數據結構中。

這樣看起來非常簡單,並且和服務端的那個例子的代碼也相差不多。然而,處理器有時候會因爲拋出IndexOutOfBoundsException而拒絕工作。在下個部分我們會討論爲什麼會發生這種情況。

流數據的傳輸處理

一個小的Socket Buffer問題

在基於流的傳輸裏比如TCP/IP,接收到的數據會先被存儲到一個socket接收緩衝裏。不幸的是,基於流的傳輸並不是一個數據包隊列,而是一個字節隊列。即使你發送了2個獨立的數據包,操作系統也不會作爲2個消息處理而僅僅是作爲一連串的字節而言。因此這是不能保證你遠程寫入的數據就會準確地讀取。舉個例子,讓我們假設操作系統的TCP/TP協議棧已經接收了3個數據包:

netty5_1.png

由於基於流傳輸的協議的這種普通的性質,在你的應用程序裏讀取數據的時候會有很高的可能性被分成下面的片段。

netty5_2.png

因此,一個接收方不管他是客戶端還是服務端,都應該把接收到的數據整理成一個或者多個更有意思並且能夠讓程序的業務邏輯更好理解的數據。在上面的例子中,接收到的數據應該被構造成下面的格式:

netty5_3.png

第一個解決方案

現在讓我們回到TIME客戶端的例子上。這裏我們遇到了同樣的問題,一個32字節數據是非常小的數據量,他並不見得會被經常拆分到到不同的數據段內。然而,問題是他確實可能會被拆分到不同的數據段內,並且拆分的可能性會隨着通信量的增加而增加。

最簡單的方案是構造一個內部的可積累的緩衝,直到4個字節全部接收到了內部緩衝。下面的代碼修改了TimeClientHandler的實現類修復了這個問題

01package io.netty.example.time;
02 
03import java.util.Date;
04 
05public class TimeClientHandler extends ChannelHandlerAdapter {
06    private ByteBuf buf;
07 
08    @Override
09    public void handlerAdded(ChannelHandlerContext ctx) {
10        buf = ctx.alloc().buffer(4); // (1)
11    }
12 
13    @Override
14    public void handlerRemoved(ChannelHandlerContext ctx) {
15        buf.release(); // (1)
16        buf = null;
17    }
18 
19    @Override
20    public void channelRead(ChannelHandlerContext ctx, Object msg) {
21        ByteBuf m = (ByteBuf) msg;
22        buf.writeBytes(m); // (2)
23        m.release();
24 
25        if (buf.readableBytes() &gt;= 4) { // (3)
26            long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L;
27            System.out.println(new Date(currentTimeMillis));
28            ctx.close();
29        }
30    }
31 
32    @Override
33    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
34        cause.printStackTrace();
35        ctx.close();
36    }
37}
  1. ChannelHandler有2個生命週期的監聽方法:handlerAdded()和handlerRemoved()。你可以完成任意初始化任務只要他不會被阻塞很長的時間。
  2. 首先,所有接收的數據都應該被累積在buf變量裏。
  3. 然後,處理器必須檢查buf變量是否有足夠的數據,在這個例子中是4個字節,然後處理實際的業務邏輯。否則,Netty會重複調用channelRead()當有更多數據到達直到4個字節的數據被積累。

第二個解決方案

儘管第一個解決方案已經解決了Time客戶端的問題了,但是修改後的處理器看起來不那麼的簡潔,想象一下如果由多個字段比如可變長度的字段組成的更爲複雜的協議時,你的ChannelHandler的實現將很快地變得難以維護。

正如你所知的,你可以增加多個ChannelHandlerChannelPipeline ,因此你可以把一整個ChannelHandler拆分成多個模塊以減少應用的複雜程度,比如你可以把TimeClientHandler拆分成2個處理器:

  • TimeDecoder處理數據拆分的問題
  • TimeClientHandler原始版本的實現

幸運地是,Netty提供了一個可擴展的類,幫你完成TimeDecoder的開發。

01package io.netty.example.time;
02 
03public class TimeDecoder extends ByteToMessageDecoder { // (1)
04    @Override
05    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List&lt;Object&gt; out) { // (2)
06        if (in.readableBytes() &lt; 4) {
07            return// (3)
08        }
09 
10        out.add(in.readBytes(4)); // (4)
11    }
12}
  1. ByteToMessageDecoderChannelHandler的一個實現類,他可以在處理數據拆分的問題上變得很簡單。
  2. 每當有新數據接收的時候,ByteToMessageDecoder都會調用decode()方法來處理內部的那個累積緩衝。
  3. Decode()方法可以決定當累積緩衝裏沒有足夠數據時可以往out對象裏放任意數據。當有更多的數據被接收了ByteToMessageDecoder會再一次調用decode()方法。
  4. 如果在decode()方法裏增加了一個對象到out對象裏,這意味着解碼器解碼消息成功。ByteToMessageDecoder將會丟棄在累積緩衝裏已經被讀過的數據。請記得你不需要對多條消息調用decode(),ByteToMessageDecoder會持續調用decode()直到不放任何數據到out裏。

現在我們有另外一個處理器插入到ChannelPipeline裏,我們應該在TimeClient裏修改ChannelInitializer 的實現:

1b.handler(new ChannelInitializer&lt;SocketChannel&gt;() {
2    @Override
3    public void initChannel(SocketChannel ch) throws Exception {
4        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
5    }
6});

如果你是一個大膽的人,你可能會嘗試使用更簡單的解碼類ReplayingDecoder。不過你還是需要參考一下API文檔來獲取更多的信息。

1public class TimeDecoder extends ReplayingDecoder {
2@Override
3protected void decode(
4ChannelHandlerContext ctx, ByteBuf in, List<object width="300"height="150">out) {out.add(in.readBytes(4));}}

此外,Netty還提供了更多可以直接拿來用的解碼器使你可以更簡單地實現更多的協議,幫助你避免開發一個難以維護的處理器實現。請參考下面的包以獲取更多更詳細的例子:

用POJO代替ByteBuf

我們已經討論了所有的例子,到目前爲止一個消息的消息都是使用ByteBuf作爲一個基本的數據結構。在這一部分,我們會改進TIME協議的客戶端和服務端的例子,用POJO替代ByteBuf。在你的ChannelHandlerS中使用POJO優勢是比較明顯的。通過從ChannelHandler中提取出ByteBuf的代碼,將會使ChannelHandler的實現變得更加可維護和可重用。在TIME客戶端和服務端的例子中,我們讀取的僅僅是一個32位的整形數據,直接使用ByteBuf不會是一個主要的問題。然後,你會發現當你需要實現一個真實的協議,分離代碼變得非常的必要。首先,讓我們定義一個新的類型叫做UnixTime。

01package io.netty.example.time;
02 
03import java.util.Date;
04 
05public class UnixTime {
06 
07    private final int value;
08 
09    public UnixTime() {
10        this((int) (System.currentTimeMillis() / 1000L + 2208988800L));
11    }
12 
13    public UnixTime(int value) {
14        this.value = value;
15    }
16 
17    public int value() {
18        return value;
19    }
20 
21    @Override
22    public String toString() {
23        return new Date((value() - 2208988800L) * 1000L).toString();
24    }
25}

現在我們可以修改下TimeDecoder類,返回一個UnixTime,以替代ByteBuf

1@Override
2protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
3    if (in.readableBytes() < 4) {
4        return;
5    }
6 
7    out.add(new UnixTime(in.readInt()));
8}

下面是修改後的解碼器,TimeClientHandler不再有任何的ByteBuf代碼了。

1@Override
2public void channelRead(ChannelHandlerContext ctx, Object msg) {
3    UnixTime m = (UnixTime) msg;
4    System.out.println(m);
5    ctx.close();
6}

是不是變得更加簡單和優雅了?相同的技術可以被運用到服務端。讓我們修改一下TimeServerHandler的代碼。

1@Override
2public void channelActive(ChannelHandlerContext ctx) {
3    ChannelFuture f = ctx.writeAndFlush(new UnixTime());
4    f.addListener(ChannelFutureListener.CLOSE);
5}

現在,僅僅需要修改的是ChannelHandler的實現,這裏需要把UnixTime對象重新轉化爲一個ByteBuf。不過這已經是非常簡單了,因爲當你對一個消息編碼的時候,你不需要再處理拆包和組裝的過程。

01package io.netty.example.time;
02 
03public class TimeEncoder extends ChannelHandlerAdapter {
04    @Override
05    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
06        UnixTime m = (UnixTime) msg;
07        ByteBuf encoded = ctx.alloc().buffer(4);
08        encoded.writeInt(m.value());
09        ctx.write(encoded, promise); // (1)
10    }
11}
  1. 在這幾行代碼裏還有幾個重要的事情。第一, 通過ChannelPromise,當編碼後的數據被寫到了通道上Netty可以通過這個對象標記是成功還是失敗。第二, 我們不需要調用cxt.flush()。因爲處理器已經單獨分離出了一個方法void flush(ChannelHandlerContext cxt),如果像自己實現flush方法內容可以自行覆蓋這個方法。

進一步簡化操作,你可以使用MessageToByteEncode:

1public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
2    @Override
3    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
4        out.writeInt(msg.value());
5    }
6}

最後的任務就是在TimeServerHandler之前把TimeEncoder插入到ChannelPipeline。但這是不那麼重要的工作。

關閉你的應用

關閉一個Netty應用往往只需要簡單地通過shutdownGracefully()方法來關閉你構建的所有的NioEventLoopGroupS.當EventLoopGroup被完全地終止,並且對應的所有channels都已經被關閉時,Netty會返回一個Future對象。

概述

在這一章節中,我們會快速地回顧下如果在熟練掌握Netty的情況下編寫出一個健壯能運行的網絡應用程序。在Netty接下去的章節中還會有更多更相信的信息。我們也鼓勵你去重新複習下在io.netty.example包下的例子。請注意社區一直在等待你的問題和想法以幫助Netty的持續改進,Netty的文檔也是基於你們的快速反饋上。

原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com本文鏈接地址: Netty 5用戶指南

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