Energy of Love |
|
|||
日歷
統(tǒng)計
導(dǎo)航常用鏈接留言簿隨筆分類
隨筆檔案
搜索最新評論
閱讀排行榜評論排行榜 |
![]() 序言 本指南對Netty 進行了介紹并指出其意義所在。 1. 問題 現(xiàn)在,我們使用適合一般用途的應(yīng)用或組件來和彼此通信。例如,我們常常使用一個HTTP客戶端從遠程服務(wù)器獲取信息或者通過web services進行遠程方法的調(diào)用。 然而,一個適合普通目的的協(xié)議或其實現(xiàn)并不具備其規(guī)模上的擴展性。例如,我們無法使用一個普通的HTTP服務(wù)器進行大型文件,電郵信息的交互,或者處理金 融信息和多人游戲數(shù)據(jù)那種要求準實時消息傳遞的應(yīng)用場景。因此,這些都要求使用一個適用于特殊目的并經(jīng)過高度優(yōu)化的協(xié)議實現(xiàn)。例如,你可能想要實現(xiàn)一個對 基于AJAX的聊天應(yīng)用,媒體流或大文件傳輸進行過特殊優(yōu)化的HTTP服務(wù)器。你甚至可能想去設(shè)計和實現(xiàn)一個全新的,特定于你的需求的通信協(xié)議。 另一種無法避免的場景是你可能不得不使用一種專有的協(xié)議和原有系統(tǒng)交互。在這種情況下,你需要考慮的是如何能夠快速的開發(fā)出這個協(xié)議的實現(xiàn)并且同時還沒有犧牲最終應(yīng)用的性能和穩(wěn)定性。 2. 方案 Netty 是一個異步的,事件驅(qū)動的網(wǎng)絡(luò)編程框架和工具,使用Netty 可以快速開發(fā)出可維護的,高性能、高擴展能力的協(xié)議服務(wù)及其客戶端應(yīng)用。 也就是說,Netty 是一個基于NIO的客戶,服務(wù)器端編程框架,使用Netty 可以確保你快速和簡單的開發(fā)出一個網(wǎng)絡(luò)應(yīng)用,例如實現(xiàn)了某種協(xié)議的客戶,服務(wù)端應(yīng)用。Netty相當簡化和流線化了網(wǎng)絡(luò)應(yīng)用的編程開發(fā)過程,例如,TCP和UDP的socket服務(wù)開發(fā)。 “快速”和“簡單”并不意味著會讓你的最終應(yīng)用產(chǎn)生維護性或性能上的問題。Netty 是一個吸收了多種協(xié)議的實現(xiàn)經(jīng)驗,這些協(xié)議包括FTP,SMPT,HTTP,各種二進制,文本協(xié)議,并經(jīng)過相當精心設(shè)計的項目,最終,Netty 成功的找到了一種方式,在保證易于開發(fā)的同時還保證了其應(yīng)用的性能,穩(wěn)定性和伸縮性。 一些用戶可能找到了某些同樣聲稱具有這些特性的編程框架,因此你們可能想問Netty 又有什么不一樣的地方。這個問題的答案是Netty 項目的設(shè)計哲學(xué)。從創(chuàng)立之初,無論是在API還是在其實現(xiàn)上Netty 都致力于為你提供最為舒適的使用體驗。雖然這并不是顯而易見的,但你終將會認識到這種設(shè)計哲學(xué)將令你在閱讀本指南和使用Netty 時變得更加得輕松和容易。 第一章. 開始 這一章節(jié)將圍繞Netty的核心結(jié)構(gòu)展開,同時通過一些簡單的例子可以讓你更快的了解Netty的使用。當你讀完本章,你將有能力使用Netty完成客戶端和服務(wù)端的開發(fā)。 如果你更喜歡自上而下式的學(xué)習方式,你可以首先完成 第二章:架構(gòu)總覽 的學(xué)習,然后再回到這里。 1.1. 開始之前 運行本章示例程序的兩個最低要求是:最新版本的Netty程序以及JDK 1.5或更高版本。最新版本的Netty程序可在項目下載頁 下載。下載正確版本的JDK,請到你偏好的JDK站點下載。 這就已經(jīng)足夠了嗎?實際上你會發(fā)現(xiàn),這兩個條件已經(jīng)足夠你完成任何協(xié)議的開發(fā)了。如果不是這樣,請聯(lián)系Netty項目社區(qū) ,讓我們知道還缺少了什么。 最終但不是至少,當你想了解本章所介紹的類的更多信息時請參考API手冊。為方便你的使用,這篇文檔中所有的類名均連接至在線API手冊。此外,如果本篇文檔中有任何錯誤信息,無論是語法錯誤,還是打印排版錯誤或者你有更好的建議,請不要顧慮,立即聯(lián)系Netty項目社區(qū) 。 1.2. 拋棄協(xié)議服務(wù) 在這個世界上最簡化的協(xié)議不是“Hello,world!”而是拋棄協(xié)議 。這是一種丟棄接收到的任何數(shù)據(jù)并不做任何回應(yīng)的協(xié)議。 實現(xiàn)拋棄協(xié)議(DISCARD protocol),你僅需要忽略接受到的任何數(shù)據(jù)即可。讓我們直接從處理器(handler)實現(xiàn)開始,這個處理器處理Netty的所有I/O事件。 Java代碼 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通道共享(同時關(guān)聯(lián)著ChannelPipeline)。DiscardServerHandler沒有處理任何有狀態(tài)的信息, 因此這里的注解是“all”。 2)DiscardServerHandler繼承了SimpleChannelHandler,這也是一個ChannelHandler 的實現(xiàn)。SimpleChannelHandler提供了多種你可以重寫的事件處理方法。目前直接繼承SimpleChannelHandler已經(jīng)足夠 了,并不需要你完成一個自己的處理器接口。 3)我們這里重寫了messageReceived事件處理方法。這個方法由一個接收了客戶端傳送數(shù)據(jù)的MessageEvent事件調(diào)用。在這個例子中,我們忽略接收到的任何數(shù)據(jù),并以此來實現(xiàn)一個拋棄協(xié)議(DISCARD protocol)。 4)exceptionCaught 事件處理方法由一個ExceptionEvent異常事件調(diào)用,這個異常事件起因于Netty的I/O異常或一個處理器實現(xiàn)的內(nèi)部異常。多數(shù)情況下,捕捉 到的異常應(yīng)當被記錄下來,并在這個方法中關(guān)閉這個channel通道。當然處理這種異常情況的方法實現(xiàn)可能因你的實際需求而有所不同,例如,在關(guān)閉這個連 接之前你可能會發(fā)送一個包含了錯誤碼的響應(yīng)消息。 目前進展不錯,我們已經(jīng)完成了拋棄協(xié)議服務(wù)器的一半開發(fā)工作。下面要做的是完成一個可以啟動這個包含DiscardServerHandler處理器服務(wù)的主方法。 Java代碼 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 是一個創(chuàng)建和管理Channel通道及其相關(guān)資源的工廠接口,它處理所有的I/O請求并產(chǎn)生相應(yīng)的I/O ChannelEvent通道事件。Netty 提供了多種 ChannelFactory 實現(xiàn)。這里我們需要實現(xiàn)一個服務(wù)端的例子,因此我們使用NioServerSocketChannelFactory實現(xiàn)。另一件需要注意的事情是這個工 廠并自己不負責創(chuàng)建I/O線程。你應(yīng)當在其構(gòu)造器中指定該工廠使用的線程池,這樣做的好處是你獲得了更高的控制力來管理你的應(yīng)用環(huán)境中使用的線程,例如一 個包含了安全管理的應(yīng)用服務(wù)。 2)ServerBootstrap 是一個設(shè)置服務(wù)的幫助類。你甚至可以在這個服務(wù)中直接設(shè)置一個Channel通道。然而請注意,這是一個繁瑣的過程,大多數(shù)情況下并不需要這樣做。 3)這里,我們將DiscardServerHandler處理器添加至默認的ChannelPipeline通道。任何時候當服務(wù)器接收到一個新的連 接,一個新的ChannelPipeline管道對象將被創(chuàng)建,并且所有在這里添加的ChannelHandler對象將被添加至這個新的 ChannelPipeline管道對象。這很像是一種淺拷貝操作(a shallow-copy operation);所有的Channel通道以及其對應(yīng)的ChannelPipeline實例將分享相同的DiscardServerHandler 實例。 4)你也可以設(shè)置我們在這里指定的這個通道實現(xiàn)的配置參數(shù)。我們正在寫的是一個TCP/IP服務(wù),因此我們運行設(shè)定一些socket選項,例如 tcpNoDelay和keepAlive。請注意我們在配置選項里添加的"child."前綴。這意味著這個配置項僅適用于我們接收到的通道實例,而不 是ServerSocketChannel實例。因此,你可以這樣給一個ServerSocketChannel設(shè)定參數(shù): bootstrap.setOption("reuseAddress", true); 5)我們繼續(xù)。剩下要做的是綁定這個服務(wù)使用的端口并且啟動這個服務(wù)。這里,我們綁定本機所有網(wǎng)卡(NICs,network interface cards)上的8080端口。當然,你現(xiàn)在也可以對應(yīng)不同的綁定地址多次調(diào)用綁定操作。 大功告成!現(xiàn)在你已經(jīng)完成你的第一個基于Netty的服務(wù)端程序。 1.3. 查看接收到的數(shù)據(jù) 現(xiàn)在你已經(jīng)完成了你的第一個服務(wù)端程序,我們需要測試它是否可以真正的工作。最簡單的方法是使用telnet 命令。例如,你可以在命令行中輸入“telnet localhost 8080 ”或其他類型參數(shù)。 然而,我們可以認為服務(wù)器在正常工作嗎?由于這是一個丟球協(xié)議服務(wù),所以實際上我們無法真正的知道。你最終將收不到任何回應(yīng)。為了證明它在真正的工作,讓我們修改代碼打印其接收到的數(shù)據(jù)。 我們已經(jīng)知道當完成數(shù)據(jù)的接收后將產(chǎn)生MessageEvent消息事件,并且也會觸發(fā)messageReceived處理方法。所以讓我在DiscardServerHandler處理器的messageReceived方法內(nèi)增加一些代碼。 Java代碼 @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的一個基本數(shù)據(jù)結(jié)構(gòu),這個數(shù) 據(jù)結(jié)構(gòu)存儲了一個字節(jié)序列。ChannelBuffer類似于NIO的ByteBuffer,但是前者卻更加的靈活和易于使用。例如,Netty允許你創(chuàng) 建一個由多個ChannelBuffer構(gòu)建的復(fù)合ChannelBuffer類型,這樣就可以減少不必要的內(nèi)存拷貝次數(shù)。 2) 雖然ChannelBuffer有些類似于NIO的ByteBuffer,但強烈建議你參考Netty的API手冊。學(xué)會如何正確的使用ChannelBuffer是無障礙使用Netty的關(guān)鍵一步。 如果你再次運行telnet命令,你將會看到你所接收到的數(shù)據(jù)。 拋棄協(xié)議服務(wù)的所有源代碼均存放在在分發(fā)版的org.jboss.netty.example.discard包下。 1.4. 響應(yīng)協(xié)議服務(wù) 目前,我們雖然使用了數(shù)據(jù),但最終卻未作任何回應(yīng)。然而一般情況下,一個服務(wù)都需要回應(yīng)一個請求。讓我們實現(xiàn)ECHO協(xié)議 來學(xué)習如何完成一個客戶請求的回應(yīng)消息,ECHO協(xié)議規(guī)定要返回任何接收到的數(shù)據(jù)。 與我們上一節(jié)實現(xiàn)的拋棄協(xié)議服務(wù)唯一不同的地方是,這里需要返回所有的接收數(shù)據(jù)而不是僅僅打印在控制臺之上。因此我們再次修改messageReceived方法就足夠了。 Java代碼 @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { Channel ch = e.getChannel(); ch.write(e.getMessage()); } 代碼說明 1) 一個ChannelEvent通道事件對象自身存有一個和其關(guān)聯(lián)的Channel對象引用。這個返回的Channel通道對象代表了這個接收 MessageEvent消息事件的連接(connection)。因此,我們可以通過調(diào)用這個Channel通道對象的write方法向遠程節(jié)點寫入返 回數(shù)據(jù)。 現(xiàn)在如果你再次運行telnet命令,你將會看到服務(wù)器返回的你所發(fā)送的任何數(shù)據(jù)。 相應(yīng)服務(wù)的所有源代碼存放在分發(fā)版的org.jboss.netty.example.echo包下。 1.5. 時間協(xié)議服務(wù) 這一節(jié)需要實現(xiàn)的協(xié)議是TIME協(xié)議 。這是一個與先前所介紹的不同的例子。這個例子里,服務(wù)端返回一個32位的整數(shù)消息,我們不接受請求中包含的任何數(shù)據(jù)并且當消息返回完畢后立即關(guān)閉連接。通過這個例子你將學(xué)會如何構(gòu)建和發(fā)送消息,以及當完成處理后如何主動關(guān)閉連接。 因為我們會忽略接收到的任何數(shù)據(jù)而只是返回消息,這應(yīng)當在建立連接后就立即開始。因此這次我們不再使用messageReceived方法,取而代之的是使用channelConnected方法。下面是具體的實現(xiàn): Java代碼 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方法將在一個連接建立后立即觸發(fā)。因此讓我們在這個方法里完成一個代表當前時間(秒)的32位整數(shù)消息的構(gòu)建工作。 2) 為了發(fā)送一個消息,我們需要分配一個包含了這個消息的buffer緩沖。因為我們將要寫入一個32位的整數(shù),因此我們需要一個4字節(jié)的 ChannelBuffer。ChannelBuffers是一個可以創(chuàng)建buffer緩沖的幫助類。除了這個buffer方 法,ChannelBuffers還提供了很多和ChannelBuffer相關(guān)的實用方法。更多信息請參考API手冊。 另外,一個很不錯的方法是使用靜態(tài)的導(dǎo)入方式: import static org.jboss.netty.buffer.ChannelBuffers.*; ... ChannelBuffer dynamicBuf = dynamicBuffer(256); ChannelBuffer ordinaryBuf = buffer(1024); 3) 像通常一樣,我們需要自己構(gòu)造消息。 但是打住,flip在哪?過去我們在使用NIO發(fā)送消息時不是常常需要調(diào)用 ByteBuffer.flip()方法嗎?實際上ChannelBuffer之所以不需要這個方法是因為 ChannelBuffer有兩個指針;一個對應(yīng)讀操作,一個對應(yīng)寫操作。當你向一個 ChannelBuffer寫入數(shù)據(jù)的時候?qū)懼羔樀乃饕当銜黾樱c此同時讀指針的索引值不會有任何變化。讀寫指針的索引值分別代表了這個消息的開 始、結(jié)束位置。 與之相應(yīng)的是,NIO的buffer緩沖沒有為我們提供如此簡潔的一種方法,除非你調(diào)用它的flip方法。因此,當你忘記調(diào)用flip方法而引起發(fā)送錯誤 時,你便會陷入困境。這樣的錯誤不會再Netty中發(fā)生,因為我們對應(yīng)不同的操作類型有不同的指針。你會發(fā)現(xiàn)就像你已習慣的這樣過程變得更加容易—一種沒 有flippling的體驗! 另一點需要注意的是這個寫方法返回了一個ChannelFuture對象。一個ChannelFuture 對象代表了一個尚未發(fā)生的I/O操作。這意味著,任何已請求的操作都可能是沒有被立即執(zhí)行的,因為在Netty內(nèi)部所有的操作都是異步的。例如,下面的代 碼可能會關(guān)閉一 個連接,這個操作甚至會發(fā)生在消息發(fā)送之前: Channel ch = ...; ch.write(message); ch.close(); 因此,你需要這個write方法返回的ChannelFuture對象,close方法需要等待寫操作異步完成之后的ChannelFuture通知/監(jiān)聽觸發(fā)。需要注意的是,關(guān)閉方法仍舊不是立即關(guān)閉一個連接,它同樣也是返回了一個ChannelFuture對象。 4) 在寫操作完成之后我們又如何得到通知?這個只需要簡單的為這個返回的ChannelFuture對象增加一個ChannelFutureListener 即可。在這里我們創(chuàng)建了一個匿名ChannelFutureListener對象,在這個ChannelFutureListener對象內(nèi)部我們處理了 異步操作完成之后的關(guān)閉操作。 另外,你也可以通過使用一個預(yù)定義的監(jiān)聽類來簡化代碼。 f.addListener(ChannelFutureListener.CLOSE); 1.6. 時間協(xié)議服務(wù)客戶端 不同于DISCARD和ECHO協(xié)議服務(wù),我們需要一個時間協(xié)議服務(wù)的客戶端,因為人們無法直接將一個32位的二進制數(shù)據(jù)轉(zhuǎn)換一個日歷時間。在這一節(jié)我們將學(xué)習如何確保服務(wù)器端工作正常,以及如何使用Netty完成客戶端的開發(fā)。 使用Netty開發(fā)服務(wù)器端和客戶端代碼最大的不同是要求使用不同的Bootstrap及ChannelFactory。請參照以下的代碼: Java代碼 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來創(chuàng)建客戶端的Channel通道對象。 2) 客戶端的ClientBootstrap對應(yīng)ServerBootstrap。 3) 請注意,這里不存在使用“child.”前綴的配置項,客戶端的SocketChannel實例不存在父級Channel對象。 4) 我們應(yīng)當調(diào)用connect連接方法,而不是之前的bind綁定方法。 正如你所看到的,這與服務(wù)端的啟動過程是完全不一樣的。ChannelHandler又該如何實現(xiàn)呢?它應(yīng)當負責接收一個32位的整數(shù),將其轉(zhuǎn)換為可讀的格式后,打印輸出時間,并關(guān)閉這個連接。 Java代碼 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(); } } 這看起來很是簡單,與服務(wù)端的實現(xiàn)也并未有什么不同。然而,這個處理器卻時常會因為拋出IndexOutOfBoundsException異常而拒絕工作。我們將在下一節(jié)討論這個問題產(chǎn)生的原因。 1.7. 流數(shù)據(jù)的傳輸處理 1.7.1. Socket Buffer的缺陷 對于例如TCP/IP這種基于流的傳輸協(xié)議實現(xiàn),接收到的數(shù)據(jù)會被存儲在socket的接受緩沖區(qū)內(nèi)。不幸的是,這種基于流的傳輸緩沖區(qū)并不是一個包隊 列,而是一個字節(jié)隊列。這意味著,即使你以兩個數(shù)據(jù)包的形式發(fā)送了兩條消息,操作系統(tǒng)卻不會把它們看成是兩條消息,而僅僅是一個批次的字節(jié)序列。因此,在 這種情況下我們就無法保證收到的數(shù)據(jù)恰好就是遠程節(jié)點所發(fā)送的數(shù)據(jù)。例如,讓我們假設(shè)一個操作系統(tǒng)的TCP/IP堆棧收到了三個數(shù)據(jù)包: +-----+-----+-----+ | ABC | DEF | GHI | +-----+-----+-----+ 由于這種流傳輸協(xié)議的普遍性質(zhì),在你的應(yīng)用中有較高的可能會把這些數(shù)據(jù)讀取為另外一種形式: +----+-------+---+---+ | AB | CDEFG | H | I | +----+-------+---+---+ 因此對于數(shù)據(jù)的接收方,不管是服務(wù)端還是客戶端,應(yīng)當重構(gòu)這些接收到的數(shù)據(jù),讓其變成一種可讓你的應(yīng)用邏輯易于理解的更有意義的數(shù)據(jù)結(jié)構(gòu)。在上面所述的這個例子中,接收到的數(shù)據(jù)應(yīng)當重構(gòu)為下面的形式: +-----+-----+-----+ | ABC | DEF | GHI | +-----+-----+-----+ 1.7.2. 第一種方案 現(xiàn)在讓我們回到時間協(xié)議服務(wù)客戶端的例子中。我們在這里遇到了同樣的問題。一個32位的整數(shù)是一個非常小的數(shù)據(jù)量,因此它常常不會被切分在不同的數(shù)據(jù)段內(nèi)。然而,問題是它確實可以被切分在不同的數(shù)據(jù)段內(nèi),并且這種可能性隨著流量的增加而提高。 最簡單的方案是在程序內(nèi)部創(chuàng)建一個可準確接收4字節(jié)數(shù)據(jù)的累積性緩沖。下面的代碼是修復(fù)了這個問題后的TimeClientHandler實現(xiàn)。 Java代碼 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不在不 在內(nèi)部保持一個buffer緩沖,因此這個TimeClientHandler實例不可以再被多個Channel通道或ChannelPipeline共 享。否則這個內(nèi)部的buffer緩沖將無法緩沖正確的數(shù)據(jù)內(nèi)容。 2) 動態(tài)的buffer緩沖也是ChannelBuffer的一種實現(xiàn),其擁有動態(tài)增加緩沖容量的能力。當你無法預(yù)估消息的數(shù)據(jù)長度時,動態(tài)的buffer緩沖是一種很有用的緩沖結(jié)構(gòu)。 3) 首先,所有的數(shù)據(jù)將會被累積的緩沖至buf容器。 4) 之后,這個處理器將會檢查是否收到了足夠的數(shù)據(jù)然后再進行真實的業(yè)務(wù)邏輯處理,在這個例子中需要接收4字節(jié)數(shù)據(jù)。否則,Netty將重復(fù)調(diào)用messageReceived方法,直至4字節(jié)數(shù)據(jù)接收完成。 這里還有另一個地方需要進行修改。你是否還記得我們把TimeClientHandler實例添加到了這個ClientBootstrap實例的默 認ChannelPipeline管道里?這意味著同一個TimeClientHandler實例將被多個Channel通道共享,因此接受的數(shù)據(jù)也將受 到破壞。為了給每一個Channel通道創(chuàng)建一個新的TimeClientHandler實例,我們需要實現(xiàn)一個 ChannelPipelineFactory管道工廠: Java代碼 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; } } 現(xiàn)在,我們需要把TimeClient下面的代碼片段: Java代碼 TimeClientHandler handler = new TimeClientHandler(); bootstrap.getPipeline().addLast("handler", handler); 替換為: Java代碼 bootstrap.setPipelineFactory(new TimeClientPipelineFactory()); 雖然這看上去有些復(fù)雜,并且由于在TimeClient內(nèi)部我們只創(chuàng)建了一個連接(connection),因此我們在這里確實沒必要引入TimeClientPipelineFactory實例。 然而,當你的應(yīng)用變得越來越復(fù)雜,你就總會需要實現(xiàn)自己的ChannelPipelineFactory,這個管道工廠將會令你的管道配置變得更加具有靈活性。 1.7.3. 第二種方案 雖然第二種方案解決了時間協(xié)議客戶端遇到的問題,但是這個修改后的處理器實現(xiàn)看上去卻不再那么簡潔。設(shè)想一種更為復(fù)雜的,由多個可變長度字段組成的協(xié)議。你的ChannelHandler實現(xiàn)將變得越來越難以維護。 正如你已注意到的,你可以為一個ChannelPipeline添加多個ChannelHandler,因此,為了減小應(yīng)用的復(fù)雜性,你可以把這個臃腫的 ChannelHandler切分為多個獨立的模塊單元。例如,你可以把TimeClientHandler切分為兩個獨立的處理器: TimeDecoder,解決數(shù)據(jù)分段的問題。 TimeClientHandler,原始版本的實現(xiàn)。 幸運的是,Netty提供了一個可擴展的類,這個類可以直接拿過來使用幫你完成TimeDecoder的開發(fā): Java代碼 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) 當接收到新的數(shù)據(jù)后,F(xiàn)rameDecoder會調(diào)用decode方法,同時傳入一個FrameDecoder內(nèi)部持有的累積型buffer緩沖。 3) 如果decode返回null值,這意味著還沒有接收到足夠的數(shù)據(jù)。當有足夠數(shù)量的數(shù)據(jù)后FrameDecoder會再次調(diào)用decode方法。 4) 如果decode方法返回一個非空值,這意味著decode方法已經(jīng)成功完成一條信息的解碼。FrameDecoder將丟棄這個內(nèi)部的累計型緩沖。請注 意你不需要對多條消息進行解碼,F(xiàn)rameDecoder將保持對decode方法的調(diào)用,直到decode方法返回非空對象。 如果你是一個勇于嘗試的人,你或許應(yīng)當使用ReplayingDecoder,ReplayingDecoder更加簡化了解碼的過程。為此你需要查看API手冊獲得更多的幫助信息。 Java代碼 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實現(xiàn),這些decoder實現(xiàn)不僅可以讓你非常容易的實現(xiàn)大多數(shù)協(xié)議,并且還會幫你避免某些臃腫、難以維護的處理器實現(xiàn)。請參考下面的代碼包獲得更加詳細的實例: 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做為協(xié)議消息的原始數(shù)據(jù)結(jié)構(gòu)。在這一節(jié),我們將改進時間協(xié)議服務(wù)的客戶/服務(wù)端實現(xiàn),使用POJO 而不是ChannelBuffer做為協(xié)議消息的原始數(shù)據(jù)結(jié)構(gòu)。 在你的ChannelHandler實現(xiàn)中使用POJO的優(yōu)勢是很明顯的;從你的ChannelHandler實現(xiàn)中分離從ChannelBuffer獲 取數(shù)據(jù)的代碼,將有助于提高你的ChannelHandler實現(xiàn)的可維護性和可重用性。在時間協(xié)議服務(wù)的客戶/服務(wù)端代碼中,直接使用 ChannelBuffer讀取一個32位的整數(shù)并不是一個主要的問題。然而,你會發(fā)現(xiàn),當你試圖實現(xiàn)一個真實的協(xié)議的時候,這種代碼上的分離是很有必要 的。 首先,讓我們定義一個稱之為UnixTime的新類型。 Java代碼 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(); } } 現(xiàn)在讓我們重新修改TimeDecoder實現(xiàn),讓其返回一個UnixTime,而不是一個ChannelBuffer。 Java代碼 @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對象轉(zhuǎn)換 為UnixTime對象的ChannelHandler實現(xiàn)。 有了這個修改后的decoder實現(xiàn),這個TimeClientHandler便不會再依賴ChannelBuffer。 Java代碼 @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { UnixTime m = (UnixTime) e.getMessage(); System.out.println(m); e.getChannel().close(); } 更加簡單優(yōu)雅了,不是嗎?同樣的技巧也可以應(yīng)用在服務(wù)端,讓我們現(xiàn)在更新TimeServerHandler的實現(xiàn): Java代碼 @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); } 現(xiàn)在剩下的唯一需要修改的部分是這個ChannelHandler實現(xiàn),這個ChannelHandler實現(xiàn)需要把一個UnixTime對象重新 轉(zhuǎn)換為一個ChannelBuffer。但這卻已是相當簡單了,因為當你對消息進行編碼的時候你不再需要處理數(shù)據(jù)包的拆分及組裝。 Java代碼 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是無狀態(tài)的,所以其使用的ChannelPipelineCoverage注解值是“all”。實際上,大多數(shù)encoder實現(xiàn)都是無狀態(tài)的。 2) 一個encoder通過重寫writeRequested方法來實現(xiàn)對寫操作請求的攔截。不過請注意雖然這個writeRequested方法使用了和 messageReceived方法一樣的MessageEvent參數(shù),但是它們卻分別對應(yīng)了不同的解釋。一個ChannelEvent事件可以既是一 個上升流事件(upstream event)也可以是一個下降流事件(downstream event),這取決于事件流的方向。例如:一個MessageEvent消息事件可以作為一個上升流事件(upstream event)被messageReceived方法調(diào)用,也可以作為一個下降流事件(downstream event)被writeRequested方法調(diào)用。請參考API手冊獲得上升流事件(upstream event)和下降流事件(downstream event)的更多信息。 3) 一旦完成了POJO和ChannelBuffer轉(zhuǎn)換,你應(yīng)當確保把這個新的buffer緩沖轉(zhuǎn)發(fā)至先前的 ChannelDownstreamHandler處理,這個下降通道的處理器由某個ChannelPipeline管理。Channels提供了多個可 以創(chuàng)建和發(fā)送ChannelEvent事件的幫助方法。在這個例子中,Channels.write(...)方法創(chuàng)建了一個新的 MessageEvent事件,并把這個事件發(fā)送給了先前的處于某個ChannelPipeline內(nèi)的 ChannelDownstreamHandler處理器。 另外,一個很不錯的方法是使用靜態(tài)的方式導(dǎo)入Channels類: import static org.jboss.netty.channel.Channels.*; ... ChannelPipeline pipeline = pipeline(); write(ctx, e.getFuture(), buf); fireChannelDisconnected(ctx); 最后的任務(wù)是把這個TimeEncoder插入服務(wù)端的ChannelPipeline,這是一個很簡單的步驟。 1.9. 關(guān)閉你的應(yīng)用 如果你運行了TimeClient,你肯定可以注意到,這個應(yīng)用并沒有自動退出而只是在那里保持著無意義的運行。跟蹤堆棧記錄你可以發(fā)現(xiàn),這里有一些運行 狀態(tài)的I/O線程。為了關(guān)閉這些I/O線程并讓應(yīng)用優(yōu)雅的退出,你需要釋放這些由ChannelFactory分配的資源。 一個典型的網(wǎng)絡(luò)應(yīng)用的關(guān)閉過程由以下三步組成: 關(guān)閉負責接收所有請求的server socket。 關(guān)閉所有客戶端socket或服務(wù)端為響應(yīng)某個請求而創(chuàng)建的socket。 釋放ChannelFactory使用的所有資源。 為了讓TimeClient執(zhí)行這三步,你需要在TimeClient.main()方法內(nèi)關(guān)閉唯一的客戶連接以及ChannelFactory使用的所有資源,這樣做便可以優(yōu)雅的關(guān)閉這個應(yīng)用。 Java代碼 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對象將告知這個連接操 作的成功或失敗狀態(tài)。同時這個ChannelFuture對象也保存了一個代表這個連接操作的Channel對象引用。 2) 阻塞式的等待,直到ChannelFuture對象返回這個連接操作的成功或失敗狀態(tài)。 3) 如果連接失敗,我們將打印連接失敗的原因。如果連接操作沒有成功或者被取消,ChannelFuture對象的getCause()方法將返回連接失敗的原因。 4) 現(xiàn)在,連接操作結(jié)束,我們需要等待并且一直到這個Channel通道返回的closeFuture關(guān)閉這個連接。每一個Channel都可獲得自己的closeFuture對象,因此我們可以收到通知并在這個關(guān)閉時間點執(zhí)行某種操作。 并且即使這個連接操作失敗,這個closeFuture仍舊會收到通知,因為這個代表連接的 Channel對象將會在連接操作失敗后自動關(guān)閉。 5) 在這個時間點,所有的連接已被關(guān)閉。剩下的唯一工作是釋放ChannelFactory通道工廠使用的資源。這一步僅需要調(diào)用 releaseExternalResources()方法即可。包括NIO Secector和線程池在內(nèi)的所有資源將被自動的關(guān)閉和終止。 關(guān)閉一個客戶端應(yīng)用是很簡單的,但又該如何關(guān)閉一個服務(wù)端應(yīng)用呢?你需要釋放其綁定的端口并關(guān)閉所有接受和打開的連接。為了做到這一點,你需要使用一種數(shù)據(jù)結(jié)構(gòu)記錄所有的活動連接,但這卻并不是一件容易的事。幸運的是,這里有一種解決方案,ChannelGroup。 ChannelGroup是Java 集合 API的一個特有擴展,ChannelGroup內(nèi)部持有所有打開狀態(tài)的Channel通道。如果一個Channel通道對象被加入到 ChannelGroup,如果這個Channel通道被關(guān)閉,ChannelGroup將自動移除這個關(guān)閉的Channel通道對象。此外,你還可以對 一個ChannelGroup對象內(nèi)部的所有Channel通道對象執(zhí)行相同的操作。例如,當你關(guān)閉服務(wù)端應(yīng)用時你可以關(guān)閉一個ChannelGroup 內(nèi)部的所有Channel通道對象。 為了記錄所有打開的socket,你需要修改你的TimeServerHandler實現(xiàn),將一個打開的Channel通道加入全局的ChannelGroup對象,TimeServer.allChannels: Java代碼 @Override public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) { TimeServer.allChannels.add(e.getChannel()); } 代碼說明 是的,ChannelGroup是線程安全的。 現(xiàn)在,所有活動的Channel通道將被自動的維護,關(guān)閉一個服務(wù)端應(yīng)用有如關(guān)閉一個客戶端應(yīng)用一樣簡單。 Java代碼 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需要一個組名作為其構(gòu)造器參數(shù)。這個組名僅是區(qū)分每個ChannelGroup的一個標示。 2) ServerBootstrap對象的bind方法返回了一個綁定了本地地址的服務(wù)端Channel通道對象。調(diào)用這個Channel通道的close()方法將釋放這個Channel通道綁定的本地地址。 3) 不管這個Channel對象屬于服務(wù)端,客戶端,還是為響應(yīng)某一個請求創(chuàng)建,任何一種類型的Channel對象都會被加入ChannelGroup。因此,你盡可在關(guān)閉服務(wù)時關(guān)閉所有的Channel對象。 4) waitForShutdownCommand()是一個想象中等待關(guān)閉信號的方法。你可以在這里等待某個客戶端的關(guān)閉信號或者JVM的關(guān)閉回調(diào)命令。 5) 你可以對ChannelGroup管理的所有Channel對象執(zhí)行相同的操作。在這個例子里,我們將關(guān)閉所有的通道,這意味著綁定在服務(wù)端特定地址的 Channel通道將解除綁定,所有已建立的連接也將異步關(guān)閉。為了獲得成功關(guān)閉所有連接的通知,close()方法將返回一個 ChannelGroupFuture對象,這是一個類似ChannelFuture的對象。 1.10. 總述 在這一章節(jié),我們快速瀏覽并示范了如何使用Netty開發(fā)網(wǎng)絡(luò)應(yīng)用。下一章節(jié)將涉及更多的問題。同時請記住,為了幫助你以及能夠讓Netty基于你的回饋得到持續(xù)的改進和提高,Netty社區(qū) 將永遠歡迎你的問題及建議。 第二章. 架構(gòu)總覽 在這個章節(jié),我們將闡述Netty提供的核心功能以及在此基礎(chǔ)之上如何構(gòu)建一個完備的網(wǎng)絡(luò)應(yīng)用。 2.1. 豐富的緩沖實現(xiàn) Netty使用自建的buffer API,而不是使用NIO的ByteBuffer來代表一個連續(xù)的字節(jié)序列。與ByteBuffer相比這種方式擁有明顯的優(yōu)勢。Netty使用新的 buffer類型ChannelBuffer,ChannelBuffer被設(shè)計為一個可從底層解決ByteBuffer問題,并可滿足日常網(wǎng)絡(luò)應(yīng)用開發(fā) 需要的緩沖類型。這些很酷的特性包括: 如果需要,允許使用自定義的緩沖類型。 復(fù)合緩沖類型中內(nèi)置的透明的零拷貝實現(xiàn)。 開箱即用的動態(tài)緩沖類型,具有像StringBuffer一樣的動態(tài)緩沖能力。 不再需要調(diào)用的flip()方法。 正常情況下具有比ByteBuffer更快的響應(yīng)速度。 更多信息請參考:org.jboss.netty.buffer package description 2.2. 統(tǒng)一的異步 I/O API 傳統(tǒng)的Java I/O API在應(yīng)對不同的傳輸協(xié)議時需要使用不同的類型和方法。例如:java.net.Socket 和 java.net.DatagramSocket它們并不具有相同的超類型,因此,這就需要使用不同的調(diào)用方式執(zhí)行socket操作。 這種模式上的不匹配使得在更換一個網(wǎng)絡(luò)應(yīng)用的傳輸協(xié)議時變得繁雜和困難。由于(Java I/O API)缺乏協(xié)議間的移植性,當你試圖在不修改網(wǎng)絡(luò)傳輸層的前提下增加多種協(xié)議的支持,這時便會產(chǎn)生問題。并且理論上講,多種應(yīng)用層協(xié)議可運行在多種傳輸 層協(xié)議之上例如TCP/IP,UDP/IP,SCTP和串口通信。 讓這種情況變得更糟的是,Java新的I/O(NIO)API與原有的阻塞式的I/O(OIO)API并不兼容,NIO.2(AIO)也是如此。由于所有的API無論是在其設(shè)計上還是性能上的特性都與彼此不同,在進入開發(fā)階段,你常常會被迫的選擇一種你需要的API。 例如,在用戶數(shù)較小的時候你可能會選擇使用傳統(tǒng)的OIO(Old I/O) API,畢竟與NIO相比使用OIO將更加容易一些。然而,當你的業(yè)務(wù)呈指數(shù)增長并且服務(wù)器需要同時處理成千上萬的客戶連接時你便會遇到問題。這種情況下 你可能會嘗試使用NIO,但是復(fù)雜的NIO Selector編程接口又會耗費你大量時間并最終會阻礙你的快速開發(fā)。 Netty有一個叫做Channel的統(tǒng)一的異步I/O編程接口,這個編程接口抽象了所有點對點的通信操作。也就是說,如果你的應(yīng)用是基于Netty的某 一種傳輸實現(xiàn),那么同樣的,你的應(yīng)用也可以運行在Netty的另一種傳輸實現(xiàn)上。Netty提供了幾種擁有相同編程接口的基本傳輸實現(xiàn): NIO-based TCP/IP transport (See org.jboss.netty.channel.socket.nio), OIO-based TCP/IP transport (See org.jboss.netty.channel.socket.oio), OIO-based UDP/IP transport, and Local transport (See org.jboss.netty.channel.local). 切換不同的傳輸實現(xiàn)通常只需對代碼進行幾行的修改調(diào)整,例如選擇一個不同的ChannelFactory實現(xiàn)。 此外,你甚至可以利用新的傳輸實現(xiàn)沒有寫入的優(yōu)勢,只需替換一些構(gòu)造器的調(diào)用方法即可,例如串口通信。而且由于核心API具有高度的可擴展性,你還可以完成自己的傳輸實現(xiàn)。 2.3. 基于攔截鏈模式的事件模型 一個定義良好并具有擴展能力的事件模型是事件驅(qū)動開發(fā)的必要條件。Netty具有定義良好的I/O事件模型。由于嚴格的層次結(jié)構(gòu)區(qū)分了不同的事件類型,因 此Netty也允許你在不破壞現(xiàn)有代碼的情況下實現(xiàn)自己的事件類型。這是與其他框架相比另一個不同的地方。很多NIO框架沒有或者僅有有限的事件模型概 念;在你試圖添加一個新的事件類型的時候常常需要修改已有的代碼,或者根本就不允許你進行這種擴展。 在一個ChannelPipeline內(nèi)部一個ChannelEvent被一組ChannelHandler處理。這個管道是攔截過濾器 模式的一種高級形式的實現(xiàn),因此對于一個事件如何被處理以及管道內(nèi)部處理器間的交互過程,你都將擁有絕對的控制力。例如,你可以定義一個從socket讀取到數(shù)據(jù)后的操作: Java代碼 public class MyReadHandler implements SimpleChannelHandler { public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) { Object message = evt.getMessage(); // Do something with the received message. ... // And forward the event to the next handler. ctx.sendUpstream(evt); } } 同時你也可以定義一種操作響應(yīng)其他處理器的寫操作請求: Java代碼 public class MyWriteHandler implements SimpleChannelHandler { public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) { Object message = evt.getMessage(); // Do something with the message to be written. ... // And forward the event to the next handler. ctx.sendDownstream(evt); } } 有關(guān)事件模型的更多信息,請參考API文檔ChannelEvent和ChannelPipeline部分。 2.4. 適用快速開發(fā)的高級組件 上述所提及的核心組件已經(jīng)足夠?qū)崿F(xiàn)各種類型的網(wǎng)絡(luò)應(yīng)用,除此之外,Netty也提供了一系列的高級組件來加速你的開發(fā)過程。 2.4.1. Codec框架 就像“1.8. 使用POJO代替ChannelBuffer”一節(jié)所展示的那樣,從業(yè)務(wù)邏輯代碼中分離協(xié)議處理部分總是一個很不錯的想法。然而如果一切從零開始便會遭遇 到實現(xiàn)上的復(fù)雜性。你不得不處理分段的消息。一些協(xié)議是多層的(例如構(gòu)建在其他低層協(xié)議之上的協(xié)議)。一些協(xié)議過于復(fù)雜以致難以在一臺主機(single state machine)上實現(xiàn)。 因此,一個好的網(wǎng)絡(luò)應(yīng)用框架應(yīng)該提供一種可擴展,可重用,可單元測試并且是多層的codec框架,為用戶提供易維護的codec代碼。 Netty提供了一組構(gòu)建在其核心模塊之上的codec實現(xiàn),這些簡單的或者高級的codec實現(xiàn)幫你解決了大部分在你進行協(xié)議處理開發(fā)過程會遇到的問題,無論這些協(xié)議是簡單的還是復(fù)雜的,二進制的或是簡單文本的。 2.4.2. SSL / TLS 支持 不同于傳統(tǒng)阻塞式的I/O實現(xiàn),在NIO模式下支持SSL功能是一個艱難的工作。你不能只是簡單的包裝一下流數(shù)據(jù)并進行加密或解密工作,你不得不借助于 javax.net.ssl.SSLEngine,SSLEngine是一個有狀態(tài)的實現(xiàn),其復(fù)雜性不亞于SSL自身。你必須管理所有可能的狀態(tài),例如密 碼套件,密鑰協(xié)商(或重新協(xié)商),證書交換以及認證等。此外,與通常期望情況相反的是SSLEngine甚至不是一個絕對的線程安全實現(xiàn)。 在Netty內(nèi)部,SslHandler封裝了所有艱難的細節(jié)以及使用SSLEngine可能帶來的陷阱。你所做的僅是配置并將該SslHandler插入到你的ChannelPipeline中。同樣Netty也允許你實現(xiàn)像StartTlS 那樣所擁有的高級特性,這很容易。 2.4.3. HTTP實現(xiàn) HTTP無疑是互聯(lián)網(wǎng)上最受歡迎的協(xié)議,并且已經(jīng)有了一些例如Servlet容器這樣的HTTP實現(xiàn)。因此,為什么Netty還要在其核心模塊之上構(gòu)建一套HTTP實現(xiàn)? 與現(xiàn)有的HTTP實現(xiàn)相比Netty的HTTP實現(xiàn)是相當與眾不同的。在HTTP消息的低層交互過程中你將擁有絕對的控制力。這是因為Netty的 HTTP實現(xiàn)只是一些HTTP codec和HTTP消息類的簡單組合,這里不存在任何限制——例如那種被迫選擇的線程模型。你可以隨心所欲的編寫那種可以完全按照你期望的工作方式工作 的客戶端或服務(wù)器端代碼。這包括線程模型,連接生命期,快編碼,以及所有HTTP協(xié)議允許你做的,所有的一切,你都將擁有絕對的控制力。 由于這種高度可定制化的特性,你可以開發(fā)一個非常高效的HTTP服務(wù)器,例如: 要求持久化鏈接以及服務(wù)器端推送技術(shù)的聊天服務(wù)(e.g. Comet ) 需要保持鏈接直至整個文件下載完成的媒體流服務(wù)(e.g. 2小時長的電影) 需要上傳大文件并且沒有內(nèi)存壓力的文件服務(wù)(e.g. 上傳1GB文件的請求) 支持大規(guī)模mash-up應(yīng)用以及數(shù)以萬計連接的第三方web services異步處理平臺 2.4.4. Google Protocol Buffer 整合 Google Protocol Buffers 是快速實現(xiàn)一個高效的二進制協(xié)議的理想方案。通過使用ProtobufEncoder和ProtobufDecoder,你可以把Google Protocol Buffers 編譯器 (protoc)生成的消息類放入到Netty的codec實現(xiàn)中。請參考“LocalTime ”實例,這個例子也同時顯示出開發(fā)一個由簡單協(xié)議定義 的客戶及服務(wù)端是多么的容易。 2.5. 總述 在這一章節(jié),我們從功能特性的角度回顧了Netty的整體架構(gòu)。Netty有一個簡單卻不失強大的架構(gòu)。這個架構(gòu)由三部分組成——緩沖(buffer), 通道(channel),事件模型(event model)——所有的高級特性都構(gòu)建在這三個核心組件之上。一旦你理解了它們之間的工作原理,你便不難理解在本章簡要提及的更多高級特性。 你可能對Netty的整體架構(gòu)以及每一部分的工作原理仍舊存有疑問。如果是這樣,最好的方式是告訴我們 應(yīng)該如何改進這份指南
評論:
|
![]() |
|
Copyright © 不高興 | Powered by: 博客園 模板提供:滬江博客 |