Netty使用者手冊簡單翻譯

王金龍發表於2017-12-06

前言

問題

如今我們使用一般目的的應用和類庫來相互交流。例如,我們經常使用HTTP客戶端庫來從Web伺服器端獲取資訊和通過RPC的 方式來呼叫WebService。然而,一般目的的協議或實現並不能很好地伸縮。就像我們不會使用常規的HTTP協議來交換大檔案,電子郵件以及實時的訊息如金融資訊或多使用者的遊戲資料。這些都是針對特殊目的實現的高度優化的協議。例如,你可能會針對基於ajax的聊天應用,媒體流以及大檔案傳輸場景使用優化的協議。甚至你會根據你自己的需要設計並實現一個全新的協議。另外一種不可避免的情況是你必須處理遺留下來的與老系統進行互動的協議。那種情況下需要關心的是在不犧牲最終應用穩定性和效能的情況如何快速地實現那個協議。

解決方案

Netty專案是在盡最大努力提供一個非同步的,基於事件驅動的網路應用框架以及為可維護的,高效能的,高擴充套件性的協議伺服器和客戶端的快速部署提供工具支援。

換句話說,Netty是一個NIO的客戶端和服務端框架,通過它能使我們快速簡單地開發網路應用(如協議服務端與客戶端)。它極大地簡化和流水線化了網路程式設計,如TCP和UDP網路伺服器的開發。

簡單快速並不意味著會導致應用程式出現可維護性與效能問題。Netty在設計時就考慮到了許多協議的實現如FTP,SMTP,HTTP以及一些二進位制和文字的遺留協議的一些經驗。作為結果,Netty成功地找到了一種在不妥協的前提下實現開發簡單,高效能,高穩定性,靈活性的方式。

一些使用者可能已經發現一些其它的網路應用框架宣告有同樣的優勢,並且你可能會問Netty與其它框架的不同之外在哪裡。答案就在於它所基於的哲理。Netty設計的初衷就是為了在API的角度和實現的第一天開始就提供最好的使用者體驗。這不是有開有的東西,但你會意識到,當你閱讀這篇教程並與Netty玩時,你的生活將變得更加容易。

開始

這個章節圍繞Netty的核心結構,通過簡單的例子讓你快速入門。當你在本章末尾的時候,你可以快速寫一個客戶端和伺服器程式。

如果你喜歡自頂向下的方式學習一些東西,你可能會喜歡從第二章開始,再回到這裡。

開始之前

執行本章中引入的例子的最低要求有兩個:最新的Netty版本以及JDK1.6或以上。最新版本的Netty在下載頁進行下載。為了下載正確版本的JDK,請到JDK供應商的網站上進行下載。

正如你所讀到的,你可能會對本章中所引入的類在點疑惑。請在需要知道API明細的時候檢視對應的API。為方便起見,在這文章的類都連結到了線上的文件。另外,請不要猶豫向Netty Project Community傳送郵件,並讓我們知道是否有不正確的資訊,語法錯誤,列印錯誤以及提高文件的一些好的想法。

寫一個廢棄的伺服器

現實中的最簡單的協議並不是"Hello,World!",而是DISCARD。這是一個不進行響應,直接丟棄收到的資料的協議。

為了實現DISCARD協議,你唯一需要做的事情是忽略任何接收到的資料。我們直接從控制器的實現開始,這個控制器用來處理Netty產生的IO事件:

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已經足夠了,你並不需要自己實現它實現ChannelInboundHandler介面。
  2. 我們在這裡覆蓋了channelReload()事件處理方法。這個方法無論在什麼時候,只要在客戶端接收到資料就會被呼叫。在這個例子中,接收到的資料型別是ByteBuffer。
  3. 為了實現DISCARD協議,事件處理函式必須丟棄接收到的資料。ByteBuffer是一個引用物件,因此,它必須呼叫release()方法來釋放。請記住必須由事件處理函式來釋放任何傳遞給事件處理函式的物件。通常情況下,channelRead()處理函式像這樣實現:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        // Do something with msg
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
複製程式碼
  1. 當異常物件被Nettey丟擲時,這些異常物件通常由IO異常或者在處理訊息時由訊息處理函式丟擲的,此時exceptionCaught()事件處理函式會被呼叫。在大多數情況下,異常資訊需要被記錄,並且相關的通道需要被關閉。儘管這個方法的實現是由你根據具體的異常情況來決定的。例如,在關閉連線之前,你可能想傳送包含錯誤程式碼的響應訊息。

到目前為止,一切正常。我們已經實現了DISCARD伺服器的前半部分。接下來我們來執行DiscardServer中的main方法來啟動伺服器。

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是一個事件迴圈,它是用來處理IO操作。Netty針對不同的傳輸協議提供了一系列的EventLoopGroup實現類。在這個例子中,我們正在實現一個服務端的應用,並且使用了兩個NIOEventGroup。第一個叫做boss,用來接收新進來的連線。第二個叫做worker,用來處理已接收的連線的流量。一旦boss接收了連線,並且將這個連線註冊到worker上。使用多少執行緒以及它們對映到建立的通道上是由EventLoopGroup實現決定的,甚至也可以通過構造器來配置。
  2. ServerBootstrap是一個幫助類來建立伺服器。你也可以通過使用Channel直接建立一個伺服器。然而,請注意這是一個無聊的過程,而且你在大多數情況下也不需要那樣做。
  3. 在這裡,我們指定了一個NioServerSocketChannel類初始化了一個新的通道來接收新進來的連線。
  4. 每個新進來的通道的事件處理函式都會被重新評估。ChannelInitializer是一個用來配置新通道的特殊的處理函式。在配置新通道的ChannelPipeline時,你很有可能會像這樣新增一些處理函式如DiscardServerHandler來實現網路應用。隨著應用程式變得越來越複雜,你很有可能會向管道中新增更多的處理函式,並最終提取這個匿名函式到類中。
  5. 你也可以針對通道的實現來設定引數。我們在寫一個TCP/IP伺服器,所以我們可以設定一些socket的選項,如tcpNoDelay和keepAlive。請檢視ChannelOption的javaDoc以及特殊的ChannelConfig實現來獲得更多資訊。
  6. 你注意到option()和childOption()選項嗎?option()是用於NioServerChannel實現接收新的連線。childOption()是用於父ServerSocketChannel(在這裡是NioServerSocketChannel)所接收的連線。
  7. 我們現在可以開始了。剩下的就是繫結埠和啟動了伺服器了。我這裡我們繫結了埠8080。你可以呼叫bind()方法來任意繫結埠。

檢視接收的資料

既然我們已經寫了我們的第一個伺服器,我們需要測試來檢測這工作是否正常。最簡單的方式是使用telnet來進行測試。例如,你可以在命令列輸入telnet 8080,並輸入一些內容。

然而,我們可以說伺服器工作正常嗎?我們並不能夠確定,因為它是一個Discard伺服器。我們不會得到任何響應。為了驗證它正在工作,我們修改伺服器讓它輸出所接收到的資料。

我們現在已經知道channelReload()方法會在接收到資料時被呼叫。我們在DidcardServerHandler類中的channelReload方法中新增一些程式碼。

@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. 效率低下的loop事實上可以簡化為 System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))。
  2. 可選的,你也可以使用in.release()方法。

如果你再次執行telnet命令,你會發現伺服器輸入你所接收到的資料。 完整的discard伺服器的程式碼在io.netty.example.discard包下。

寫一個Echo伺服器

到目前為止,我們已經消費了資料但並沒有一點響應。一個伺服器,然而一般都是用來響應請求的。讓我們通過實現g 一個Echo協議來學習如何向客戶端響應請求,在這個例子裡,伺服器返回接收到的資料。

與前一小節我們實現的discard伺服器不同的是它返回客戶端接收到的資料,而不是在控制檯輸出。因此,修改channelReload()方法已經足夠了。

   @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); // (1)
        ctx.flush(); // (2)
    }
複製程式碼
  1. ChannelHandlerContext物件提供了各種操作來讓你觸發各種IO事件以及操作。在這裡,我們呼叫了write(Object)方法來逐字地寫入接收到的資料。請注意,除非你像DISCARD例子中的那樣,我們不會釋放任何接收到的資料。因為,在將資料寫入到匯流排上時,Netty將釋放資源的決定權交給你。
  2. ctx.write(Object)不會將訊息寫入到匯流排上。它內部是快取的,然後是通過ctx.flush()方法將它重新整理到匯流排上。可選的,你可以使用ctx.writeAndFlush(msg)來簡化操作。

如果你再次執行telnet命令,你會發現伺服器向你發回你所傳送的資料。

echo伺服器的完整程式碼在io.netty.example.echo包下。

編寫一個時間伺服器

這個小節需要實現的協議是TIME協議。這跟以前的例子有點不同。在這個例子中,它會傳送一個32位的整數,同時並不會接收任何請求。當訊息傳送完成之後會關閉連線。在這個例子中,你將會學習到如何構造和傳送訊息,在完成傳送時關閉連線。

由於我們會忽略掉在連線建立好以後接收到的任何資料,我們這次不使用channelReload方法。作為替代,我們重寫了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. 為了傳送一條訊息,我們需要分配一個新的ByteBuffer,這個ByteBuffer會包含訊息。我們會寫入一個32位的整數,因此,我們需要一個ByteBuffer,它的長度是四個位元組。通過ChannelHandlerContext.alloc()方法來獲得ByteBufferAllocator,並分配新的Buffer。
  3. 一般情況下,你們會寫入構造過的訊息。

但是請等一下,flip去哪裡了?在NIO中我們傳送資料之前不需要呼叫java.nio.ByteBuffer.flip()方法嗎?ByteBuf沒有這樣的方法,因為這有兩個指標,一個是用來讀的,一個是用來寫的。寫的指標會隨著資料寫入的增加而增加,而讀指標不會發生變化。

相反,NIO Buffer在沒有呼叫flip方法這前,並不會提供一個清除方式來計算訊息是從哪裡開始與結束的。當你沒有呼叫flip方法,你會遇到一些麻煩。因為沒有資料或錯誤的資料將會被髮送。這種錯誤不會發生在Netty中,因為我們針對不同的型別有不同的指標。你會發現隨著你熟悉netty,你的生活將會變得更加容易-一個沒有flipping out的生活。

另外一個需要注意的點是ChannelHandlerContext.write()方法和writeAndFlush()方法,它們會返回一個ChannelFutrue。ChannelFuture程式碼一個還沒有出現IO操作。這意味著,任何請求操作可能還沒有被操作,因為所有的操作在Netty中都是非同步的。例如,下面的程式碼可能在傳送訊息之前就關閉連線了。

Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
複製程式碼

因此,你需要在write()方法返回的ChannelFuture方法完成之後呼叫close()方法。它會通知它的監聽器所有的操作都已經完成了。請注意,close()方法也不會立刻關閉連線,它會返回一下ChannelFuture。

在寫請求完成之後我們要怎樣被通知?這隻需要簡單地為返回的ChannelFuture物件新增一個ChannelFutrueListener。在這裡,我們建立了一個匿名的ChannelFutureListener,在這個Listener中會在操作完成之後關閉通道。

可選的,你可以使用預定義的監聽器:

f.addListener(ChannelFutureListener.CLOSE);
複製程式碼

為了檢測我們的時間伺服器是否預期的工作,可以使用unix的rdate命令:

rdate -o <port> -p <host>
複製程式碼

埠號是main函式是指定的埠號,一般情況下是localohst。

寫一個時間客戶端

不像DISCARD和ECHO伺服器,我們需要為TIME協議提供一個客戶端。因為使用者無法將一個整數轉換為日曆中的某個日期。在這個小節中,我們將討論如何讓伺服器工作正常,並學習如何用Netty寫一個客戶端。

服務端和客戶端最大的也是唯一的不同之處是Netty中的客戶端使用了不同的Bootstrap和Channel實現。我們來看一下以下的程式碼:

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. BootStrap與ServerBootstrap類似,除了它是針對非伺服器通道,如客戶端或其他無連線的通道。
  2. 如果你僅指定不念舊惡EventLoopGroup,它將既被用於boss group,也會被用於work group。儘管在客戶端不會使用boss group。
  3. 使用了NioSocketChannel來代替NioServerSocketChannel。
  4. 注意到在這裡我們沒有像ServerBootstrap那樣使用使用childOption。因為客戶端的SocketChannel是沒有父親的。
  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從其它節點讀取的資料會放入到ByteBuffer中。

這看上去很簡單,看上去跟服務端的例子差不多。然而,這個handler有時會拒絕工作,並搜出IndexOutOfBoundsException,這個會在下一小節進行詳細介紹。

處理基於流的傳輸

Socket Buffer的小警告

在基於流的傳輸通道(如TCP/IP)中,接收到的資料是放在Socket接收快取中的。不幸的是,基於流的傳輸通道不是資料包的佇列而是基於位元組的佇列。這意味著,即使你是通過兩個獨立的包傳送資料的,作業系統並不會把它們當成兩條訊息,而是一堆位元組。因此,你所讀取到的資料並不一定是遠端的其它節點寫入的資料。例如,我們想像一下作業系統的TCP/IP棧接收到了三個包。

image
因為這是基於流的協議,在你的應用中很有可能會按照如下的格式讀取到資料。
image
因此,接收部分,無論是服務端還是客戶端,應該將接收到的資料整理成一個或多個有意義的片段。這些片段能夠方便地被應用程式理解。以上面的例子為例,接收到的資料應該按以下進行劃分:
image

第一種解決方案

讓我們回到TIME客戶端的例子。我們也有相同的問題。一個32位的整數是一個非常小的資料,它不太可能會被分割。然而,問題是它可以被分割,分割的可能性會導致流量增加。

最簡單的方法是建立一個內部的累計的快取,並且等到所有的四個位元組都寫入到快取。以下是修改過的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有兩個生命週期相關的方法:handlerAdd()和handlerRemoved()。你可以執行任意初始化任務,只要這些任務不要阻塞太長的時間。
  2. 首先, 所有接收到的資料都會放入到buf中。
  3. 然後,handler必須檢查buffer是否有足夠的資料。在這個例子中是四個位元組,並且處理實際的業務邏輯。不然,Netty會當資料到達時呼叫channelReload方法,並最終累計到4個位元組。

第二個解決方案

儘管第一個方案已經解決了TIME客戶端所遇到的問題,修改後的handler看上去並不是那樣的乾淨。想像一下一個更加複雜的協議,它由多個欄位組成,如一個可變的長度欄位。你的ChannelInboundHandler的實現類馬上會變得不可維護。

正如你所看到的,你可以新增不止一個Channel到ChannelPipeLine。因此,你可以將一個整體的ChannelHandler分割成多個模組化的Handler來降低系統的複雜性。例如,你可以將TimeClinetHandler分割成兩個Handler。

  • TimeDecoder負責處理片段問題。
  • 最初的簡單版本的TimeClinetHandler。

幸運的是,Netty提供了一個要擴充套件的類來幫助你編寫程式。

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. ByteToMessageDecoder是一個ChannelInboundHandler的實現類,它主要用來處理分片事件。
  2. ByteToMessageDecoder會在接收到新資料時呼叫了decode()方法,這個方法內部維護了一個累加的buffer。
  3. decode方法可以決定是否需要新增內容到out物件。ByteToMessageDecoder方法會在接收到更多資料時呼叫decode方法。
  4. 如果decode方法往out中新增了新的物件。這意味著deceder成功地編碼了一條訊息。ByteToMessageDecoder會丟棄累計的buffer中已經讀到的資料。請記住,你不需要解碼多條訊息,ByteToMessageDecoder會一直呼叫 decode直到out中沒有新增資料為止。

既然我們已經往ChannelPipeLine中插入了一個新的handler,我們應該修改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也提供了開箱即用的解碼器來幫助你實現你的協議更加容易,避免出現整體的,不可維護的handler實現。請檢視以下包來獲得更多的例子:

  • io.netty.example.factorial for a binary protocol, and
  • io.netty.example.telnet for a text line-based protocol.

用POJO代替ByteBuf

到目前為止的所有例子使用了ByteBuf作為協議訊息的主要結構。在這個例子中,我們會實現TIME協議的客戶端和服務端的例子,這些例子中使用了POJO代替了ByteBuf。

在ChannelHandler中使用POJO的優勢是非常明顯的。我的Handler將變得更加可維護與可重用,並且可以將我們的程式碼從解析ByteBuf中的資料分離出來。在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()));
}
複製程式碼

在更新後的Decoder中,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);
}
複製程式碼

現在,少了的部分是一個編碼器,它是一個ChannelInboundHandler的實現類。現在編寫解碼器變得更加簡單了,因為現在沒有必要處理包分段和編碼訊息時進行裝配。

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

Shutting Down Your Application

相關文章