Netty 3.1 中文用戶手冊(二)-開始

原文地址: http://www.jboss.org/file-access/default/members/netty/freezone/guide/3.1/html_single/index.html

 

Netty 3.1 中文用戶手冊(一)-序言

Netty 3.1 中文用戶手冊(二)-開始

Netty 3.1 中文用戶手冊(三)-架構總覽

 

第一章. 開始


這一章節將圍繞Netty的核心結構展開,同時通過一些簡單的例子可以讓你更快的瞭解Netty的使用。當你讀完本章,你將有能力使用Netty完成客戶端和服務端的開發。

如果你更喜歡自上而下式的學習方式,你可以首先完成 第二章:架構總覽 的學習,然後再回到這裏。

1.1. 開始之前


運行本章示例程序的兩個最低要求是:最新版本的Netty程序以及JDK 1.5或更高版本。最新版本的Netty程序可在項目下載頁 下載。下載正確版本的JDK,請到你偏好的JDK站點下載。

這就已經足夠了嗎?實際上你會發現,這兩個條件已經足夠你完成任何協議的開發了。如果不是這樣,請聯繫Netty項目社區 ,讓我們知道還缺少了什麼。

最終但不是至少,當你想了解本章所介紹的類的更多信息時請參考API手冊。爲方便你的使用,這篇文檔中所有的類名均連接至在線API手冊。此外,如果本篇文檔中有任何錯誤信息,無論是語法錯誤,還是打印排版錯誤或者你有更好的建議,請不要顧慮,立即聯繫Netty項目社區

1.2. 拋棄協議服務


在這個世界上最簡化的協議不是“Hello,world!”而是拋棄協議 。這是一種丟棄接收到的任何數據並不做任何迴應的協議。

實現拋棄協議(DISCARD protocol),你僅需要忽略接受到的任何數據即可。讓我們直接從處理器(handler)實現開始,這個處理器處理Netty的所有I/O事件。

package org.jboss.netty.example.discard;

@ChannelPipelineCoverage("all")1
public class DiscardServerHandler extends SimpleChannelHandler {2

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {3
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {4
        e.getCause().printStackTrace();
       
        Channel ch = e.getChannel();
        ch.close();
    }
}

 

     代碼說明

1)ChannelPipelineCoverage註解了一種處理器類型,這個註解標示了一個處理器是否可被多個Channel通道共享(同時關聯着ChannelPipeline)。DiscardServerHandler沒有處理任何有狀態的信息,因此這裏的註解是“all”。

2)DiscardServerHandler繼承了SimpleChannelHandler,這也是一個ChannelHandler 的實現。SimpleChannelHandler提供了多種你可以重寫的事件處理方法。目前直接繼承SimpleChannelHandler已經足夠了,並不需要你完成一個自己的處理器接口。

3)我們這裏重寫了messageReceived事件處理方法。這個方法由一個接收了客戶端傳送數據的MessageEvent事件調用。在這個例子中,我們忽略接收到的任何數據,並以此來實現一個拋棄協議(DISCARD protocol)。

4)exceptionCaught 事件處理方法由一個ExceptionEvent異常事件調用,這個異常事件起因於Netty的I/O異常或一個處理器實現的內部異常。多數情況下,捕捉到的異常應當被記錄下來,並在這個方法中關閉這個channel通道。當然處理這種異常情況的方法實現可能因你的實際需求而有所不同,例如,在關閉這個連接之前你可能會發送一個包含了錯誤碼的響應消息。
 

目前進展不錯,我們已經完成了拋棄協議服務器的一半開發工作。下面要做的是完成一個可以啓動這個包含DiscardServerHandler處理器服務的主方法。

 

package org.jboss.netty.example.discard;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class DiscardServer {

    public static void main(String[] args) throws Exception {
        ChannelFactory factory =
            new NioServerSocketChannelFactory (
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool());

        ServerBootstrap bootstrap = new ServerBootstrap (factory);

        DiscardServerHandler handler = new DiscardServerHandler();
        ChannelPipeline pipeline = bootstrap.getPipeline();
        pipeline.addLast("handler", handler);

        bootstrap.setOption("child.tcpNoDelay", true);
        bootstrap.setOption("child.keepAlive", true);

        bootstrap.bind(new InetSocketAddress(8080));
    }
}

 

     代碼說明


1)ChannelFactory 是一個創建和管理Channel通道及其相關資源的工廠接口,它處理所有的I/O請求併產生相應的I/O ChannelEvent通道事件。Netty 提供了多種 ChannelFactory 實現。這裏我們需要實現一個服務端的例子,因此我們使用NioServerSocketChannelFactory實現。另一件需要注意的事情是這個工廠並自己不負責創建I/O線程。你應當在其構造器中指定該工廠使用的線程池,這樣做的好處是你獲得了更高的控制力來管理你的應用環境中使用的線程,例如一個包含了安全管理的應用服務。

2)ServerBootstrap 是一個設置服務的幫助類。你甚至可以在這個服務中直接設置一個Channel通道。然而請注意,這是一個繁瑣的過程,大多數情況下並不需要這樣做。

3)這裏,我們將DiscardServerHandler處理器添加至默認的ChannelPipeline通道。任何時候當服務器接收到一個新的連接,一個新的ChannelPipeline管道對象將被創建,並且所有在這裏添加的ChannelHandler對象將被添加至這個新的ChannelPipeline管道對象。這很像是一種淺拷貝操作(a shallow-copy operation);所有的Channel通道以及其對應的ChannelPipeline實例將分享相同的DiscardServerHandler實例。

4)你也可以設置我們在這裏指定的這個通道實現的配置參數。我們正在寫的是一個TCP/IP服務,因此我們運行設定一些socket選項,例如tcpNoDelay和keepAlive。請注意我們在配置選項裏添加的"child."前綴。這意味着這個配置項僅適用於我們接收到的通道實例,而不是ServerSocketChannel實例。因此,你可以這樣給一個ServerSocketChannel設定參數:
bootstrap.setOption("reuseAddress", true);

5)我們繼續。剩下要做的是綁定這個服務使用的端口並且啓動這個服務。這裏,我們綁定本機所有網卡(NICs,network interface cards)上的8080端口。當然,你現在也可以對應不同的綁定地址多次調用綁定操作。
 

大功告成!現在你已經完成你的第一個基於Netty的服務端程序。

1.3. 查看接收到的數據


現在你已經完成了你的第一個服務端程序,我們需要測試它是否可以真正的工作。最簡單的方法是使用telnet 命令。例如,你可以在命令行中輸入“telnet localhost 8080 ”或其他類型參數。

然而,我們可以認爲服務器在正常工作嗎?由於這是一個丟球協議服務,所以實際上我們無法真正的知道。你最終將收不到任何迴應。爲了證明它在真正的工作,讓我們修改代碼打印其接收到的數據。
我們已經知道當完成數據的接收後將產生MessageEvent消息事件,並且也會觸發messageReceived處理方法。所以讓我在DiscardServerHandler處理器的messageReceived方法內增加一些代碼。

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
    ChannelBuffer  buf = (ChannelBuffer) e.getMessage();
    while(buf.readable()) {
        System.out.println((char) buf.readByte());
    }
}

 

     代碼說明

1) 基本上我們可以假定在socket的傳輸中消息類型總是ChannelBuffer。ChannelBuffer是Netty的一個基本數據結構,這個數據結構存儲了一個字節序列。ChannelBuffer類似於NIO的ByteBuffer,但是前者卻更加的靈活和易於使用。例如,Netty允許你創建一個由多個ChannelBuffer構建的複合ChannelBuffer類型,這樣就可以減少不必要的內存拷貝次數。

2) 雖然ChannelBuffer有些類似於NIO的ByteBuffer,但強烈建議你參考Netty的API手冊。學會如何正確的使用ChannelBuffer是無障礙使用Netty的關鍵一步。
 

如果你再次運行telnet命令,你將會看到你所接收到的數據。
拋棄協議服務的所有源代碼均存放在在分發版的org.jboss.netty.example.discard包下。


1.4. 響應協議服務


目前,我們雖然使用了數據,但最終卻未作任何迴應。然而一般情況下,一個服務都需要回應一個請求。讓我們實現ECHO協議 來學習如何完成一個客戶請求的迴應消息,ECHO協議規定要返回任何接收到的數據。

與我們上一節實現的拋棄協議服務唯一不同的地方是,這裏需要返回所有的接收數據而不是僅僅打印在控制檯之上。因此我們再次修改messageReceived方法就足夠了。

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
    Channel  ch = e.getChannel();
    ch.write(e.getMessage());
}
代碼說明
1) 一個ChannelEvent通道事件對象自身存有一個和其關聯的Channel對象引用。這個返回的Channel通道對象代表了這個接收 MessageEvent消息事件的連接(connection)。因此,我們可以通過調用這個Channel通道對象的write方法向遠程節點寫入返回數據。
 

現在如果你再次運行telnet命令,你將會看到服務器返回的你所發送的任何數據。

相應服務的所有源代碼存放在分發版的org.jboss.netty.example.echo包下。

1.5. 時間協議服務


這一節需要實現的協議是TIME協議 。這是一個與先前所介紹的不同的例子。這個例子裏,服務端返回一個32位的整數消息,我們不接受請求中包含的任何數據並且當消息返回完畢後立即關閉連接。通過這個例子你將學會如何構建和發送消息,以及當完成處理後如何主動關閉連接。

因爲我們會忽略接收到的任何數據而只是返回消息,這應當在建立連接後就立即開始。因此這次我們不再使用messageReceived方法,取而代之的是使用channelConnected方法。下面是具體的實現:

 

package org.jboss.netty.example.time;

@ChannelPipelineCoverage("all")
public class TimeServerHandler extends SimpleChannelHandler {

    @Override
    public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
        Channel ch = e.getChannel();
       
        ChannelBuffer time = ChannelBuffers.buffer(4);
        time.writeInt(System.currentTimeMillis() / 1000);
       
        ChannelFuture f = ch.write(time);
       
        f.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture future) {
                Channel ch = future.getChannel();
                ch.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}
 
代碼說明
1) 正如我們解釋過的,channelConnected方法將在一個連接建立後立即觸發。因此讓我們在這個方法裏完成一個代表當前時間(秒)的32位整數消息的構建工作。

2) 爲了發送一個消息,我們需要分配一個包含了這個消息的buffer緩衝。因爲我們將要寫入一個32位的整數,因此我們需要一個4字節的 ChannelBuffer。ChannelBuffers是一個可以創建buffer緩衝的幫助類。除了這個buffer方法,ChannelBuffers還提供了很多和ChannelBuffer相關的實用方法。更多信息請參考API手冊。

另外,一個很不錯的方法是使用靜態的導入方式:
import static org.jboss.netty.buffer.ChannelBuffers.*;
...
ChannelBuffer dynamicBuf = dynamicBuffer(256);
ChannelBuffer ordinaryBuf = buffer(1024);

3) 像通常一樣,我們需要自己構造消息。

但是打住,flip在哪?過去我們在使用NIO發送消息時不是常常需要調用 ByteBuffer.flip()方法嗎?實際上ChannelBuffer之所以不需要這個方法是因爲 ChannelBuffer有兩個指針;一個對應讀操作,一個對應寫操作。當你向一個 ChannelBuffer寫入數據的時候寫指針的索引值便會增加,但與此同時讀指針的索引值不會有任何變化。讀寫指針的索引值分別代表了這個消息的開始、結束位置。

與之相應的是,NIO的buffer緩衝沒有爲我們提供如此簡潔的一種方法,除非你調用它的flip方法。因此,當你忘記調用flip方法而引起發送錯誤時,你便會陷入困境。這樣的錯誤不會再Netty中發生,因爲我們對應不同的操作類型有不同的指針。你會發現就像你已習慣的這樣過程變得更加容易—一種沒有flippling的體驗!

另一點需要注意的是這個寫方法返回了一個ChannelFuture對象。一個ChannelFuture 對象代表了一個尚未發生的I/O操作。這意味着,任何已請求的操作都可能是沒有被立即執行的,因爲在Netty內部所有的操作都是異步的。例如,下面的代碼可能會關閉一 個連接,這個操作甚至會發生在消息發送之前:

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

因此,你需要這個write方法返回的ChannelFuture對象,close方法需要等待寫操作異步完成之後的ChannelFuture通知/監聽觸發。需要注意的是,關閉方法仍舊不是立即關閉一個連接,它同樣也是返回了一個ChannelFuture對象。

4) 在寫操作完成之後我們又如何得到通知?這個只需要簡單的爲這個返回的ChannelFuture對象增加一個ChannelFutureListener 即可。在這裏我們創建了一個匿名ChannelFutureListener對象,在這個ChannelFutureListener對象內部我們處理了異步操作完成之後的關閉操作。

另外,你也可以通過使用一個預定義的監聽類來簡化代碼。
f.addListener(ChannelFutureListener.CLOSE);
 


1.6. 時間協議服務客戶端


不同於DISCARD和ECHO協議服務,我們需要一個時間協議服務的客戶端,因爲人們無法直接將一個32位的二進制數據轉換一個日曆時間。在這一節我們將學習如何確保服務器端工作正常,以及如何使用Netty完成客戶端的開發。

使用Netty開發服務器端和客戶端代碼最大的不同是要求使用不同的Bootstrap及ChannelFactory。請參照以下的代碼:

package org.jboss.netty.example.time;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class TimeClient {

    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);

        ChannelFactory factory =
            new NioClientSocketChannelFactory (
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool());

        ClientBootstrap bootstrap = new ClientBootstrap (factory);

        TimeClientHandler handler = new TimeClientHandler();
        bootstrap.getPipeline().addLast("handler", handler);
       
        bootstrap.setOption("tcpNoDelay" , true);
        bootstrap.setOption("keepAlive", true);

        bootstrap.connect (new InetSocketAddress(host, port));
    }
}
代碼說明
1) 使用NioClientSocketChannelFactory而不是NioServerSocketChannelFactory來創建客戶端的Channel通道對象。

2) 客戶端的ClientBootstrap對應ServerBootstrap。

3) 請注意,這裏不存在使用“child.”前綴的配置項,客戶端的SocketChannel實例不存在父級Channel對象。

4) 我們應當調用connect連接方法,而不是之前的bind綁定方法。
 

正如你所看到的,這與服務端的啓動過程是完全不一樣的。ChannelHandler又該如何實現呢?它應當負責接收一個32位的整數,將其轉換爲可讀的格式後,打印輸出時間,並關閉這個連接。

 

package org.jboss.netty.example.time;

import java.util.Date;

@ChannelPipelineCoverage("all")
public class TimeClientHandler extends SimpleChannelHandler {

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        ChannelBuffer buf = (ChannelBuffer) e.getMessage();
        long currentTimeMillis = buf.readInt() * 1000L;
        System.out.println(new Date(currentTimeMillis));
        e.getChannel().close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}
 

這看起來很是簡單,與服務端的實現也並未有什麼不同。然而,這個處理器卻時常會因爲拋出IndexOutOfBoundsException異常而拒絕工作。我們將在下一節討論這個問題產生的原因。

1.7. 流數據的傳輸處理

 

1.7.1. Socket Buffer的缺陷


對於例如TCP/IP這種基於流的傳輸協議實現,接收到的數據會被存儲在socket的接受緩衝區內。不幸的是,這種基於流的傳輸緩衝區並不是一個包隊列,而是一個字節隊列。這意味着,即使你以兩個數據包的形式發送了兩條消息,操作系統卻不會把它們看成是兩條消息,而僅僅是一個批次的字節序列。因此,在這種情況下我們就無法保證收到的數據恰好就是遠程節點所發送的數據。例如,讓我們假設一個操作系統的TCP/IP堆棧收到了三個數據包:

 

+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
 

由於這種流傳輸協議的普遍性質,在你的應用中有較高的可能會把這些數據讀取爲另外一種形式:

 

+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+

 

因此對於數據的接收方,不管是服務端還是客戶端,應當重構這些接收到的數據,讓其變成一種可讓你的應用邏輯易於理解的更有意義的數據結構。在上面所述的這個例子中,接收到的數據應當重構爲下面的形式:

 

+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
 

1.7.2. 第一種方案


現在讓我們回到時間協議服務客戶端的例子中。我們在這裏遇到了同樣的問題。一個32位的整數是一個非常小的數據量,因此它常常不會被切分在不同的數據段內。然而,問題是它確實可以被切分在不同的數據段內,並且這種可能性隨着流量的增加而提高。

最簡單的方案是在程序內部創建一個可準確接收4字節數據的累積性緩衝。下面的代碼是修復了這個問題後的TimeClientHandler實現。

 

package org.jboss.netty.example.time;

import static org.jboss.netty.buffer.ChannelBuffers.*;

import java.util.Date;

@ChannelPipelineCoverage("one")
public class TimeClientHandler extends SimpleChannelHandler {

    private final ChannelBuffer buf = dynamicBuffer();

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        ChannelBuffer m = (ChannelBuffer) e.getMessage();
        buf.writeBytes(m);
       
        if (buf.readableBytes() >= 4) {
            long currentTimeMillis = buf.readInt() * 1000L;
            System.out.println(new Date(currentTimeMillis));
            e.getChannel().close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

 

     代碼說明  

1) 這一次我們使用“one”做爲ChannelPipelineCoverage的註解值。這是由於這個修改後的TimeClientHandler不在不在內部保持一個buffer緩衝,因此這個TimeClientHandler實例不可以再被多個Channel通道或ChannelPipeline共享。否則這個內部的buffer緩衝將無法緩衝正確的數據內容。

2) 動態的buffer緩衝也是ChannelBuffer的一種實現,其擁有動態增加緩衝容量的能力。當你無法預估消息的數據長度時,動態的buffer緩衝是一種很有用的緩衝結構。

3) 首先,所有的數據將會被累積的緩衝至buf容器。

4) 之後,這個處理器將會檢查是否收到了足夠的數據然後再進行真實的業務邏輯處理,在這個例子中需要接收4字節數據。否則,Netty將重複調用messageReceived方法,直至4字節數據接收完成。
 

這裏還有另一個地方需要進行修改。你是否還記得我們把TimeClientHandler實例添加到了這個ClientBootstrap實例的默認ChannelPipeline管道里?這意味着同一個TimeClientHandler實例將被多個Channel通道共享,因此接受的數據也將受到破壞。爲了給每一個Channel通道創建一個新的TimeClientHandler實例,我們需要實現一個ChannelPipelineFactory管道工廠:

package org.jboss.netty.example.time;

public class TimeClientPipelineFactory implements ChannelPipelineFactory {

    public ChannelPipeline getPipeline() {
        ChannelPipeline pipeline = Channels.pipeline();
        pipeline.addLast("handler", new TimeClientHandler());
        return pipeline;
    }
}
 

現在,我們需要把TimeClient下面的代碼片段:

TimeClientHandler handler = new TimeClientHandler();
bootstrap.getPipeline().addLast("handler", handler);
 

替換爲:

bootstrap.setPipelineFactory(new TimeClientPipelineFactory());
 

雖然這看上去有些複雜,並且由於在TimeClient內部我們只創建了一個連接(connection),因此我們在這裏確實沒必要引入TimeClientPipelineFactory實例。

然而,當你的應用變得越來越複雜,你就總會需要實現自己的ChannelPipelineFactory,這個管道工廠將會令你的管道配置變得更加具有靈活性。

1.7.3. 第二種方案

 

雖然第二種方案解決了時間協議客戶端遇到的問題,但是這個修改後的處理器實現看上去卻不再那麼簡潔。設想一種更爲複雜的,由多個可變長度字段組成的協議。你的ChannelHandler實現將變得越來越難以維護。

正如你已注意到的,你可以爲一個ChannelPipeline添加多個ChannelHandler,因此,爲了減小應用的複雜性,你可以把這個臃腫的ChannelHandler切分爲多個獨立的模塊單元。例如,你可以把TimeClientHandler切分爲兩個獨立的處理器:

  •   TimeDecoder,解決數據分段的問題。
  •   TimeClientHandler,原始版本的實現。

幸運的是,Netty提供了一個可擴展的類,這個類可以直接拿過來使用幫你完成TimeDecoder的開發:

package org.jboss.netty.example.time;


public class TimeDecoder extends FrameDecoder {

    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer)  {
           
        if (buffer.readableBytes() < 4) {
            return null; 
        }
       
        return buffer.readBytes(4);
    }
}

 

代碼說明
1) 這裏不再需要使用ChannelPipelineCoverage的註解,因爲FrameDecoder總是被註解爲“one”。

2) 當接收到新的數據後,FrameDecoder會調用decode方法,同時傳入一個FrameDecoder內部持有的累積型buffer緩衝。

3) 如果decode返回null值,這意味着還沒有接收到足夠的數據。當有足夠數量的數據後FrameDecoder會再次調用decode方法。

4) 如果decode方法返回一個非空值,這意味着decode方法已經成功完成一條信息的解碼。FrameDecoder將丟棄這個內部的累計型緩衝。請注意你不需要對多條消息進行解碼,FrameDecoder將保持對decode方法的調用,直到decode方法返回非空對象。
 

如果你是一個勇於嘗試的人,你或許應當使用ReplayingDecoder,ReplayingDecoder更加簡化了解碼的過程。爲此你需要查看API手冊獲得更多的幫助信息。

package org.jboss.netty.example.time;

public class TimeDecoder extends ReplayingDecoder<VoidEnum> {

    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel,
            ChannelBuffer buffer, VoidEnum state) {
           
        return buffer.readBytes(4);
    }
}
 

此外,Netty還爲你提供了一些可以直接使用的decoder實現,這些decoder實現不僅可以讓你非常容易的實現大多數協議,並且還會幫你避免某些臃腫、難以維護的處理器實現。請參考下面的代碼包獲得更加詳細的實例:

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

1.8. 使用POJO代替ChannelBuffer


目前爲止所有的實例程序都是使用ChannelBuffer做爲協議消息的原始數據結構。在這一節,我們將改進時間協議服務的客戶/服務端實現,使用POJO 而不是ChannelBuffer做爲協議消息的原始數據結構。

在你的ChannelHandler實現中使用POJO的優勢是很明顯的;從你的ChannelHandler實現中分離從ChannelBuffer獲取數據的代碼,將有助於提高你的ChannelHandler實現的可維護性和可重用性。在時間協議服務的客戶/服務端代碼中,直接使用ChannelBuffer讀取一個32位的整數並不是一個主要的問題。然而,你會發現,當你試圖實現一個真實的協議的時候,這種代碼上的分離是很有必要的。

首先,讓我們定義一個稱之爲UnixTime的新類型。

package org.jboss.netty.example.time;

import java.util.Date;

public class UnixTime {
    private final int value;
   
    public UnixTime(int value) {
        this.value = value;
    }
   
    public int getValue() {
        return value;
    }
   
    @Override
    public String toString() {
        return new Date(value * 1000L).toString();
    }
}
 

現在讓我們重新修改TimeDecoder實現,讓其返回一個UnixTime,而不是一個ChannelBuffer。

@Override
protected Object decode(
        ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
    if (buffer.readableBytes() < 4) {
        return null;
    }

    return new UnixTime(buffer.readInt());
}
 

FrameDecoder和ReplayingDecoder允許你返回一個任何類型的對象。如果它們僅允許返回一個ChannelBuffer類型的對象,我們將不得不插入另一個可以從ChannelBuffer對象轉換 爲UnixTime對象的ChannelHandler實現。


有了這個修改後的decoder實現,這個TimeClientHandler便不會再依賴ChannelBuffer。

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
    UnixTime m = (UnixTime) e.getMessage();
    System.out.println(m);
    e.getChannel().close();
}
 

更加簡單優雅了,不是嗎?同樣的技巧也可以應用在服務端,讓我們現在更新TimeServerHandler的實現:

@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
    UnixTime time = new UnixTime(System.currentTimeMillis() / 1000);
    ChannelFuture f = e.getChannel().write(time);
    f.addListener(ChannelFutureListener.CLOSE);
}
 

現在剩下的唯一需要修改的部分是這個ChannelHandler實現,這個ChannelHandler實現需要把一個UnixTime對象重新轉換爲一個ChannelBuffer。但這卻已是相當簡單了,因爲當你對消息進行編碼的時候你不再需要處理數據包的拆分及組裝。

package org.jboss.netty.example.time;
   
import static org.jboss.netty.buffer.ChannelBuffers.*;

@ChannelPipelineCoverage("all")
public class TimeEncoder extends SimpleChannelHandler {

    public void writeRequested(ChannelHandlerContext ctx, MessageEvent  e) {
        UnixTime time = (UnixTime) e.getMessage();
       
        ChannelBuffer buf = buffer(4);
        buf.writeInt(time.getValue());
       
        Channels.write(ctx, e.getFuture(), buf);
    }
}
 

     代碼說明

1) 因爲這個encoder是無狀態的,所以其使用的ChannelPipelineCoverage註解值是“all”。實際上,大多數encoder實現都是無狀態的。

2) 一個encoder通過重寫writeRequested方法來實現對寫操作請求的攔截。不過請注意雖然這個writeRequested方法使用了和 messageReceived方法一樣的MessageEvent參數,但是它們卻分別對應了不同的解釋。一個ChannelEvent事件可以既是一個上升流事件(upstream event)也可以是一個下降流事件(downstream event),這取決於事件流的方向。例如:一個MessageEvent消息事件可以作爲一個上升流事件(upstream event)被messageReceived方法調用,也可以作爲一個下降流事件(downstream event)被writeRequested方法調用。請參考API手冊獲得上升流事件(upstream event)和下降流事件(downstream event)的更多信息。

3) 一旦完成了POJO和ChannelBuffer轉換,你應當確保把這個新的buffer緩衝轉發至先前的 ChannelDownstreamHandler處理,這個下降通道的處理器由某個ChannelPipeline管理。Channels提供了多個可以創建和發送ChannelEvent事件的幫助方法。在這個例子中,Channels.write(...)方法創建了一個新的 MessageEvent事件,並把這個事件發送給了先前的處於某個ChannelPipeline內的 ChannelDownstreamHandler處理器。

另外,一個很不錯的方法是使用靜態的方式導入Channels類:

import static org.jboss.netty.channel.Channels.*;
...
ChannelPipeline pipeline = pipeline();
write(ctx, e.getFuture(), buf);
fireChannelDisconnected(ctx);

 

最後的任務是把這個TimeEncoder插入服務端的ChannelPipeline,這是一個很簡單的步驟。

1.9. 關閉你的應用


如果你運行了TimeClient,你肯定可以注意到,這個應用並沒有自動退出而只是在那裏保持着無意義的運行。跟蹤堆棧記錄你可以發現,這裏有一些運行狀態的I/O線程。爲了關閉這些I/O線程並讓應用優雅的退出,你需要釋放這些由ChannelFactory分配的資源。

一個典型的網絡應用的關閉過程由以下三步組成:

  • 關閉負責接收所有請求的server socket。
  • 關閉所有客戶端socket或服務端爲響應某個請求而創建的socket。
  • 釋放ChannelFactory使用的所有資源。

爲了讓TimeClient執行這三步,你需要在TimeClient.main()方法內關閉唯一的客戶連接以及ChannelFactory使用的所有資源,這樣做便可以優雅的關閉這個應用。

package org.jboss.netty.example.time;

public class TimeClient {
    public static void main(String[] args) throws Exception {
        ...
        ChannelFactory factory = ...;
        ClientBootstrap bootstrap = ...;
        ...
        ChannelFuture future  = bootstrap.connect(...);
        future.awaitUninterruptible();
        if (!future.isSuccess()) {
            future.getCause().printStackTrace();
        }
        future.getChannel().getCloseFuture().awaitUninterruptibly();
        factory.releaseExternalResources();
    }
}
代碼說明
1) ClientBootstrap對象的connect方法返回一個ChannelFuture對象,這個ChannelFuture對象將告知這個連接操作的成功或失敗狀態。同時這個ChannelFuture對象也保存了一個代表這個連接操作的Channel對象引用。

2) 阻塞式的等待,直到ChannelFuture對象返回這個連接操作的成功或失敗狀態。

3) 如果連接失敗,我們將打印連接失敗的原因。如果連接操作沒有成功或者被取消,ChannelFuture對象的getCause()方法將返回連接失敗的原因。

4) 現在,連接操作結束,我們需要等待並且一直到這個Channel通道返回的closeFuture關閉這個連接。每一個Channel都可獲得自己的closeFuture對象,因此我們可以收到通知並在這個關閉時間點執行某種操作。

並且即使這個連接操作失敗,這個closeFuture仍舊會收到通知,因爲這個代表連接的 Channel對象將會在連接操作失敗後自動關閉。

5) 在這個時間點,所有的連接已被關閉。剩下的唯一工作是釋放ChannelFactory通道工廠使用的資源。這一步僅需要調用 releaseExternalResources()方法即可。包括NIO Secector和線程池在內的所有資源將被自動的關閉和終止。
 

關閉一個客戶端應用是很簡單的,但又該如何關閉一個服務端應用呢?你需要釋放其綁定的端口並關閉所有接受和打開的連接。爲了做到這一點,你需要使用一種數據結構記錄所有的活動連接,但這卻並不是一件容易的事。幸運的是,這裏有一種解決方案,ChannelGroup。

ChannelGroup是Java 集合 API的一個特有擴展,ChannelGroup內部持有所有打開狀態的Channel通道。如果一個Channel通道對象被加入到ChannelGroup,如果這個Channel通道被關閉,ChannelGroup將自動移除這個關閉的Channel通道對象。此外,你還可以對一個ChannelGroup對象內部的所有Channel通道對象執行相同的操作。例如,當你關閉服務端應用時你可以關閉一個ChannelGroup內部的所有Channel通道對象。

爲了記錄所有打開的socket,你需要修改你的TimeServerHandler實現,將一個打開的Channel通道加入全局的ChannelGroup對象,TimeServer.allChannels:

@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
    TimeServer.allChannels.add(e.getChannel());
}
代碼說明
是的,ChannelGroup是線程安全的。
 

現在,所有活動的Channel通道將被自動的維護,關閉一個服務端應用有如關閉一個客戶端應用一樣簡單。

package org.jboss.netty.example.time;

public class TimeServer {

    static final ChannelGroup allChannels = new DefaultChannelGroup("time-server" );

    public static void main(String[] args) throws Exception {
        ...
        ChannelFactory factory = ...;
        ServerBootstrap bootstrap = ...;
        ...
        Channel channel  = bootstrap.bind(...);
        allChannels.add(channel);
        waitForShutdownCommand();
        ChannelGroupFuture future = allChannels.close();
        future.awaitUninterruptibly();
        factory.releaseExternalResources();
    }
}
代碼說明
1) DefaultChannelGroup需要一個組名作爲其構造器參數。這個組名僅是區分每個ChannelGroup的一個標示。

2) ServerBootstrap對象的bind方法返回了一個綁定了本地地址的服務端Channel通道對象。調用這個Channel通道的close()方法將釋放這個Channel通道綁定的本地地址。

3) 不管這個Channel對象屬於服務端,客戶端,還是爲響應某一個請求創建,任何一種類型的Channel對象都會被加入ChannelGroup。因此,你儘可在關閉服務時關閉所有的Channel對象。

4) waitForShutdownCommand()是一個想象中等待關閉信號的方法。你可以在這裏等待某個客戶端的關閉信號或者JVM的關閉回調命令。

5) 你可以對ChannelGroup管理的所有Channel對象執行相同的操作。在這個例子裏,我們將關閉所有的通道,這意味着綁定在服務端特定地址的 Channel通道將解除綁定,所有已建立的連接也將異步關閉。爲了獲得成功關閉所有連接的通知,close()方法將返回一個 ChannelGroupFuture對象,這是一個類似ChannelFuture的對象。
 

1.10. 總述


在這一章節,我們快速瀏覽並示範瞭如何使用Netty開發網絡應用。下一章節將涉及更多的問題。同時請記住,爲了幫助你以及能夠讓Netty基於你的回饋得到持續的改進和提高,Netty社區 將永遠歡迎你的問題及建議。

 

 

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