Netty從入門到精通——概念篇

前一篇blog,講解了如何快速啓動netty服務,並通過telnet命令來訪問的簡單過程。其中用到了netty中常用的幾個類和方法,本文將做一一介紹(其中翻譯了netty的api文檔,同時結合自己的理解)。
  首先,看類: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事件的整個流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章