Netty4.x用戶指南翻譯

前言

問題

現如今我們使用通用的應用程序或者類庫來實現系統之間地互相訪問。例如,我們經常使用一個HTTP客戶端來從web服務器上獲取信息,或者通過web service來執行一個遠程的調用。然而,有時候一個通用的協議或者它的實現并沒有覆蓋一些場景。比如我們無法使用一個通用的HTTP服務器來處理大文件、電子郵件、近實時消息比如財務信息和多人游戲數據。這就需要一個高度優化的協議實現來滿足特殊的場景。比如,你可以實現一個優化的Ajax的聊天應用、媒體流傳輸或者是大文件傳輸的HTTP服務器。你甚至可以自己設計和實現一個新的協議來為你的需求量身定制。另一個不可避免的情況是,您必須處理遺留的專有協議,以確保與舊系統的互操作性。在這種情況下,重點是我們如何能快速實現該協議,并且不犧牲最終應用程序的穩定性和性能。

解決方案

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

換句話說,Netty是一個NIO框架,它能夠快速方便地開發網絡應用程序,如服務器和客戶端協議。Netty大大簡化了網絡程序的開發,如TCP和UDP的Socket開發。

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

有一些用戶可能已經發現其他的一些網絡框架也聲稱自己有同樣的優勢,所以你可能會問是Netty和它們的不同之處。答案就是Netty的哲學設計理念。Netty從第一天開始就為用戶提供了用戶體驗最好的API以及實現設計。這不是什么實質的東西,但當你閱讀本指南并使用Netty時,你會意識到,這種哲學將使你的生活變得更容易。

入門指南

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

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

開始之前

運行本章所介紹的示例有兩個最低要求:最新版本的Netty和JDK 1.6或以上。最新的Netty版本在項目下載頁面可以找到。為了下載到正確的JDK版本,請到你喜歡的網站下載。

當您閱讀時,您可能會對本章中介紹的類有更多的問題。關于這些類的詳細的信息請請參考API說明文檔。為了方便,所有文檔中涉及到的類名字都會被關聯到一個在線的API說明。當然如果有任何錯誤信息、語法錯誤或者你有任何好的建議來改進文檔說明,那么請聯系Netty社區

寫一個丟棄(DISCARD)服務

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

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

package io.netty.example.discard;

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

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

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // Discard the received data silently.
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}
  1. DisCardServerHandler 繼承自 ChannelInboundHandlerAdapter,這個類實現了 ChannelInboundHandler 接口,ChannelInboundHandler 提供了許多事件處理的接口方法,你可以覆蓋這些方法。現在,僅僅只需要繼承ChannelInboundHandlerAdapter類而不是你自己去實現接口方法。

  2. 這里我們覆蓋了 channelRead() 事件處理方法。當從客戶端接收到新數據時,這個方法就會傳入接收到的消息并被調用。這個例子中,收到的消息的類型是 ByteBuf

  3. 為了實現DISCARD協議,處理器需要忽略所有接受到的消息。 ByteBuf 是一個引用計數(reference-counted)對象,這個對象必須顯示地調用 release() 方法來釋放。請記住,釋放所有傳遞到處理器的引用計數對象,是處理器的職責。通常,channelRead() 方法的實現就像下面的這段代碼:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // Do something with msg
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
    
  4. exceptionCaught() 事件處理方法是當出現Throwable對象才會被調用,即當Netty由于IO錯誤或者處理器在處理事件時拋出的異常時。在大部分情況下,捕獲的異常應該被記錄下來并且把關聯的channel給關閉掉。然而這種方法的實現可能會有所不同,這取決于您想要處理的異常情況。比如,你可能想在關閉連接之前發送一個錯誤碼的響應消息。

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

package io.netty.example.discard;
    
import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
    
/**
 * Discards any incoming data.
 */
public class DiscardServer {
    
    private int port;
    
    public DiscardServer(int port) {
        this.port = port;
    }
    
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new DiscardServerHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
    
            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)
    
            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    
    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
    }
}
  1. NioEventLoopGroup 是用來處理I/O操作的多線程事件循環器,Netty提供了許多不同的 EventLoopGroup 的實現用來處理不同傳輸協議。在這個例子中我們實現了一個服務端的應用,因此會有2個 NioEventLoopGroup 會被使用。第一個通常被稱作‘boss’,用來接收進來的連接。第二個通常被稱作‘worker’,用來處理已經被接收的連接,一旦‘boss’接收到連接,就會把連接信息注冊到‘worker’上。使用多少線程,以及它們如何映射到創建的通道(Channel)上,這取決于 EventLoopGroup 的實現,甚至可以通過構造函數來配置。
  2. ServerBootstrap 是一個用于設置服務器的助手類。你可以直接使用 Channel 設置服務。然而,請注意這會是一個復雜的處理過程,在很多情況下你并不需要這樣做。
  3. 這里,我們指定使用 NioServerSocketChannel 類來舉例說明一個新的 Channel 如何接收進來的連接。
  4. 這里指定的 handler 將始終由新接受的 Channel 進行調用。 ChannelInitializer 是一個特殊的處理類,他的目的是幫助使用者配置一個新的 Channel 。也許你想通過增加一些處理類比如 DiscardServerHandler 來配置這個新的 Channel 所對應的 ChannelPipeline 來實現你的網絡程序。隨著應用程序變得復雜,您可能會在管道中添加更多的處理程序,并最終將這個匿名類提取到頂級類。
  5. 你可以設置這里指定的通道實現的配置參數。我們正在寫一個TCP/IP的服務端,因此我們被允許設置socket的參數選項比如 tcpNoDelaykeepAlive 。請參考 ChannelOption 和詳細的 ChannelConfig 實現的接口文檔以此可以對 ChannelOptions 的有一個大概的認識。
  6. 你注意到 option()childOption() 嗎?option() 是提供給 NioServerSocketChannel 用來接收進來的連接。childOption() 是提供給由父管道 ServerChannel 接收到的連接,在這個例子中就是 NioServerSocketChannel
  7. 我們繼續。剩下的就是綁定端口然后啟動服務。這里我們在機器上綁定了機器所有網卡上的8080端口。當然現在你可以多次調用 bind() 方法(基于不同綁定地址)。

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

觀察接收到的數據

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

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

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

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf in = (ByteBuf) msg;
    try {
        while (in.isReadable()) { // (1)
            System.out.print((char) in.readByte());
            System.out.flush();
        }
    } finally {
        ReferenceCountUtil.release(msg); // (2)
    }
}
  1. 這個低效的循環事實上可以簡化為:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
  2. 或者,你可以在這里調用 in.release()

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

ECHO服務

到目前為止,我們雖然接收到了數據,但沒有做任何的響應。然而一個服務端通常會對一個請求作出響應。讓我們學習如何通過實現 ECHO 協議來為客戶端編寫響應消息,其中任何接收到的數據都會被發送回去。

我們在前幾節中實現的丟棄服務器的唯一區別是,它將接收到的數據發送回來,而不是將接收到的數據打印到控制臺。因此,可以再次修改channelRead()方法:

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

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

寫一個時間服務器

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

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

package io.netty.example.time;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) { // (1)
        final ByteBuf time = ctx.alloc().buffer(4); // (2)
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
        
        final ChannelFuture f = ctx.writeAndFlush(time); // (3)
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        }); // (4)
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  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里所有的操作都是異步的。舉個例子下面的代碼中在消息被發送之前可能會先關閉連接。

    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();
    

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

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

    f.addListener(ChannelFutureListener.CLOSE);
    

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

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

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

寫一個Time客戶端

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

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

package io.netty.example.time;

public class TimeClient {
    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            Bootstrap b = new Bootstrap(); // (1)
            b.group(workerGroup); // (2)
            b.channel(NioSocketChannel.class); // (3)
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            
            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
  1. BootstrapServerBootstrap 類似,不過他是對非服務端的channel而言,比如客戶端或者無連接傳輸模式(connectionless)的channel。
  2. 如果你只指定了一個 EventLoopGroup,那它就會同時作為一個‘boss’和‘workder’線程。盡管客戶端不需要使用到‘boss’線程。
  3. 使用 NioSocketChannel 創建客戶端的 Channel,而不是 NioServerSocketChannel
  4. 注意,這里我們不使用 childOption() ,不像 ServerBootstrap 那樣,因為客戶端的SocketChannel 沒有父channel。
  5. 我們用 connect() 方法代替了 bind() 方法。

正如你看到的,它和服務端的代碼并沒有太大的區別。 ChannelHandler 是如何實現的?他應該從服務端接受一個32位的整數消息,把他翻譯成人們能讀懂的格式,并打印翻譯好的時間,然后關閉連接:

package io.netty.example.time;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg; // (1)
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. 在TCP/IP中,NETTY會把讀到的數據放到ByteBuf 中。

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

處理基于流的傳輸

一個小的Socket Buffer問題

在基于流的傳輸里,比如TCP/IP,接收到的數據會先被存儲到一個socket接收緩沖里。不幸的是,基于流的傳輸并不是一個數據包隊列,而是一個字節隊列。這意味這,即使您將兩個消息發送為兩個獨立的包,操作系統不會將它們視為兩個消息,但只是一堆字節。因此,無法保證你所讀的內容與你的遠程節點所寫的完全相同。舉個例子,讓我們假設操作系統的TCP/TP協議棧已經接收了3個數據包:

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

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

第一個解決方案

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

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

package io.netty.example.time;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    private ByteBuf buf;
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        buf = ctx.alloc().buffer(4); // (1)
    }
    
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        buf.release(); // (1)
        buf = null;
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        buf.writeBytes(m); // (2)
        m.release();
        
        if (buf.readableBytes() >= 4) { // (3)
            long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. ChannelHandler 有2個生命周期的監聽方法:handlerAdded()handlerRemoved()。你可以完成任意初始化任務只要他不會被阻塞很長的時間。
  2. 首先,所有接收的數據都應該被累積在 buf 變量里。
  3. 然后,處理器必須檢查buf變量是否有足夠的數據,在這個例子中是4個字節,然后處理實際的業務邏輯。否則,Netty會重復調用channelRead()當有更多數據到達直到4個字節的數據被積累。

第二個解決方案

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

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

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

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

package io.netty.example.time;

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

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

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

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

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

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

用POJO代替ByteBuf

我們已經討論了所有的例子,到目前為止一個消息的消息都是使用 ByteBuf 作為一個基本的數據結構。在這一部分,我們會改進 TIME 協議的客戶端和服務端的例子,用POJO替代 ByteBuf

在你的 ChannelHandler 中使用POJO優勢是比較明顯的。通過從ByteBuf提取信息的方式,分離handler的代碼,將會使你的handler變得更加可維護和可重用。在 TIME 客戶端和服務端的例子中,我們讀取的僅僅是一個32位的整形數據,直接使用ByteBuf不會是一個主要的問題。然后,你會發現當你需要實現一個真實的協議,分離代碼變得非常的必要。

首先,讓我們定義一個新的類型叫做 UnixTime

package io.netty.example.time;

import java.util.Date;

public class UnixTime {

    private final long value;
    
    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }
    
    public UnixTime(long value) {
        this.value = value;
    }
        
    public long value() {
        return value;
    }
        
    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}

現在我們可以修改下 TimeDecoder 類,返回一個 UnixTime 而不是 ByteBuf

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    if (in.readableBytes() < 4) {
        return;
    }

    out.add(new UnixTime(in.readUnsignedInt()));
}

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

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    UnixTime m = (UnixTime) msg;
    System.out.println(m);
    ctx.close();
}

更加簡單和優雅了,是吧?相同的技術可以被運用到服務端。讓我們更改一下一開始的 TimeServerHandler

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

現在,唯一缺失的部分是編碼器,解碼器是 ChannelOutboundHandler 的實現,能把 UnixTime 對象重新轉化為一個 ByteBuf。這比編寫解碼器要簡單得多,因為在編碼消息時,不需要處理包的分段和組裝。

package io.netty.example.time;

public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)m.value());
        ctx.write(encoded, promise); // (1)
    }
}
  1. 在這幾行代碼里還有幾個重要的事情。
    第一, 通過 ChannelPromise ,當編碼后的數據被寫到了通道上Netty可以通過這個對象標記是成功還是失敗。
    第二, 我們不需要調用 cxt.flush() 。因為處理器已經單獨分離出了一個方法void flush(ChannelHandlerContext cxt) ,用于覆蓋 flush() 操作。

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

public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}

最后的任務就是在TimeServerHandler之前把TimeEncoder插入到 ChannelPipeline ,剩下的只是簡單的練習。

關閉你的應用

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

總結

在本章中,我們快速瀏覽了Netty,并演示了如何在Netty上編寫一個完整的工作網絡應用程序。在Netty接下去的章節中還會有更多更相信的信息。我們也鼓勵你去重新復習下在 io.netty.example 包下的例子。

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

推薦閱讀更多精彩內容