【JAVA教程】Netty代碼分析

   Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序[官方定義],總體來看其包含了如下內容:1.提供了豐富的協議編解碼支持,2.實現自有的buffer系統,減小複製所帶來的消耗,3.整套channel的實現,4.基於事件的過程流轉以及完整的網絡事件響應與擴展,5.豐富的example。本文並不對Netty實際使用中可能出現的問題作分析,只是從代碼角度分析它的架構以及實現上的一些關鍵細節。java

首先來看下最如何使用Netty(其自帶example很好展現了使用),Netty普通使用通常是經過BootStrap來啓動,BootStrap主要分爲兩類:1.面向鏈接(TCP)的BootStrap(ClientBootStrap和ServerBootstrap),2.非面向鏈接(UDP) 的(ConnectionlessBootstrap)。bootstrap

Netty總體架構很清晰的分紅2個部分,ChannelFactory 和ChannelPipelineFactory,前者主要生產網絡通訊相關的Channel實例和ChannelSink實例,Netty提供的 ChannelFactory實現基本可以知足絕大部分用戶的需求,固然你也能夠定製本身的ChannelFactory,後者主要關注於具體傳輸數據的處理,同時也包括其餘方面的內容,好比異常處理等等,只要是你但願的,你均可以往裏添加相應的handler,通常 ChannelPipelineFactory由用戶本身實現,由於傳輸數據的處理及其餘操做和業務關聯比較緊密,須要自定義處理的handler。api

如今,使用Netty的步驟實際上已經很是明確了,好比面向鏈接的Netty服務端客戶端使用,第一步:實例化一個BootStrap,而且經過構造方法指定一個ChannelFactory實現,第二步:向bootstrap實例註冊一個本身實現的ChannelPipelineFactory,第三步:若是是服務器端,bootstrap.bind(new InetSocketAddress(port)),而後等待客戶端來鏈接,若是是客戶端,bootstrap.connect(new InetSocketAddress(host,port))取得一個future,這個時候Netty會去鏈接遠程主機,在鏈接完成後,會發起類型爲 CONNECTED的ChannelStateEvent,而且開始在你自定義的Pipeline裏面流轉,若是你註冊的handler有這個事件的響應方法的話那麼就會調用到這個方法。在此以後就是數據的傳輸了。下面是一個簡單客戶端的代碼解讀。
[java]
// 實例化一個客戶端Bootstrap實例,其中NioClientSocketChannelFactory實例由Netty提供
ClientBootstrap bootstrap = new ClientBootstrap(
new NioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));服務器

// 設置PipelineFactory,由客戶端本身實現
bootstrap.setPipelineFactory(new FactorialClientPipelineFactory(count));網絡

//向目標地址發起一個鏈接
ChannelFuture connectFuture =
bootstrap.connect(new InetSocketAddress(host, port));多線程

// 等待連接成功,成功後發起的connected事件將會使handler開始發送信息而且等待messageRecive,固然這只是示例。
Channel channel = connectFuture.awaitUninterruptibly().getChannel();架構

// 獲得用戶自定義的handler
FactorialClientHandler handler =
(FactorialClientHandler) channel.getPipeline().getLast();app

// 從handler裏面取數據而且打印,這裏須要注意的是,handler.getFactorial使用了從結果隊列result take數據的阻塞方法,而結果隊列會在messageRecieve事件發生時被填充接收回來的數據
System.err.format(
"Factorial of %,d is: %,d", count, handler.getFactorial());
[/java]
Netty提供了NIO與BIO(OIO)兩種模式處理這些邏輯,其中NIO主要經過一個BOSS線程處理等待連接的接入,若干個WORKER線程(從worker線程池中挑選一個賦給Channel實例,由於Channel實例持有真正的 java網絡對象)接過BOSS線程遞交過來的CHANNEL進行數據讀寫而且觸發相應事件傳遞給pipeline進行數據處理,而BIO(OIO)方式服務器端雖然仍是經過一個BOSS線程來處理等待連接的接入,可是客戶端是由主線程直接connect,另外寫數據C/S兩端都是直接主線程寫,而數據讀操做是經過一個WORKER 線程BLOCK方式讀取(一直等待,直到讀到數據,除非channel關閉)。框架

網絡動做歸結到最簡單就是服務器端bind->accept->read->write,客戶端 connect->read->write,通常bind或者connect後會有屢次read、write。這種特性致使,bind,accept與read,write的線程分離,connect與read、write線程分離,這樣作的好處就是不管是服務器端仍是客戶端吞吐量將有效增大,以便充分利用機器的處理能力,而不是卡在網絡鏈接上,不過一旦機器處理能力充分利用後,這種方式反而可能會由於過於頻繁的線程切換致使性能損失而得不償失,而且這種處理模型複雜度比較高。less

採用什麼樣的網絡事件響應處理機制對於網絡吞吐量是很是重要的,Netty採用的是標準的SEDA(Staged Event-Driven Architecture)架構[http://en.wikipedia.org/wiki/ Staged_event-driven_architecture],其所設計的事件類型,表明了網絡交互的各個階段,而且在每一個階段發生時,觸發相應事件交給初始化時生成的pipeline實例進行處理。事件處理都是經過Channels類的靜態方法調用開始的,將事件、channel傳遞給 channel持有的Pipeline進行處理,Channels類幾乎全部方法都爲靜態,提供一種Proxy的效果(整個工程裏不管什麼時候何地均可以調用其靜態方法觸發固定的事件流轉,但其自己並不關注具體的處理流程)。

Channels部分事件流轉靜態方法
1.fireChannelOpen 2.fireChannelBound 3.fireChannelConnected 4.fireMessageReceived 5.fireWriteComplete 6.fireChannelInterestChanged
7.fireChannelDisconnected 8.fireChannelUnbound 9.fireChannelClosed 10.fireExceptionCaught 11.fireChildChannelStateChanged

Netty提供了全面而又豐富的網絡事件類型,其將java中的網絡事件分爲了兩種類型Upstream和Downstream。通常來講,Upstream類型的事件主要是由網絡底層反饋給Netty的,好比messageReceived,channelConnected等事件,而Downstream類型的事件是由框架本身發起的,好比bind,write,connect,close等事件。

wKioL1VtdgTgLj3mAAJN2ACZTjI305.jpg

Netty的Upstream和Downstream網絡事件類型特性也使一個Handler分爲了3種類型,專門處理Upstream,專門處理Downstream,同時處理Upstream,Downstream。實現方式是某個具體Handler經過繼承ChannelUpstreamHandler和ChannelDownstreamHandler類來進行區分。PipeLine在Downstream或者Upstream類型的網絡事件發生時,會調用匹配事件類型的Handler響應這種調用。ChannelPipeline維持有全部handler有序鏈表,而且由handler自身控制是否繼續流轉到下一個handler(ctx.sendDownstream(e),這樣設計有個好處就是隨時終止流轉,業務目的達到無需繼續流轉到下一個handler)。下面的代碼是取得下一個處理Downstream事件的處理器。
[java]
DefaultChannelHandlerContext realCtx = ctx;
while (!realCtx.canHandleUpstream()) {
realCtx = realCtx.next;
if (realCtx == null) {
return null;
}
}

return realCtx;
[/java]
若是是一個網絡會話最末端的事件,好比messageRecieve,那麼可能在某個handler裏面就直接結束整個會話,並把數據交給上層應用,可是若是是網絡會話的中途事件,好比connect事件,那麼當觸發connect事件時,通過pipeline流轉,最終會到達掛載pipeline最底下的ChannelSink實例中,這類實例主要做用就是發送請求和接收請求,以及數據的讀寫操做。

wKiom1VtdLnipkt6AAIjZhKiH0k915.jpg

NIO方式ChannelSink通常會有1個BOSS實例(implements Runnable),以及若干個worker實例(不設置默認爲cpu cores*2個worker),這在前面已經提起過,BOSS線程在客戶端類型的ChannelSink和服務器端類型的ChannelSink觸發條件不同,客戶端類型的BOSS線程是在發生connect事件時啓動,主要監聽connect是否成功,若是成功,將啓動一個worker線程,將connected的channel交給這個線程繼續下面的工做,而服務器端的BOSS線程是發生在bind事件時啓動,它的工做也相對比較簡單,對於channel.socket().accept()進來的請求向Nioworker進行工做分配便可。這裏須要提到的是,Server端ChannelSink實現比較特別,不管是NioServerSocketPipelineSink 仍是OioServerSocketPipelineSink的eventSunk方法實現都將channel分爲 ServerSocketChannel和SocketChannel分開處理。這主要緣由是Boss線程accept()一個新的鏈接生成一個 SocketChannel交給Worker進行數據接收。
[java]
public void eventSunk(
ChannelPipeline pipeline, ChannelEvent e) throws Exception {
Channel channel = e.getChannel();
if (channel instanceof NioServerSocketChannel) {
handleServerSocket(e);
} else if (channel instanceof NioSocketChannel) {
handleAcceptedSocket(e);
}
}

NioWorker worker = nextWorker();
worker.register(new NioAcceptedSocketChannel(
channel.getFactory(), pipeline, channel,
NioServerSocketPipelineSink.this, acceptedSocket,
worker, currentThread), null);
[/java]
另外二者實例化時都會走一遍以下流程:
[java]
setConnected();
fireChannelOpen(this);
fireChannelBound(this, getLocalAddress());
fireChannelConnected(this, getRemoteAddress());
[/java]
而對應的ChannelSink裏面的處理代碼就不一樣於ServerSocketChannel了,由於走的是 handleAcceptedSocket(e)這一塊代碼,從默認實現代碼來講,實例化調用 fireChannelOpen(this);fireChannelBound(this,getLocalAddress());fireChannelConnected(this,getRemoteAddress())沒有什麼意義,可是對於本身實現的ChannelSink有着特殊意義。具體的用途我沒去了解,可是可讓用戶插手Server accept鏈接到準備讀寫數據這一個過程的處理。
[java]
switch (state) {
case OPEN:
if (Boolean.FALSE.equals(value)) {
channel.worker.close(channel, future);
}
break;
case BOUND:
case CONNECTED:
if (value == null) {
channel.worker.close(channel, future);
}
break;
case INTEREST_OPS:
channel.worker.setInterestOps(channel, future, ((Integer) value).intValue());
break;
}
[/java]
Netty提供了大量的handler來處理網絡數據,可是大部分是CODEC相關的,以便支持多種協議,下面一個圖繪製了現階段Netty提供的Handlers(紅色部分不徹底)

wKiom1VtdPewBsUCAAP31IipUys392.jpg

Netty實現封裝實現了本身的一套ByteBuffer系統,這個ByteBuffer系統對外統一的接口就是ChannelBuffer,這個接口從總體上來講定義了兩類方法,一種是相似getXXX(int index…),setXXX(int index…)須要指定開始操做buffer的起始位置,簡單點來講就是直接操做底層buffer,並不用到Netty特有的高可重用性buffer特性,因此Netty內部對於這類方法調用很是少,另一種是相似readXXX(),writeXXX()不須要指定位置的buffer操做,這類方法實現放在了AbstractChannelBuffer,其主要的特性就是維持buffer的位置信息,包括readerIndex,writerIndex,以及回溯做用的markedReaderIndex和markedWriterIndex,當用戶調用readXXX()或者writeXXX()方法時,AbstractChannelBuffer會根據維護的readerIndex,writerIndex計算出讀取位置,而後調用繼承本身的ChannelBuffer的getXXX(int index…)或者setXXX(int index…)方法返回結果,這類方法在Netty內部被大量調用,由於這個特性最大的好處就是很方便地重用buffer而沒必要去費心費力維護index或者新建大量的ByteBuffer。

另外WrappedChannelBuffer接口提供的是對ChannelBuffer的代理,他的用途說白了就是重用底層buffer,可是會轉換一些buffer的角色,好比本來是讀寫皆可 ,wrap成ReadOnlyChannelBuffer,那麼整個buffer只能使用readXXX()或者getXXX()方法,也就是隻讀,而後底層的buffer仍是原來那個,再如一個已經進行過讀寫的ChannelBuffer被wrap成TruncatedChannelBuffer,那麼新的buffer將會忽略掉被wrap的buffer內數據,而且能夠指定新的writeIndex,至關於slice功能。

wKioL1VtdwqgkSi-AAJC88UtMZA715.jpg

Netty實現了本身的一套完整Channel系統,這個channel說實在也是對java 網絡作了一層封裝,加上了SEDA特性(基於事件響應,異步,多線程等)。其最終的網絡通訊仍是依靠底下的java網絡api。提到異步,不得不提到Netty的Future系統,從channel的定義來講,write,bind,connect,disconnect,unbind,close,甚至包括setInterestOps等方法都會返回一個channelFuture,這這些方法調用都會觸發相關網絡事件,而且在pipeline中流轉。Channel不少方法調用基本上不會立刻就執行到最底層,而是觸發事件,在pipeline中走一圈,最後纔在channelsink中執行相關操做,若是涉及網絡操做,那麼最終調用會回到Channel中,也就是serversocketchannel,socketchannel,serversocket,socket等java原生網絡api的調用,而這些實例就是jboss實現的channel所持有的(部分channel)。

wKioL1VtdzuC6i01AAGZuU0yRp0398.jpg

Netty新版本出現了一個特性zero-copy,這個機制可使文件內容直接傳輸到相應channel上而不須要經過cpu參與,也就少了一次內存複製。Netty內部ChunkedFile 和 FileRegion 構成了non zero-copy 和zero-copy兩種形式的文件內容傳輸機制,前者須要CPU參與,後者根據操做系統是否支持zero-copy將文件數據傳輸到特定channel,若是操做系統支持,不須要cpu參與,從而少了一次內存複製。ChunkedFile主要使用file的read,readFully等API,而FileRegion使用FileChannel的transferTo API,2者實現並不複雜。Zero-copy的特性仍是得看操做系統的,自己代碼沒有很大的特別之處。

最後總結下,Netty的架構思想和細節能夠說讓人眼前一亮,對於java網絡IO的各個注意點,能夠說Netty已經解決得比較徹底了,同時Netty 的做者也是另一個NIO框架MINA的做者,在實際使用中積累了豐富的經驗,可是本文也只是一個新手對於Netty的初步理解,尚未足夠的能力指出某一細節的所發揮的做用。


若有問題請直接到羣:457036818或者登錄http://java.tanzhouedu.net

相關文章
相關標籤/搜索