天天看點

Netty代碼分析

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()));

[/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線程分離,這樣做的好處就是無論是伺服器端還是用戶端吞吐量将有效增大,以便充分利用機器的處理能力,而不是卡在網絡連接配接上,不過一旦機器處理能力充分利用後,這種方式反而可能會因為過于頻繁的線程切換導緻性能損失而得不償失,并且這種處理模型複雜度比較高。

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代碼分析

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事件的處理器。

defaultchannelhandlercontext realctx = ctx;

while (!realctx.canhandleupstream()) {

realctx = realctx.next;

if (realctx == null) {

return null;

}

return realctx;

如果是一個網絡會話最末端的事件,比如messagerecieve,那麼可能在某個handler裡面就直接結束整個會話,并把資料交給上層應用,但是如果是網絡會話的中途事件,比如connect事件,那麼當觸發connect事件時,經過pipeline流轉,最終會到達挂載pipeline最底下的channelsink執行個體中,這類執行個體主要作用就是發送請求和接收請求,以及資料的讀寫操作。

Netty代碼分析

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進行資料接收。

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);

另外兩者執行個體化時都會走一遍如下流程:

setconnected();

firechannelopen(this);

firechannelbound(this, getlocaladdress());

firechannelconnected(this, getremoteaddress());

而對應的channelsink裡面的處理代碼就不同于serversocketchannel了,因為走的是 handleacceptedsocket(e)這一塊代碼,從預設實作代碼來說,執行個體化調用 firechannelopen(this);firechannelbound(this,getlocaladdress());firechannelconnected(this,getremoteaddress())沒有什麼意義,但是對于自己實作的channelsink有着特殊意義。具體的用途我沒去了解,但是可以讓使用者插手server accept連接配接到準備讀寫資料這一個過程的處理。

switch (state) {

case open:

if (boolean.false.equals(value)) {

channel.worker.close(channel, future);

break;

case bound:

case connected:

if (value == null) {

case interest_ops:

channel.worker.setinterestops(channel, future, ((integer) value).intvalue());

netty提供了大量的handler來處理網絡資料,但是大部分是codec相關的,以便支援多種協定,下面一個圖繪制了現階段netty提供的handlers(紅色部分不完全)

Netty代碼分析

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代碼分析

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代碼分析

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的初步了解,還沒有足夠的能力指出某一細節的所發揮的作用。

本文來源于"阿裡中間件",原文釋出時間"2010-09-25"