首先來看下最如何使用Netty(其自帶example很好展示了使用),Netty普通使用一般是通過BootStrap來啟動,BootStrap主要分為兩類:1.面向連接(TCP)的BootStrap(ClientBootStrap和ServerBootstrap),2.非面向連接(UDP)的(ConnectionlessBootstrap)。
Netty整體架構(gòu)很清晰的分成2個(gè)部分,ChannelFactory和ChannelPipelineFactory,前者主要生產(chǎn)網(wǎng)絡(luò)通信相關(guān)的Channel實(shí)例和ChannelSink實(shí)例,Netty提供的ChannelFactory實(shí)現(xiàn)基本能夠滿足絕大部分用戶的需求,當(dāng)然你也可以定制自己的ChannelFactory,后者主要關(guān)注于具體傳輸數(shù)據(jù)的處理,同時(shí)也包括其他方面的內(nèi)容,比如異常處理等等,只要是你希望的,你都可以往里添加相應(yīng)的handler,一般ChannelPipelineFactory由用戶自己實(shí)現(xiàn),因?yàn)閭鬏敂?shù)據(jù)的處理及其他操作和業(yè)務(wù)關(guān)聯(lián)比較緊密,需要自定義處理的handler。
現(xiàn)在,使用Netty的步驟實(shí)際上已經(jīng)非常明確了,比如面向連接的Netty服務(wù)端客戶端使用,第一步:實(shí)例化一個(gè)BootStrap,并且通過構(gòu)造方法指定一個(gè)ChannelFactory實(shí)現(xiàn),第二步:向bootstrap實(shí)例注冊一個(gè)自己實(shí)現(xiàn)的ChannelPipelineFactory,第三步:如果是服務(wù)器端,bootstrap.bind(new InetSocketAddress(port)),然后等待客戶端來連接,如果是客戶端,bootstrap.connect(new InetSocketAddress(host,port))取得一個(gè)future,這個(gè)時(shí)候Netty會去連接遠(yuǎn)程主機(jī),在連接完成后,會發(fā)起類型為CONNECTED的ChannelStateEvent,并且開始在你自定義的Pipeline里面流轉(zhuǎn),如果你注冊的handler有這個(gè)事件的響應(yīng)方法的話那么就會調(diào)用到這個(gè)方法。在此之后就是數(shù)據(jù)的傳輸了。下面是一個(gè)簡單客戶端的代碼解讀。























Netty提供了NIO與BIO(OIO)兩種模式處理這些邏輯,其中NIO主要通過一個(gè)BOSS線程處理等待鏈接的接入,若干個(gè)WORKER線程(從worker線程池中挑選一個(gè)賦給Channel實(shí)例,因?yàn)镃hannel實(shí)例持有真正的java網(wǎng)絡(luò)對象)接過BOSS線程遞交過來的CHANNEL進(jìn)行數(shù)據(jù)讀寫并且觸發(fā)相應(yīng)事件傳遞給pipeline進(jìn)行數(shù)據(jù)處理,而BIO(OIO)方式服務(wù)器端雖然還是通過一個(gè)BOSS線程來處理等待鏈接的接入,但是客戶端是由主線程直接connect,另外寫數(shù)據(jù)C/S兩端都是直接主線程寫,而數(shù)據(jù)讀操作是通過一個(gè)WORKER 線程BLOCK方式讀取(一直等待,直到讀到數(shù)據(jù),除非channel關(guān)閉)。
網(wǎng)絡(luò)動作歸結(jié)到最簡單就是服務(wù)器端bind->accept->read->write,客戶端connect->read->write,一般bind或者connect后會有多次read、write。這種特性導(dǎo)致,bind,accept與read,write的線程分離,connect與read、write線程分離,這樣做的好處就是無論是服務(wù)器端還是客戶端吞吐量將有效增大,以便充分利用機(jī)器的處理能力,而不是卡在網(wǎng)絡(luò)連接上,不過一旦機(jī)器處理能力充分利用后,這種方式反而可能會因?yàn)檫^于頻繁的線程切換導(dǎo)致性能損失而得不償失,并且這種處理模型復(fù)雜度比較高。
采用什么樣的網(wǎng)絡(luò)事件響應(yīng)處理機(jī)制對于網(wǎng)絡(luò)吞吐量是非常重要的,Netty采用的是標(biāo)準(zhǔn)的SEDA(Staged Event-Driven Architecture)架構(gòu)[http://en.wikipedia.org/wiki/ Staged_event-driven_architecture],其所設(shè)計(jì)的事件類型,代表了網(wǎng)絡(luò)交互的各個(gè)階段,并且在每個(gè)階段發(fā)生時(shí),觸發(fā)相應(yīng)事件交給初始化時(shí)生成的pipeline實(shí)例進(jìn)行處理。事件處理都是通過Channels類的靜態(tài)方法調(diào)用開始的,將事件、channel傳遞給channel持有的Pipeline進(jìn)行處理,Channels類幾乎所有方法都為靜態(tài),提供一種Proxy的效果(整個(gè)工程里無論何時(shí)何地都可以調(diào)用其靜態(tài)方法觸發(fā)固定的事件流轉(zhuǎn),但其本身并不關(guān)注具體的處理流程)。
Channels部分事件流轉(zhuǎn)靜態(tài)方法
1.fireChannelOpen 2.fireChannelBound 3.fireChannelConnected 4.fireMessageReceived 5.fireWriteComplete 6.fireChannelInterestChanged
7.fireChannelDisconnected 8.fireChannelUnbound 9.fireChannelClosed 10.fireExceptionCaught 11.fireChildChannelStateChanged
Netty提供了全面而又豐富的網(wǎng)絡(luò)事件類型,其將java中的網(wǎng)絡(luò)事件分為了兩種類型Upstream和Downstream。一般來說,Upstream類型的事件主要是由網(wǎng)絡(luò)底層反饋給Netty的,比如messageReceived,channelConnected等事件,而Downstream類型的事件是由框架自己發(fā)起的,比如bind,write,connect,close等事件。

Netty的Upstream和Downstream網(wǎng)絡(luò)事件類型特性也使一個(gè)Handler分為了3種類型,專門處理Upstream,專門處理Downstream,同時(shí)處理Upstream,Downstream。實(shí)現(xiàn)方式是某個(gè)具體Handler通過繼承ChannelUpstreamHandler和ChannelDownstreamHandler類來進(jìn)行區(qū)分。PipeLine在Downstream或者Upstream類型的網(wǎng)絡(luò)事件發(fā)生時(shí),會調(diào)用匹配事件類型的Handler響應(yīng)這種調(diào)用。ChannelPipeline維持有所有handler有序鏈表,并且由handler自身控制是否繼續(xù)流轉(zhuǎn)到下一個(gè)handler(ctx.sendDownstream(e),這樣設(shè)計(jì)有個(gè)好處就是隨時(shí)終止流轉(zhuǎn),業(yè)務(wù)目的達(dá)到無需繼續(xù)流轉(zhuǎn)到下一個(gè)handler)。下面的代碼是取得下一個(gè)處理Downstream事件的處理器。
1
DefaultChannelHandlerContext realCtx = ctx;
2
while (!realCtx.canHandleUpstream()) {
3
realCtx = realCtx.next;
4
if (realCtx == null) {
5
return null;
6
}
7
}
8
9
return realCtx;

2

3

4

5

6

7

8

9

如果是一個(gè)網(wǎng)絡(luò)會話最末端的事件,比如messageRecieve,那么可能在某個(gè)handler里面就直接結(jié)束整個(gè)會話,并把數(shù)據(jù)交給上層應(yīng)用,但是如果是網(wǎng)絡(luò)會話的中途事件,比如connect事件,那么當(dāng)觸發(fā)connect事件時(shí),經(jīng)過pipeline流轉(zhuǎn),最終會到達(dá)掛載pipeline最底下的ChannelSink實(shí)例中,這類實(shí)例主要作用就是發(fā)送請求和接收請求,以及數(shù)據(jù)的讀寫操作。

NIO方式ChannelSink一般會有1個(gè)BOSS實(shí)例(implements Runnable),以及若干個(gè)worker實(shí)例(不設(shè)置默認(rèn)為cpu cores*2個(gè)worker),這在前面已經(jīng)提起過,BOSS線程在客戶端類型的ChannelSink和服務(wù)器端類型的ChannelSink觸發(fā)條件不一樣,客戶端類型的BOSS線程是在發(fā)生connect事件時(shí)啟動,主要監(jiān)聽connect是否成功,如果成功,將啟動一個(gè)worker線程,將connected的channel交給這個(gè)線程繼續(xù)下面的工作,而服務(wù)器端的BOSS線程是發(fā)生在bind事件時(shí)啟動,它的工作也相對比較簡單,對于channel.socket().accept()進(jìn)來的請求向Nioworker進(jìn)行工作分配即可。這里需要提到的是,Server端ChannelSink實(shí)現(xiàn)比較特別,無論是NioServerSocketPipelineSink還是OioServerSocketPipelineSink的eventSunk方法實(shí)現(xiàn)都將channel分為ServerSocketChannel和SocketChannel分開處理。這主要原因是Boss線程accept()一個(gè)新的連接生成一個(gè)SocketChannel交給Worker進(jìn)行數(shù)據(jù)接收。
1
public void eventSunk(
2
ChannelPipeline pipeline, ChannelEvent e) throws Exception {
3
Channel channel = e.getChannel();
4
if (channel instanceof NioServerSocketChannel) {
5
handleServerSocket(e);
6
} else if (channel instanceof NioSocketChannel) {
7
handleAcceptedSocket(e);
8
}
9
}

2

3

4

5

6

7

8

9

1
NioWorker worker = nextWorker();
2
worker.register(new NioAcceptedSocketChannel(
3
channel.getFactory(), pipeline, channel,
4
NioServerSocketPipelineSink.this, acceptedSocket,
5
worker, currentThread), null);

2

3

4

5

另外兩者實(shí)例化時(shí)都會走一遍如下流程:
1
setConnected();
2
fireChannelOpen(this);
3
fireChannelBound(this, getLocalAddress());
4
fireChannelConnected(this, getRemoteAddress());

2

3

4

而對應(yīng)的ChannelSink里面的處理代碼就不同于ServerSocketChannel了,因?yàn)樽叩氖莌andleAcceptedSocket(e)這一塊代碼,從默認(rèn)實(shí)現(xiàn)代碼來說,實(shí)例化調(diào)用fireChannelOpen(this);fireChannelBound(this,getLocalAddress());fireChannelConnected(this,getRemoteAddress())沒有什么意義,但是對于自己實(shí)現(xiàn)的ChannelSink有著特殊意義。具體的用途我沒去了解,但是可以讓用戶插手Server accept連接到準(zhǔn)備讀寫數(shù)據(jù)這一個(gè)過程的處理。
1
switch (state) {
2
case OPEN:
3
if (Boolean.FALSE.equals(value)) {
4
channel.worker.close(channel, future);
5
}
6
break;
7
case BOUND:
8
case CONNECTED:
9
if (value == null) {
10
channel.worker.close(channel, future);
11
}
12
break;
13
case INTEREST_OPS:
14
channel.worker.setInterestOps(channel, future, ((Integer) value).intValue());
15
break;
16
}

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

Netty提供了大量的handler來處理網(wǎng)絡(luò)數(shù)據(jù),但是大部分是CODEC相關(guān)的,以便支持多種協(xié)議,下面一個(gè)圖繪制了現(xiàn)階段Netty提供的Handlers(紅色部分不完全)
Netty實(shí)現(xiàn)封裝實(shí)現(xiàn)了自己的一套ByteBuffer系統(tǒng),這個(gè)ByteBuffer系統(tǒng)對外統(tǒng)一的接口就是ChannelBuffer,這個(gè)接口從整體上來說定義了兩類方法,一種是類似getXXX(int index…),setXXX(int index…)需要指定開始操作buffer的起始位置,簡單點(diǎn)來說就是直接操作底層buffer,并不用到Netty特有的高可重用性buffer特性,所以Netty內(nèi)部對于這類方法調(diào)用非常少,另外一種是類似readXXX(),writeXXX()不需要指定位置的buffer操作,這類方法實(shí)現(xiàn)放在了AbstractChannelBuffer,其主要的特性就是維持buffer的位置信息,包括readerIndex,writerIndex,以及回溯作用的markedReaderIndex和markedWriterIndex,當(dāng)用戶調(diào)用readXXX()或者writeXXX()方法時(shí),AbstractChannelBuffer會根據(jù)維護(hù)的readerIndex,writerIndex計(jì)算出讀取位置,然后調(diào)用繼承自己的ChannelBuffer的getXXX(int index…)或者setXXX(int index…)方法返回結(jié)果,這類方法在Netty內(nèi)部被大量調(diào)用,因?yàn)檫@個(gè)特性最大的好處就是很方便地重用buffer而不必去費(fèi)心費(fèi)力維護(hù)index或者新建大量的ByteBuffer。
另外WrappedChannelBuffer接口提供的是對ChannelBuffer的代理,他的用途說白了就是重用底層buffer,但是會轉(zhuǎn)換一些buffer的角色,比如原本是讀寫皆可 ,wrap成ReadOnlyChannelBuffer,那么整個(gè)buffer只能使用readXXX()或者getXXX()方法,也就是只讀,然后底層的buffer還是原來那個(gè),再如一個(gè)已經(jīng)進(jìn)行過讀寫的ChannelBuffer被wrap成TruncatedChannelBuffer,那么新的buffer將會忽略掉被wrap的buffer內(nèi)數(shù)據(jù),并且可以指定新的writeIndex,相當(dāng)于slice功能。

Netty實(shí)現(xiàn)了自己的一套完整Channel系統(tǒng),這個(gè)channel說實(shí)在也是對java 網(wǎng)絡(luò)做了一層封裝,加上了SEDA特性(基于事件響應(yīng),異步,多線程等)。其最終的網(wǎng)絡(luò)通信還是依靠底下的java網(wǎng)絡(luò)api。提到異步,不得不提到Netty的Future系統(tǒng),從channel的定義來說,write,bind,connect,disconnect,unbind,close,甚至包括setInterestOps等方法都會返回一個(gè)channelFuture,這這些方法調(diào)用都會觸發(fā)相關(guān)網(wǎng)絡(luò)事件,并且在pipeline中流轉(zhuǎn)。Channel很多方法調(diào)用基本上不會馬上就執(zhí)行到最底層,而是觸發(fā)事件,在pipeline中走一圈,最后才在channelsink中執(zhí)行相關(guān)操作,如果涉及網(wǎng)絡(luò)操作,那么最終調(diào)用會回到Channel中,也就是serversocketchannel,socketchannel,serversocket,socket等java原生網(wǎng)絡(luò)api的調(diào)用,而這些實(shí)例就是jboss實(shí)現(xiàn)的channel所持有的(部分channel)。
Netty新版本出現(xiàn)了一個(gè)特性zero-copy,這個(gè)機(jī)制可以使文件內(nèi)容直接傳輸?shù)较鄳?yīng)channel上而不需要通過cpu參與,也就少了一次內(nèi)存復(fù)制。Netty內(nèi)部ChunkedFile 和 FileRegion 構(gòu)成了non zero-copy 和zero-copy兩種形式的文件內(nèi)容傳輸機(jī)制,前者需要CPU參與,后者根據(jù)操作系統(tǒng)是否支持zero-copy將文件數(shù)據(jù)傳輸?shù)教囟?/span>channel,如果操作系統(tǒng)支持,不需要cpu參與,從而少了一次內(nèi)存復(fù)制。ChunkedFile主要使用file的read,readFully等API,而FileRegion使用FileChannel的transferTo API,2者實(shí)現(xiàn)并不復(fù)雜。Zero-copy的特性還是得看操作系統(tǒng)的,本身代碼沒有很大的特別之處。
最后總結(jié)下,Netty的架構(gòu)思想和細(xì)節(jié)可以說讓人眼前一亮,對于java網(wǎng)絡(luò)IO的各個(gè)注意點(diǎn),可以說Netty已經(jīng)解決得比較完全了,同時(shí)Netty的作者也是另外一個(gè)NIO框架MINA的作者,在實(shí)際使用中積累了豐富的經(jīng)驗(yàn),但是本文也只是一個(gè)新手對于Netty的初步理解,還沒有足夠的能力指出某一細(xì)節(jié)的所發(fā)揮的作用。