標籤:
Netty提供非同步、事件驅動的網路應用程式架構和工具,用以快速開發高效能、高可靠性的網路伺服器和用戶端程式[官方定義],整體來看其包含了以下內容:1.提供了豐富的協議編解碼支援,2.實現自有的buffer系統,減少複製所帶來的消耗,3.整套channel的實現,4.基於事件的過程流轉以及完整的網路事件響應與擴充,5.豐富的example。本文並不對Netty實際使用中可能出現的問題做分析,只是從代碼角度分析它的架構以及實現上的一些關鍵細節。
首先來看下最如何使用Netty(其內建example很好展示了使用),Netty普通使用一般是通過BootStrap來啟動,BootStrap主要分為兩類:1.連線導向(TCP)的BootStrap(ClientBootStrap和ServerBootstrap),2.非連線導向(UDP) 的(ConnectionlessBootstrap)。
Netty整體架構很清晰的分成2個部分,ChannelFactory 和ChannelPipelineFactory,前者主要生產網路通訊相關的Channel執行個體和ChannelSink執行個體,Netty提供的 ChannelFactory實現基本能夠滿足絕大部分使用者的需求,當然你也可以定製自己的ChannelFactory,後者主要關注於具體傳輸資料的處理,同時也包括其他方面的內容,比如異常處理等等,只要是你希望的,你都可以往裡添加相應的handler,一般 ChannelPipelineFactory由使用者自己實現,因為傳輸資料的處理及其他動作和業務關聯比較緊密,需要自訂處理的handler。
現在,使用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();
// 從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線程分離,這樣做的好處就是無論是伺服器端還是用戶端輸送量將有效增大,以便充分利用機器的處理能力,而不是卡在網路連接上,不過一旦機器處理能力充分利用後,這種方式反而可能會因為過於頻繁的線程切換導致效能損失而得不償失,並且這種處理模型複雜度比較高。
採用什麼樣的網路事件響應處理機制對於網路輸送量是非常重要的,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等事件。
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執行個體中,這類執行個體主要作用就是發送請求和接收請求,以及資料的讀寫操作。
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(紅色部分不完全)
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功能。
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)。
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的初步理解,還沒有足夠的能力指出某一細節的所發揮的作用。
Netty程式碼分析