首先,看類:ServerBootstrap,Server的啓動過程就是從這裏開始的。通過簡單的構造方法注入ChannelFactory後設置ChannelPiplineFactory,再調用bind方法,服務器便啓動起來了。這裏重點關注一下兩個工廠類,從類名可以看出是用來產出Channel和ChannelPipline的。Channel和ChannelPipline都是netty的核心概念,貫穿了服務的整個過程。
NIO中的通道
那麼Channel在netty中扮演了一個怎麼樣的角色呢?顧名思義,Channel即是通道的意思。提到Channel,首先會想到NIO。在Nio中,廢棄了面向Socket和ServerSocket編程的方式,引入了通道和字節緩衝區(ByteBuffer)的概念。通道關聯着某個文件描述符(FD)和字節緩衝區,來將緩衝區的數據寫入FD關聯的文件或者套接字,或將文件或套接字的內容讀入ByteBuffer。所以通道分爲讀、寫通道,分別實現了ReadableByteChannel、WritableByteChannel,當然同時實現了兩個接口後便是雙向的通道了。不過NIO中已經爲我們定義好了很多好用的通道,能夠解決我們遇到的大多數問題,不用自己去重新實現。那麼在衆多的實現中,與網絡通信相關聯的通道有哪些呢,來看看類圖。
從下往上看,ServerSocketChannel、SocketChannel、DatagramChannel是我們需要打交道的面向連接和無連接的通道。它們的繼承關係是這樣的:AbstractSelectableChannel —-> … --> InterruptibleChannel –-> Channel。
這裏又需要引入一個概念:selector,它是Channel的最佳搭檔。我們知道NIO與OIO相比,很大優勢在於NIO可以選擇非阻塞模式處理I/O事件,從而避免了線程阻塞的情況。尤其是在高併發的情況下,使用傳統Socket,往往需要爲每一個連接創建一個線程,如果不這樣,當工作線程都阻塞了,來了新的請求就沒人幹活了。然而,創建大量的線程帶來的消耗是巨大的,例如:上下文切換等。而在NIO中,可以啓用非阻塞的模式來進行,例如,在某個連接(Connect1)中讀取消息時,如果此時沒有消息到達,讀取線程可以立即返回,無需等待讀取成功,這樣就不怕工作線程都阻塞而導致沒有工作人員的情況了。但是僅僅靠非阻塞來處理高併發是不夠的,當工作線程去處理Connect2時,將Connect1放在哪裏呢、當Connect1中有了新的消息時怎麼通知到工作線程呢?selector的加入,完美的解決了這個問題。Selector融合了linux中的select、poll或者epoll模型,通過reactor模式來達到I/O多路複用的目的(在下一篇文章中將會做詳細的介紹)。在初始化時,告知selector管理器,當前的通道是哪一個(即關聯的socket)、當該通道上面發生了xx事件時需要被記錄下來。這個時候,與該通道關聯的線程可以去做其他事情(例如:處理其他通道的消息),在必要的時候,該線程去詢問selector管理器,自己感興趣的事件中哪些已經發生了,如果發生了就加入到自己的處理隊列中,做好處理的就緒工作。當然,你也可以選擇netty中OIO的實現,不過對高併發的處理上,性能相對會低很多。
通道接口InterruptibleChannel表示該通道是可以被中斷的,當工作線程在某通道上被阻塞的時候,該線程被中斷了,那麼通道將會關閉,此線程也會產生一個ClosedByInterruptException異常。假設一個線程的中斷狀態被設置後,再去訪問某通道,此時通道也會被關閉,同時拋出ClosedByInterruptException異常。如果一個通道被關閉,休眠在該通道上面的所有線程都會被喚醒,同時收到一個AsynchronousCloseException異常。從上圖可以看出,我們用到的幾個socket相關的通道都是可中斷的通道。
在類的繼承中,ServerSocketChannel與SocketChannel、DatagramChannel是有所不同的。SocketChannel、DatagramChannel同時繼承了ReadableByteChannel、WritableByteChannel,可用於讀和寫。這是由於ServerSocketChannel本身並不會讀寫,專門用於接收connect,收到connect後,由SocketChannel來處理消息。
Netty中的通道
同樣netty中也引入了通道的概念,netty框架在其nio的實現過程中,實際上是對nio的通道進行了上層的封裝,它是關聯網絡socket或者能夠用於I/O操作(比如:讀、寫、連接和綁定)的組件。先看一下NioServerSocketChannel的繼承關係:
1. ServerChannel:用於接收連接請求的通道,它通過accept()方法來創建子通道,例如其子類ServerSocketChannel。
2. SocketChannel:TCP/IP socket 通道,它通常被serverSocketChannel的accept()方法或者ClientSocketChannelFactory類創建。
3. AbstractChannel:Channel的抽象實現。
4. DatagramChannel:UDP/IP通道,通過DatagramChannelFactory創建。
5. LocalChannel:用於本地傳輸的通道。
Channel給我們提供了:
1. 通道目前的狀態(如:是否打開?是否已連接?)
2. 通道的配置參數(如:用於接收消息的buffer的大小)
3. 通道提供的I/O操作(如:寫、連接、綁定等)
4. 還提供了用於處理與通道相關聯的I/O事件和I/O請求的ChannelPipline
通道中所有的I/O操作都是異步進行的
這意味着所有的I/O調用在結束的時候都不能保證該I/O操作已經完成了。相反的,這個時候用戶需要返回一個ChannelFuture實例,當這個請求成功、失敗或者取消的時候,Futrue就會通知你。
通道是分層級的
一個通道是否有父通道取決於它的創建方式。例如:通過ServerSocketChannel收到連接時(ServerSocketChannel.accepted()方法)創建的SocketChannel,在channel的getParent()方法中就會返回他的父通道ServerSocketChannel。
分層結構的意義在於你需要的通道是屬於哪種傳輸方式。例如:你可以寫一個新通道的實現方式,這個通道和它的子通道共享一個socket連接,例如BEEP協議和SSH協議的實現。
向下轉換解決特殊的傳輸方式
一些網絡傳輸需要附加一些特殊的操作。這時可以通過繼承的方式,在子類中去實現這些操作。例如:用OIO的方式處理報文傳輸,在DatagramChannel中就實現了廣播join和leave的操作。
感興趣事件(InterestOps)
通道有一個被稱作InterestOps的屬性,這和NIO中的SelectionKey相似。它是由兩個標誌組成的bit field來表示的。
1. OP_READ:如果設置了這個標誌,那麼從遠端發送來的消息將會被立即讀到。相反,如果沒有設置,就有等到被設置過後才能讀取遠端的消息了。
2. OP_WRITE:如果設置了這個標誌,寫請求就不會發送到遠端,而是停留在隊列中,直到清除了這個標誌爲止。如果沒有設置,寫請求就會被儘快的進行出隊列的操作。
3. OP_READ_WRITE:這個標誌關聯了OP_READ和OP_WRITE,含義是隻有寫請求才會被掛起。
4. OP_NONE:這個標誌關聯了非OP_READ和非OP_WRITE,含義是隻有讀請求才會被掛起。
用戶可以通過setReadable(boolean)函數來設置或者清除OP_READ來掛起和恢復讀操作。
需要注意的是,不能像設置或者清除OP_READ一樣來處理OP_WRITE,它是隻讀的,用於告訴應用掛起的寫請求是否達到了臨界值,避免放入過多掛起的寫請求導致內存溢出。比如:在用NIO傳輸的NioSocketChannelConfig中使用writeBufferLowWaterMark和writeBufferHighWaterMark屬性來決定何時可以放入或者清除OP_WRITE標誌。
事件
通道封裝了NIO的Channel,用於接收連接或者讀取消息,收到連接或者消息就代表一個事件發生了,在netty中同樣做了相應的映射,抽象出ChannelEvent的概念,表示:和某通道關聯的I/O事件或者I/O請求。來看看事件的類圖結構:
事件分爲UpStream事件和downStream事件,一個事件的處理流向如果是從ChannelPipline中的第一個(head)Handler(後文講解)開始到最後一個(tail)Handler,那麼就稱這個事件爲UpStream事件,相反,如果一個事件的處理流向是從ChannelPipline中的最後一個Handler開始到第一個Handler,就稱這個事件爲downStream事件。
當服務器端收到來自客戶端的消息時,攜帶消息的事件是一個Upstream事件。當服務器端向客戶端發送消息或者回應客戶端的時候,這個事件就爲downStream事件。當然,站在客戶端的角度看也是一樣。Upstream事件往往是由外向內獲取資源等操作後觸發的,例如:InputStream.read(byte[])等事件發生後通知handler去處理讀到的消息,downStream事件往往是由內向外發送請求時所觸發的,例如:OutputStream.write(byte[]),Socket.connect (SocketAddress), and Socket.close()等請求會觸發handler進行寫、連接、關閉socket等操作。
個人理解:upStream事件是事件發生之後,用於通知handler做相應的處理,這時事件已經發生;downStream事件是通知handler去做相應的請求操作,是爲了處理該事件所發起的請求。
UpStream事件包括:
事件名稱 |
事件類型與發生條件 |
含義 |
備註 |
messageReceived |
MessageEvent |
表示從遠端接收到了消息(eg:ChannelBuffer) |
|
exceptionCaught |
ExceptionEvent |
表示在某handler或者I/O線程中發生了異常 |
|
channelOpen |
ChannelStateEvent (state=OPEN,value=true) |
表示某通道打開了,但是還沒有綁定或者鏈接成功 |
注意:這個事件是由Boss 線程內部觸發的,所以不要對它做一些重量級的操作,否則會阻塞其他worker線程的調度 |
channelClosed |
ChannelStateEvent (state=OPEN,value=false) |
表示關聯的通道已經關閉和相關資源已經釋放 |
|
channelBound |
ChannelStateEvent (state=BOUND,value=socketAddress) |
表示通道已經綁定到本地地址,但還沒有連接 |
注意:同channelOpen |
channelUnbound |
ChannelStateEvent (state=BOUND,value=null) |
表示已從當前地址解除綁定 |
|
channelConnected |
ChannelStateEvent (state=CONNECTED,value=socketAddress) |
表示當前通道已經打開、綁定了本地地址、並與遠程地址連接成功 |
注意:同channelOpen |
writeComplete |
WriteCompletionEvent |
表示有消息被寫到了遠端 |
|
channelDisconnected |
ChannelStateEvent (state=CONNECTED,value=socketAddress) |
表示通道與遠端的連接斷開 |
|
channelInterestChanged |
ChannelStateEvent (state= INTEREST_OPS) |
表示修改了通道感興趣的事件 |
|
有兩種事件只被用於有子通道的通道,比如:ServerSocketChannel
事件名稱 |
事件類型與發生條件 |
含義 |
備註 |
childChannelOpen |
ChildChannelStateEvent (childChannel.isOpen() = true) |
當子通道發生OPEN事件的時候,例如:當serverChannel接到連接時 |
|
childChannelClosed |
ChildChannelStateEvent (childChannel.isOpen() = false) |
當子通道發生CLOSE事件的時候,例如:接收到的連接關閉 |
|
downStream事件包括:
事件名稱 |
事件類型與發生條件 |
含義 |
備註 |
write |
MessageEvent |
向通道發送消息 |
|
bind |
ChannelStateEvent (state=BOUND,value=socketAddress) |
將通道綁定到value所指向的地址 |
|
unbind |
ChannelStateEvent (state=BOUND,value=null) |
請求解除與關聯地址的綁定關係 |
|
connect |
ChannelStateEvent (state=CONNECTED,value=socketAddress) |
請求連接到value所指定的地址 |
|
unconnect |
ChannelStateEvent (state=CONNECTED,value=null) |
請求與當前地址解除連接關係 |
|
close |
ChannelStateEvent (state=OPEN,value=false) |
關閉通道 |
|
需要注意的是在downStream事件中沒有提到open事件,這是因爲ChannelFactory在創建通道的時候它就處於open狀態了。
Handler
當接收到一個ChannelEvent時,我們應該做怎麼樣的處理,比如:在消息被Channel讀入的時候我們應該怎麼處理,在回覆客戶端之前應該乾點什麼,這些都是應該由我們的應用程序來控制的業務邏輯。可以看到,在上一篇文章中,Server中包含了一個內部類MyChannelHandler,在接收到連接時輸出當前Channel的信息、接收到消息時回覆客戶端等操作就是我們的業務邏輯。在netty中,爲我們封裝了ChannelHandler接口,用於處理或攔截ChannelEvent,並且傳遞這個事件給所在ChannelPipline中的下一個handler。
子類
ChannelHandler接口沒有實現任何方法。用於處理事件的Handler需要去繼承它的子接口。以下的兩個子接口用於處理接收到的事件,一個是處理upStream事件的,另一個是用來處理downStream事件的。
1. ChannelUpstreamHandler:用於處理upStream事件。
通常被用於工作者線程攔截到I/O請求中轉換(編碼等處理)消息或者其它相關的業務邏輯。
SimpleChannelUpstreamHandler是實現中最常用的一個類,因爲它已經實現了關於各個事件最基礎的方法。當然,遇到特殊的需求,也可以直接實現這個接口來做處理。
2. ChannelDownstreamHandler:用於處理downStream事件。
ChannelPipline
前面介紹了handler,通過在Handler中注入業務邏輯。但是我們對業務邏輯的處理往往不像前一篇文章中講到的那麼簡單,例如:在接收到消息時,先進行解碼,得到我們需要的數據結構,再對該數據結構進行真正的邏輯處理等。這時,我們就可以將這兩個邏輯放到兩個handler中,一個用於解碼,另一個用於處理業務,並且規定handler的執行順序,先解碼後處理業務。這樣我們就可以把工作拆分開來,代碼看起來乾淨、簡潔。尤其是在我們需要做的事情很多時,將任務拆解是一種很好的方式。這就是即將隆重推出的ChannelPipline。在ChannelPipline中注入我們實現好的handler,netty就會在謀事件發生的時候依次執行handler。
其中head和tail對應的類:DefaultChannelHandlerContext,是整個處理流程的上下文。以下爲類圖:
Context中定義了當前的handler實例,並且根據ChannelHandler的類型記錄是用於處理upstream事件還是downstream事件的,分別以兩個boolean變量表示。再看看next、prev成員變量,很明顯這是一個雙向鏈表的結構,通過next找到下一個handler,通過prev找到上一個handler。
Context是ChannelPipline中的重要角色,被定義爲兩個變量:head、tail。也就是說可以從ChannelPipline中找到頭部和尾部的context即可找到對應的handler。而通過該context的sendUpstream(ChannelEvent)和sendDownstream(ChannelEvent)方法又可以將事件傳遞給其上下的handler處理,從而串起了upstream事件和downstream事件的整個流程。