在這一章我們將討論Netty的10個核心類,清楚了解他們的結(jié)構(gòu)對使用Netty很有用消返∧旒眨可能有一些不會再工作中用到驼鞭,但是也有一些很常用也很核心挣棕,你會遇到。
Bootstrap or ServerBootstrap
EventLoop
EventLoopGroup
ChannelPipeline
Channel
Future or ChannelFuture
ChannelInitializer
ChannelHandler
本節(jié)的目的就是介紹以上這些概念固耘,幫助你了解它們的用法厅目。
在我們開始之前,如果你了解Netty程序的一般結(jié)構(gòu)和大致用法(客戶端和服務(wù)器都有一個類似的結(jié)構(gòu))會更好葫笼。
一個Netty程序開始于Bootstrap類拗馒,Bootstrap類是Netty提供的一個可以通過簡單配置來設(shè)置或"引導(dǎo)"程序的一個很重要的類诱桂。Netty中設(shè)計了Handlers來處理特定的"event"和設(shè)置Netty中的事件挥等,從而來處理多個協(xié)議和數(shù)據(jù)。事件可以描述成一個非常通用的方法迁客,因為你可以自定義一個handler,用來將Object轉(zhuǎn)成byte[]或?qū)yte[]轉(zhuǎn)成Object哲泊;也可以定義個handler處理拋出的異常催蝗。
你會經(jīng)常編寫一個實現(xiàn)ChannelInboundHandler的類丙号,ChannelInboundHandler是用來接收消息犬缨,當有消息過來時怀薛,你可以決定如何處理迷郑。當程序需要返回消息時可以在ChannelInboundHandler里write/flush數(shù)據(jù)嗡害“悦茫可以認為應(yīng)用程序的業(yè)務(wù)邏輯都是在ChannelInboundHandler中來處理的,業(yè)務(wù)羅的生命周期在ChannelInboundHandler中鹃骂。
Netty連接客戶端端或綁定服務(wù)器需要知道如何發(fā)送或接收消息畏线,這是通過不同類型的handlers來做的象踊,多個Handlers是怎么配置的?Netty提供了ChannelInitializer類用來配置Handlers栈虚。ChannelInitializer是通過ChannelPipeline來添加ChannelHandler的魂务,如發(fā)送和接收消息粘姜,這些Handlers將確定發(fā)的是什么消息孤紧。ChannelInitializer自身也是一個ChannelHandler号显,在添加完其他的handlers之后會自動從ChannelPipeline中刪除自己躺酒。
所有的Netty程序都是基于ChannelPipeline羹应。ChannelPipeline和EventLoop和EventLoopGroup密切相關(guān)园匹,因為它們?nèi)齻€都和事件處理相關(guān)裸违,所以這就是為什么它們處理IO的工作由EventLoop管理的原因滞详。
Netty中所有的IO操作都是異步執(zhí)行的料饥,例如你連接一個主機默認是異步完成的岸啡;寫入/發(fā)送消息也是同樣是異步巡蘸。也就是說操作不會直接執(zhí)行悦荒,而是會等一會執(zhí)行境氢,因為你不知道返回的操作結(jié)果是成功還是失敗萍聊,但是需要有檢查是否成功的方法或者是注冊監(jiān)聽來通知;Netty使用Futures和ChannelFutures來達到這種目的。Future注冊一個監(jiān)聽,當操作成功或失敗時會通知陵刹。ChannelFuture封裝的是一個操作的相關(guān)信息,操作被執(zhí)行時會立刻返回ChannelFuture。
3.2 Channels,Events and Input/Output(IO)
Netty是一個非阻塞狗热、事件驅(qū)動的網(wǎng)絡(luò)框架匿刮。Netty實際上是使用多線程處理IO事件,對于熟悉多線程編程的讀者可能會需要同步代碼绩鸣。這樣的方式不好呀闻,因為同步會影響程序的性能,Netty的設(shè)計保證程序處理事件不會有同步。
下圖顯示一個EventLoopGroup和一個Channel關(guān)聯(lián)一個單一的EventLoop,Netty中的EventLoopGroup包含一個或多個EventLoop唆迁,而EventLoop就是一個Channel執(zhí)行實際工作的線程。EventLoop總是綁定一個單一的線程,在其生命周期內(nèi)不會改變看政。
當注冊一個Channel后,Netty將這個Channel綁定到一個EventLoop做入,在Channel的生命周期內(nèi)總是被綁定到一個EventLoop。在Netty IO操作中乳怎,你的程序不需要同步彩郊,因為一個指定通道的所有IO始終由同一個線程來執(zhí)行。
為了幫助理解蚪缀,下圖顯示了EventLoop和EventLoopGroup的關(guān)系:
EventLoop和EventLoopGroup的關(guān)聯(lián)不是直觀的秫逝,因為我們說過EventLoopGroup包含一個或多個EventLoop,但是上面的圖顯示EventLoop是一個EventLoopGroup询枚,這意味著你可以只使用一個特定的EventLoop违帆。
“引導(dǎo)”是Netty中配置程序的過程金蜀,當你需要連接客戶端或服務(wù)器綁定指定端口時需要使用bootstrap刷后。如前面所述,“引導(dǎo)”有兩種類型渊抄,一種是用于客戶端的Bootstrap(也適用于DatagramChannel)尝胆,一種是用于服務(wù)端的ServerBootstrap。不管程序使用哪種協(xié)議护桦,無論是創(chuàng)建一個客戶端還是服務(wù)器都需要使用“引導(dǎo)”含衔。
兩種bootsstraps之間有一些相似之處,其實他們有很多相似之處二庵,也有一些不同贪染。Bootstrap和ServerBootstrap之間的差異:
Bootstrap用來連接遠程主機,有1個EventLoopGroup
ServerBootstrap用來綁定本地端口催享,有2個EventLoopGroup
事件組(Groups)杭隙,傳輸(transports)和處理程序(handlers)分別在本章后面講述,我們在這里只討論兩種"引導(dǎo)"的差異(Bootstrap和ServerBootstrap)因妙。第一個差異很明顯痰憎,“ServerBootstrap”監(jiān)聽在服務(wù)器監(jiān)聽一個端口輪詢客戶端的“Bootstrap”或DatagramChannel是否連接服務(wù)器。通常需要調(diào)用“Bootstrap”類的connect()方法兰迫,但是也可以先調(diào)用bind()再調(diào)用connect()進行連接,之后使用的Channel包含在bind()返回的ChannelFuture中炬称。
第二個差別也許是最重要的汁果。客戶端bootstraps/applications使用一個單例EventLoopGroup玲躯,而ServerBootstrap使用2個EventLoopGroup(實際上使用的是相同的實例)据德,它可能不是顯而易見的鳄乏,但是它是個好的方案。一個ServerBootstrap可以認為有2個channels組棘利,第一組包含一個單例ServerChannel橱野,代表持有一個綁定了本地端口的socket;第二組包含所有的Channel善玫,代表服務(wù)器已接受了的連接水援。下圖形象的描述了這種情況:
上圖中,EventLoopGroup A唯一的目的就是接受連接然后交給EventLoopGroup B茅郎。Netty可以使用兩個不同的Group蜗元,因為服務(wù)器程序需要接受很多客戶端連接的情況下,一個EventLoopGroup將是程序性能的瓶頸系冗,因為事件循環(huán)忙于處理連接請求奕扣,沒有多余的資源和空閑來處理業(yè)務(wù)邏輯,最后的結(jié)果會是很多連接請求超時掌敬。若有兩EventLoops惯豆, 即使在高負載下,所有的連接也都會被接受奔害,因為EventLoops接受連接不會和哪些已經(jīng)連接了的處理共享資源楷兽。
EventLoopGroup和EventLoop是什么關(guān)系?EventLoopGroup可以包含很多個EventLoop舀武,每個Channel綁定一個EventLoop不會被改變拄养,因為EventLoopGroup包含少量的EventLoop的Channels,很多Channel會共享同一個EventLoop银舱。這意味著在一個Channel保持EventLoop繁忙會禁止其他Channel綁定到相同的EventLoop瘪匿。我們可以理解為EventLoop是一個事件循環(huán)線程,而EventLoopGroup是一個事件循環(huán)集合寻馏。
如果你決定兩次使用相同的EventLoopGroup實例配置Netty服務(wù)器棋弥,下圖顯示了它是如何改變的:
Netty允許處理IO和接受連接使用同一個EventLoopGroup,這在實際中適用于多種應(yīng)用诚欠。上圖顯示了一個EventLoopGroup處理連接請求和IO操作顽染。
下一節(jié)我們將介紹Netty是如何執(zhí)行IO操作以及在什么時候執(zhí)行。
3.4 Channel Handlers and Data Flow(通道處理和數(shù)據(jù)流)
本節(jié)我們一起來看看當你發(fā)送或接收數(shù)據(jù)時發(fā)生了什么轰绵?回想本章開始提到的handler概念粉寞。要明白Netty程序wirte或read時發(fā)生了什么,首先要對Handler是什么有一定的了解左腔。Handlers自身依賴于ChannelPipeline來決定它們執(zhí)行的順序唧垦,因此不可能通過ChannelPipeline定義處理程序的某些方面,反過來不可能定義也不可能通過ChannelHandler定義ChannelPipeline的某些方面。沒必要說我們必須定義一個自己和其他的規(guī)定液样。本節(jié)將介紹ChannelHandler和ChannelPipeline在某種程度上細微的依賴振亮。
在很多地方巧还,Netty的ChannelHandler是你的應(yīng)用程序中處理最多的。即使你沒有意思到這一點坊秸,若果你使用Netty應(yīng)用將至少有一個ChannelHandler參與麸祷,換句話說,ChannelHandler對很多事情是關(guān)鍵的褒搔。那么ChannelHandler究竟是什么阶牍?給ChannelHandler一個定義不容易,我們可以理解為ChannelHandler是一段執(zhí)行業(yè)務(wù)邏輯處理數(shù)據(jù)的代碼站超,它們來來往往的通過ChannelPipeline。實際上死相,ChannelHandler是定義一個handler的父接口融求,ChannelInboundHandler和ChannelOutboundHandler都實現(xiàn)ChannelHandler接口,如下圖:
上圖顯示的比較容易算撮,更重要的是ChannelHandler在數(shù)據(jù)流方面的應(yīng)用生宛,在這里討論的例子只是一個簡單的例子。ChannelHandler被應(yīng)用在許多方面肮柜,在本書中會慢慢學習陷舅。
Netty中有兩個方向的數(shù)據(jù)流,上圖顯示的入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之間有一個明顯的區(qū)別:若數(shù)據(jù)是從用戶應(yīng)用程序到遠程主機則是“出站(outbound)”审洞,相反若數(shù)據(jù)時從遠程主機到用戶應(yīng)用程序則是“入站(inbound)”莱睁。
為了使數(shù)據(jù)從一端到達另一端,一個或多個ChannelHandler將以某種方式操作數(shù)據(jù)芒澜。這些ChannelHandler會在程序的“引導(dǎo)”階段被添加ChannelPipeline中仰剿,并且被添加的順序?qū)Q定處理數(shù)據(jù)的順序。ChannelPipeline的作用我們可以理解為用來管理ChannelHandler的一個容器痴晦,每個ChannelHandler處理各自的數(shù)據(jù)(例如入站數(shù)據(jù)只能由ChannelInboundHandler處理)南吮,處理完成后將轉(zhuǎn)換的數(shù)據(jù)放到ChannelPipeline中交給下一個ChannelHandler繼續(xù)處理,直到最后一個ChannelHandler處理完成誊酌。
下圖顯示了ChannelPipeline的處理過程:
上圖顯示ChannelInboundHandler和ChannelOutboundHandler都要經(jīng)過相同的ChannelPipeline部凑。
在ChannelPipeline中,如果消息被讀取或有任何其他的入站事件碧浊,消息將從ChannelPipeline的頭部開始傳遞給第一個ChannelInboundHandler涂邀,這個ChannelInboundHandler可以處理該消息或?qū)⑾鬟f到下一個ChannelInboundHandler中,一旦在ChannelPipeline中沒有剩余的ChannelInboundHandler后箱锐,ChannelPipeline就知道消息已被所有的餓Handler處理完成了比勉。
反過來也是如此,任何出站事件或?qū)懭雽腃hannelPipeline的尾部開始,并傳遞到最后一個ChannelOutboundHandler敷搪。ChannelOutboundHandler的作用和ChannelInboundHandler相同,它可以傳遞事件消息到下一個Handler或者自己處理消息幢哨。不同的是ChannelOutboundHandler是從ChannelPipeline的尾部開始赡勘,而ChannelInboundHandler是從ChannelPipeline的頭部開始,當處理完第一個ChannelOutboundHandler處理完成后會出發(fā)一些操作捞镰,比如一個寫操作闸与。
一個事件能傳遞到下一個ChannelInboundHandler或上一個ChannelOutboundHandler,在ChannelPipeline中通過使用ChannelHandlerContext調(diào)用每一個方法岸售。Netty提供了抽象的事件基類稱為ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter践樱。每個都提供了在ChannelPipeline中通過調(diào)用相應(yīng)的方法將事件傳遞給下一個Handler的方法的實現(xiàn)。我們能覆蓋的方法就是我們需要做的處理凸丸。
可能有讀者會奇怪拷邢,出站和入站的操作不同,能放在同一個ChannelPipeline工作屎慢?Netty的設(shè)計是很巧妙的瞭稼,入站和出站Handler有不同的實現(xiàn),Netty能跳過一個不能處理的操作腻惠,所以在出站事件的情況下环肘,ChannelInboundHandler將被跳過,Netty知道每個handler都必須實現(xiàn)ChannelInboundHandler或ChannelOutboundHandler集灌。
當一個ChannelHandler添加到ChannelPipeline中時獲得一個ChannelHandlerContext悔雹。通常是安全的獲得這個對象的引用,但是當一個數(shù)據(jù)報協(xié)議如UDP時這是不正確的欣喧,這個對象可以在之后用來獲取底層通道腌零,因為要用它來read/write消息,因此通道會保留续誉。也就是說Netty中發(fā)送消息有兩種方法:直接寫入通道或?qū)懭隒hannelHandlerContext對象辖试。這兩種方法的主要區(qū)別如下:
直接寫入通道導(dǎo)致處理消息從ChannelPipeline的尾部開始
寫入ChannelHandlerContext對象導(dǎo)致處理消息從ChannelPipeline的下一個handler開始
3.5 編碼器、解碼器和業(yè)務(wù)邏輯:細看Handlers
如前面所說专酗,有很多不同類型的handlers滥比,每個handler的依賴于它們的基類。Netty提供了一系列的“Adapter”類臼隔,這讓事情變的很簡單嘹裂。每個handler負責轉(zhuǎn)發(fā)時間到ChannelPipeline的下一個handler。在Adapter類(和子類)中是自動完成的摔握,因此我們只需要在感興趣的Adapter中重寫方法寄狼。這些功能可以幫助我們非常簡單的編碼/解碼消息。有幾個適配器(adapter)允許自定義ChannelHandler,一般自定義ChannelHandler需要繼承編碼/解碼適配器類中的一個泊愧。Netty有一下適配器:
ChannelHandlerAdapter
ChannelInboundHandlerAdapter
ChannelOutboundHandlerAdapter
三個ChannelHandler漲伊磺,我們重點看看ecoders,decoders和SimpleChannelInboundHandler,SimpleChannelInboundHandler繼承ChannelInboundHandlerAdapter删咱。
3.5.1 Encoders(編碼器), decoders(解碼器)
發(fā)送或接收消息后屑埋,Netty必須將消息數(shù)據(jù)從一種形式轉(zhuǎn)化為另一種。接收消息后痰滋,需要將消息從字節(jié)碼轉(zhuǎn)成Java對象(由某種解碼器解碼)摘能;發(fā)送消息前,需要將Java對象轉(zhuǎn)成字節(jié)(由某些類型的編碼器進行編碼)敲街。這種轉(zhuǎn)換一般發(fā)生在網(wǎng)絡(luò)程序中团搞,因為網(wǎng)絡(luò)上只能傳輸字節(jié)數(shù)據(jù)。
有多種基礎(chǔ)類型的編碼器和解碼器多艇,要使用哪種取決于想實現(xiàn)的功能逻恐。要弄清楚某種類型的編解碼器,從類名就可以看出峻黍,如“ByteToMessageDecoder”梢莽、“MessageToByteEncoder”,還有Google的協(xié)議“ProtobufEncoder”和“ProtobufDecoder”奸披。
嚴格的說其他handlers可以做編碼器和適配器昏名,使用不同的Adapter
classes取決你想要做什么。如果是解碼器則有一個ChannelInboundHandlerAdapter或ChannelInboundHandler阵面,所有的解碼器都繼承或?qū)崿F(xiàn)它們轻局。“channelRead”方法/事件被覆蓋样刷,這個方法從入站(inbound)通道讀取每個消息仑扑。重寫的channelRead方法將調(diào)用每個解碼器的“decode”方法并通過ChannelHandlerContext.fireChannelRead(Object
msg)傳遞給ChannelPipeline中的下一個ChannelInboundHandler。
類似入站消息置鼻,當你發(fā)送一個消息出去(出站)時镇饮,除編碼器將消息轉(zhuǎn)成字節(jié)碼外還會轉(zhuǎn)發(fā)到下一個ChannelOutboundHandler。
3.5.2 業(yè)務(wù)邏輯(Domain logic)
也許最常見的是應(yīng)用程序處理接收到消息后進行解碼箕母,然后供相關(guān)業(yè)務(wù)邏輯模塊使用储藐。所以應(yīng)用程序只需要擴展SimpleChannelInboundHandler<I>,也就是我們自定義一個繼承SimpleChannelInboundHandler<I>的handler類嘶是,其中<I>是handler可以處理的消息類型钙勃。通過重寫父類的方法可以獲得一個ChannelHandlerContext的引用,它們接受一個ChannelHandlerContext的參數(shù)聂喇,你可以在class中當一個屬性存儲辖源。
處理程序關(guān)注的主要方法是“channelRead0(ChannelHandlerContext ctx, I
msg)”,每當Netty調(diào)用這個方法,對象“I”是消息克饶,這里使用了Java的泛型設(shè)計酝蜒,程序就能處理I。如何處理消息完全取決于程序的需要矾湃。在處理消息時有一點需要注意的秕硝,在Netty中事件處理IO一般有很多線程,程序中盡量不要阻塞IO線程洲尊,因為阻塞會降低程序的性能。
必須不阻塞IO線程意味著在ChannelHandler中使用阻塞操作會有問題奈偏。幸運的是Netty提供了解決方案坞嘀,我們可以在添加ChannelHandler到ChannelPipeline中時指定一個EventExecutorGroup,EventExecutorGroup會獲得一個EventExecutor惊来,EventExecutor將執(zhí)行ChannelHandler的所有方法丽涩。EventExecutor將使用不同的線程來執(zhí)行和釋放EventLoop。