Netty In Action中文版 - 第三章:Netty核心概念


        在這一章我們將討論Netty的10個核心類,清楚瞭解他們的結構對使用Netty很有用。可能有一些不會再工作中用到,但是也有一些很常用也很核心,你會遇到。

  • Bootstrap or ServerBootstrap
  • EventLoop
  • EventLoopGroup
  • ChannelPipeline
  • Channel
  • Future or ChannelFuture
  • ChannelInitializer
  • ChannelHandler
       本節的目的就是介紹以上這些概念,幫助你瞭解它們的用法。

3.1 Netty Crash Course

        在我們開始之前,如果你瞭解Netty程序的一般結構和大致用法(客戶端和服務器都有一個類似的結構)會更好。
        一個Netty程序開始於Bootstrap類,Bootstrap類是Netty提供的一個可以通過簡單配置來設置或"引導"程序的一個很重要的類。Netty中設計了Handlers來處理特定的"event"和設置Netty中的事件,從而來處理多個協議和數據。事件可以描述成一個非常通用的方法,因爲你可以自定義一個handler,用來將Object轉成byte[]或將byte[]轉成Object;也可以定義個handler處理拋出的異常。
        你會經常編寫一個實現ChannelInboundHandler的類,ChannelInboundHandler是用來接收消息,當有消息過來時,你可以決定如何處理。當程序需要返回消息時可以在ChannelInboundHandler裏write/flush數據。可以認爲應用程序的業務邏輯都是在ChannelInboundHandler中來處理的,業務羅的生命週期在ChannelInboundHandler中。
        Netty連接客戶端端或綁定服務器需要知道如何發送或接收消息,這是通過不同類型的handlers來做的,多個Handlers是怎麼配置的?Netty提供了ChannelInitializer類用來配置Handlers。ChannelInitializer是通過ChannelPipeline來添加ChannelHandler的,如發送和接收消息,這些Handlers將確定發的是什麼消息。ChannelInitializer自身也是一個ChannelHandler,在添加完其他的handlers之後會自動從ChannelPipeline中刪除自己。
        所有的Netty程序都是基於ChannelPipeline。ChannelPipeline和EventLoop和EventLoopGroup密切相關,因爲它們三個都和事件處理相關,所以這就是爲什麼它們處理IO的工作由EventLoop管理的原因。
        Netty中所有的IO操作都是異步執行的,例如你連接一個主機默認是異步完成的;寫入/發送消息也是同樣是異步。也就是說操作不會直接執行,而是會等一會執行,因爲你不知道返回的操作結果是成功還是失敗,但是需要有檢查是否成功的方法或者是註冊監聽來通知;Netty使用Futures和ChannelFutures來達到這種目的。Future註冊一個監聽,當操作成功或失敗時會通知。ChannelFuture封裝的是一個操作的相關信息,操作被執行時會立刻返回ChannelFuture。

3.2 Channels,Events and Input/Output(IO)

        Netty是一個非阻塞、事件驅動的網絡框架。Netty實際上是使用多線程處理IO事件,對於熟悉多線程編程的讀者可能會需要同步代碼。這樣的方式不好,因爲同步會影響程序的性能,Netty的設計保證程序處理事件不會有同步。
        下圖顯示一個EventLoopGroup和一個Channel關聯一個單一的EventLoop,Netty中的EventLoopGroup包含一個或多個EventLoop,而EventLoop就是一個Channel執行實際工作的線程。EventLoop總是綁定一個單一的線程,在其生命週期內不會改變。

當註冊一個Channel後,Netty將這個Channel綁定到一個EventLoop,在Channel的生命週期內總是被綁定到一個EventLoop。在Netty IO操作中,你的程序不需要同步,因爲一個指定通道的所有IO始終由同一個線程來執行。
        爲了幫助理解,下圖顯示了EventLoop和EventLoopGroup的關係:

EventLoop和EventLoopGroup的關聯不是直觀的,因爲我們說過EventLoopGroup包含一個或多個EventLoop,但是上面的圖顯示EventLoop是一個EventLoopGroup,這意味着你可以只使用一個特定的EventLoop。

3.3 什麼是Bootstrap?爲什麼使用它?

        “引導”是Netty中配置程序的過程,當你需要連接客戶端或服務器綁定指定端口時需要使用bootstrap。如前面所述,“引導”有兩種類型,一種是用於客戶端的Bootstrap(也適用於DatagramChannel),一種是用於服務端的ServerBootstrap。不管程序使用哪種協議,無論是創建一個客戶端還是服務器都需要使用“引導”。
        兩種bootsstraps之間有一些相似之處,其實他們有很多相似之處,也有一些不同。Bootstrap和ServerBootstrap之間的差異:
  • Bootstrap用來連接遠程主機,有1個EventLoopGroup
  • ServerBootstrap用來綁定本地端口,有2個EventLoopGroup
        事件組(Groups),傳輸(transports)和處理程序(handlers)分別在本章後面講述,我們在這裏只討論兩種"引導"的差異(Bootstrap和ServerBootstrap)。第一個差異很明顯,“ServerBootstrap”監聽在服務器監聽一個端口輪詢客戶端的“Bootstrap”或DatagramChannel是否連接服務器。通常需要調用“Bootstrap”類的connect()方法,但是也可以先調用bind()再調用connect()進行連接,之後使用的Channel包含在bind()返回的ChannelFuture中。
        第二個差別也許是最重要的。客戶端bootstraps/applications使用一個單例EventLoopGroup,而ServerBootstrap使用2個EventLoopGroup(實際上使用的是相同的實例),它可能不是顯而易見的,但是它是個好的方案。一個ServerBootstrap可以認爲有2個channels組,第一組包含一個單例ServerChannel,代表持有一個綁定了本地端口的socket;第二組包含所有的Channel,代表服務器已接受了的連接。下圖形象的描述了這種情況:

上圖中,EventLoopGroup A唯一的目的就是接受連接然後交給EventLoopGroup B。Netty可以使用兩個不同的Group,因爲服務器程序需要接受很多客戶端連接的情況下,一個EventLoopGroup將是程序性能的瓶頸,因爲事件循環忙於處理連接請求,沒有多餘的資源和空閒來處理業務邏輯,最後的結果會是很多連接請求超時。若有兩EventLoops, 即使在高負載下,所有的連接也都會被接受,因爲EventLoops接受連接不會和哪些已經連接了的處理共享資源。
         EventLoopGroup和EventLoop是什麼關係?EventLoopGroup可以包含很多個EventLoop,每個Channel綁定一個EventLoop不會被改變,因爲EventLoopGroup包含少量的EventLoop的Channels,很多Channel會共享同一個EventLoop。這意味着在一個Channel保持EventLoop繁忙會禁止其他Channel綁定到相同的EventLoop。我們可以理解爲EventLoop是一個事件循環線程,而EventLoopGroup是一個事件循環集合。
        如果你決定兩次使用相同的EventLoopGroup實例配置Netty服務器,下圖顯示了它是如何改變的:

Netty允許處理IO和接受連接使用同一個EventLoopGroup,這在實際中適用於多種應用。上圖顯示了一個EventLoopGroup處理連接請求和IO操作。
        下一節我們將介紹Netty是如何執行IO操作以及在什麼時候執行。

3.4 Channel Handlers and Data Flow(通道處理和數據流)

        本節我們一起來看看當你發送或接收數據時發生了什麼?回想本章開始提到的handler概念。要明白Netty程序wirte或read時發生了什麼,首先要對Handler是什麼有一定的瞭解。Handlers自身依賴於ChannelPipeline來決定它們執行的順序,因此不可能通過ChannelPipeline定義處理程序的某些方面,反過來不可能定義也不可能通過ChannelHandler定義ChannelPipeline的某些方面。沒必要說我們必須定義一個自己和其他的規定。本節將介紹ChannelHandler和ChannelPipeline在某種程度上細微的依賴。
        在很多地方,Netty的ChannelHandler是你的應用程序中處理最多的。即使你沒有意思到這一點,若果你使用Netty應用將至少有一個ChannelHandler參與,換句話說,ChannelHandler對很多事情是關鍵的。那麼ChannelHandler究竟是什麼?給ChannelHandler一個定義不容易,我們可以理解爲ChannelHandler是一段執行業務邏輯處理數據的代碼,它們來來往往的通過ChannelPipeline。實際上,ChannelHandler是定義一個handler的父接口,ChannelInboundHandler和ChannelOutboundHandler都實現ChannelHandler接口,如下圖:

上圖顯示的比較容易,更重要的是ChannelHandler在數據流方面的應用,在這裏討論的例子只是一個簡單的例子。ChannelHandler被應用在許多方面,在本書中會慢慢學習。
        Netty中有兩個方向的數據流,上圖顯示的入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之間有一個明顯的區別:若數據是從用戶應用程序到遠程主機則是“出站(outbound)”,相反若數據時從遠程主機到用戶應用程序則是“入站(inbound)”。
        爲了使數據從一端到達另一端,一個或多個ChannelHandler將以某種方式操作數據。這些ChannelHandler會在程序的“引導”階段被添加ChannelPipeline中,並且被添加的順序將決定處理數據的順序。ChannelPipeline的作用我們可以理解爲用來管理ChannelHandler的一個容器,每個ChannelHandler處理各自的數據(例如入站數據只能由ChannelInboundHandler處理),處理完成後將轉換的數據放到ChannelPipeline中交給下一個ChannelHandler繼續處理,直到最後一個ChannelHandler處理完成。
        下圖顯示了ChannelPipeline的處理過程:

上圖顯示ChannelInboundHandler和ChannelOutboundHandler都要經過相同的ChannelPipeline。
        在ChannelPipeline中,如果消息被讀取或有任何其他的入站事件,消息將從ChannelPipeline的頭部開始傳遞給第一個ChannelInboundHandler,這個ChannelInboundHandler可以處理該消息或將消息傳遞到下一個ChannelInboundHandler中,一旦在ChannelPipeline中沒有剩餘的ChannelInboundHandler後,ChannelPipeline就知道消息已被所有的餓Handler處理完成了。
        反過來也是如此,任何出站事件或寫入將從ChannelPipeline的尾部開始,並傳遞到最後一個ChannelOutboundHandler。ChannelOutboundHandler的作用和ChannelInboundHandler相同,它可以傳遞事件消息到下一個Handler或者自己處理消息。不同的是ChannelOutboundHandler是從ChannelPipeline的尾部開始,而ChannelInboundHandler是從ChannelPipeline的頭部開始,當處理完第一個ChannelOutboundHandler處理完成後會出發一些操作,比如一個寫操作。
        一個事件能傳遞到下一個ChannelInboundHandler或上一個ChannelOutboundHandler,在ChannelPipeline中通過使用ChannelHandlerContext調用每一個方法。Netty提供了抽象的事件基類稱爲ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。每個都提供了在ChannelPipeline中通過調用相應的方法將事件傳遞給下一個Handler的方法的實現。我們能覆蓋的方法就是我們需要做的處理。
        可能有讀者會奇怪,出站和入站的操作不同,能放在同一個ChannelPipeline工作?Netty的設計是很巧妙的,入站和出站Handler有不同的實現,Netty能跳過一個不能處理的操作,所以在出站事件的情況下,ChannelInboundHandler將被跳過,Netty知道每個handler都必須實現ChannelInboundHandler或ChannelOutboundHandler。
        當一個ChannelHandler添加到ChannelPipeline中時獲得一個ChannelHandlerContext。通常是安全的獲得這個對象的引用,但是當一個數據報協議如UDP時這是不正確的,這個對象可以在之後用來獲取底層通道,因爲要用它來read/write消息,因此通道會保留。也就是說Netty中發送消息有兩種方法:直接寫入通道或寫入ChannelHandlerContext對象。這兩種方法的主要區別如下:
  • 直接寫入通道導致處理消息從ChannelPipeline的尾部開始
  • 寫入ChannelHandlerContext對象導致處理消息從ChannelPipeline的下一個handler開始

3.5 編碼器、解碼器和業務邏輯:細看Handlers

        如前面所說,有很多不同類型的handlers,每個handler的依賴於它們的基類。Netty提供了一系列的“Adapter”類,這讓事情變的很簡單。每個handler負責轉發時間到ChannelPipeline的下一個handler。在*Adapter類(和子類)中是自動完成的,因此我們只需要在感興趣的*Adapter中重寫方法。這些功能可以幫助我們非常簡單的編碼/解碼消息。有幾個適配器(adapter)允許自定義ChannelHandler,一般自定義ChannelHandler需要繼承編碼/解碼適配器類中的一個。Netty有一下適配器:
  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
三個ChannelHandler漲,我們重點看看ecoders,decoders和SimpleChannelInboundHandler<I>,SimpleChannelInboundHandler<I>繼承ChannelInboundHandlerAdapter。

3.5.1 Encoders(編碼器), decoders(解碼器)

        發送或接收消息後,Netty必須將消息數據從一種形式轉化爲另一種。接收消息後,需要將消息從字節碼轉成Java對象(由某種解碼器解碼);發送消息前,需要將Java對象轉成字節(由某些類型的編碼器進行編碼)。這種轉換一般發生在網絡程序中,因爲網絡上只能傳輸字節數據。
        有多種基礎類型的編碼器和解碼器,要使用哪種取決於想實現的功能。要弄清楚某種類型的編解碼器,從類名就可以看出,如“ByteToMessageDecoder”、“MessageToByteEncoder”,還有Google的協議“ProtobufEncoder”和“ProtobufDecoder”。
        嚴格的說其他handlers可以做編碼器和適配器,使用不同的Adapter classes取決你想要做什麼。如果是解碼器則有一個ChannelInboundHandlerAdapter或ChannelInboundHandler,所有的解碼器都繼承或實現它們。“channelRead”方法/事件被覆蓋,這個方法從入站(inbound)通道讀取每個消息。重寫的channelRead方法將調用每個解碼器的“decode”方法並通過ChannelHandlerContext.fireChannelRead(Object msg)傳遞給ChannelPipeline中的下一個ChannelInboundHandler。
        類似入站消息,當你發送一個消息出去(出站)時,除編碼器將消息轉成字節碼外還會轉發到下一個ChannelOutboundHandler。

3.5.2 業務邏輯(Domain logic)

        也許最常見的是應用程序處理接收到消息後進行解碼,然後供相關業務邏輯模塊使用。所以應用程序只需要擴展SimpleChannelInboundHandler<I>,也就是我們自定義一個繼承SimpleChannelInboundHandler<I>的handler類,其中<I>是handler可以處理的消息類型。通過重寫父類的方法可以獲得一個ChannelHandlerContext的引用,它們接受一個ChannelHandlerContext的參數,你可以在class中當一個屬性存儲。
        處理程序關注的主要方法是“channelRead0(ChannelHandlerContext ctx, I msg)”,每當Netty調用這個方法,對象“I”是消息,這裏使用了Java的泛型設計,程序就能處理I。如何處理消息完全取決於程序的需要。在處理消息時有一點需要注意的,在Netty中事件處理IO一般有很多線程,程序中儘量不要阻塞IO線程,因爲阻塞會降低程序的性能。
        必須不阻塞IO線程意味着在ChannelHandler中使用阻塞操作會有問題。幸運的是Netty提供瞭解決方案,我們可以在添加ChannelHandler到ChannelPipeline中時指定一個EventExecutorGroup,EventExecutorGroup會獲得一個EventExecutor,EventExecutor將執行ChannelHandler的所有方法。EventExecutor將使用不同的線程來執行和釋放EventLoop。
發佈了32 篇原創文章 · 獲贊 11 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章