Netty那點事: 概述, Netty中的buffer, Channel與Pipeline

  •   Netty那點事(一)概述

Netty和Mina是Java世界很是知名的通信框架。它們都出自同一個做者,Mina誕生略早,屬於Apache基金會,而Netty開始在Jboss名下,後來出來自立門戶netty.io。關於Mina已有@FrankHui的Mina系列文章,我正好最近也要作一些網絡方面的開發,就研究一下Netty的源碼,順便分享出來了。html

Netty目前有兩個分支:4.x和3.x。4.0分支重寫了不少東西,並對項目進行了分包,規模比較龐大,入手會困難一些,而3.x版本則已經被普遍使用。本系列文章針對netty 3.7.0 final。3.x和4.0的區別能夠參考這篇文章:http://www.oschina.net/translate/netty-4-0-new-and-noteworthy?printjava

起:Netty是什麼

大概用Netty的,不管新手仍是老手,都知道它是一個「網絡通信框架」。所謂框架,基本上都是一個做用:基於底層API,提供更便捷的編程模型。那麼"通信框架"到底作了什麼事情呢?回答這個問題並不太容易,咱們不妨反過來看看,不使用netty,直接基於NIO編寫網絡程序,你須要作什麼(以Server端TCP鏈接爲例,這裏咱們使用Reactor模型):git

  1. 監聽端口,創建Socket鏈接
  2. 創建線程,處理內容
    1. 讀取Socket內容,並對協議進行解析
    2. 進行邏輯處理
    3. 回寫響應內容
    4. 若是是屢次交互的應用(SMTP、FTP),則須要保持鏈接多進行幾回交互
  3. 關閉鏈接

創建線程是一個比較耗時的操做,同時維護線程自己也有一些開銷,因此咱們會須要多線程機制,幸虧JDK已經有很方便的多線程框架了,這裏咱們不須要花不少心思。程序員

此外,由於TCP鏈接的特性,咱們還要使用鏈接池來進行管理:github

  1. 創建TCP鏈接是比較耗時的操做,對於頻繁的通信,保持鏈接效果更好
  2. 對於併發請求,可能須要創建多個鏈接
  3. 維護多個鏈接後,每次通信,須要選擇某一可用鏈接
  4. 鏈接超時和關閉機制

想一想就以爲很複雜了!實際上,基於NIO直接實現這部分東西,即便是老手也容易出現錯誤,而使用Netty以後,你只須要關注邏輯處理部分就能夠了。算法

承:體驗Netty

這裏咱們引用Netty的example包裏的一個例子,一個簡單的EchoServer,它接受客戶端輸入,並將輸入原樣返回。其主要代碼以下:編程

public void run() { // Configure the server. ServerBootstrap bootstrap = new ServerBootstrap( new NioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); // Set up the pipeline factory. bootstrap.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() throws Exception { return Channels.pipeline(new EchoServerHandler()); } }); // Bind and start to accept incoming connections. bootstrap.bind(new InetSocketAddress(port)); } 

這裏EchoServerHandler是其業務邏輯的實現者,大體代碼以下:bootstrap

public class EchoServerHandler extends SimpleChannelUpstreamHandler { @Override public void messageReceived( ChannelHandlerContext ctx, MessageEvent e) { // Send back the received message to the remote peer. e.getChannel().write(e.getMessage()); } } 

仍是挺簡單的,不是嗎?緩存

轉:Netty背後的事件驅動機制

完成了以上一段代碼,咱們算是與Netty進行了第一次親密接觸。若是想深刻學習呢?性能優化

首先推薦Netty的官方User Guide:http://netty.io/3.7/guide/。其次,閱讀源碼是瞭解一個開源工具很是好的手段,可是Java世界的框架大多追求大而全,功能完備,若是逐個閱讀,不免迷失方向,Netty也並不例外。相反,抓住幾個重點對象,理解其領域概念及設計思想,從而理清其脈絡,至關於打通了任督二脈,之後的閱讀就再也不困難了。

理解Netty的關鍵點在哪呢?我以爲,除了NIO的相關知識,另外一個就是事件驅動的設計思想。什麼叫事件驅動?咱們回頭看看EchoServerHandler的代碼,其中的參數:public void messageReceived(ChannelHandlerContext ctx, MessageEvent e),MessageEvent就是一個事件。這個事件攜帶了一些信息,例如這裏e.getMessage()就是消息的內容,而EchoServerHandler則描述了處理這種事件的方式。一旦某個事件觸發,相應的Handler則會被調用,並進行處理。這種事件機制在UI編程裏普遍應用,而Netty則將其應用到了網絡編程領域。

在Netty裏,全部事件都來自ChannelEvent接口,這些事件涵蓋監聽端口、創建鏈接、讀寫數據等網絡通信的各個階段。而事件的處理者就是ChannelHandler,這樣,不可是業務邏輯,連網絡通信流程中底層的處理,均可以經過實現ChannelHandler來完成了。事實上,Netty內部的鏈接處理、協議編解碼、超時等機制,都是經過handler完成的。當博主弄明白其中的奧妙時,不得不佩服這種設計!

下圖描述了Netty進行事件處理的流程。Channel是鏈接的通道,是ChannelEvent的產生者,而ChannelPipeline能夠理解爲ChannelHandler的集合。

event driven in Netty

合:開啓Netty源碼之門

理解了Netty的事件驅動機制,咱們如今能夠來研究Netty的各個模塊了。Netty的包結構以下:

org
└── jboss
    └── netty
        ├── bootstrap 配置並啓動服務的類
        ├── buffer 緩衝相關類,對NIO Buffer作了一些封裝
        ├── channel 核心部分,處理鏈接
        ├── container 鏈接其餘容器的代碼
        ├── example 使用示例
        ├── handler 基於handler的擴展部分,實現協議編解碼等附加功能 ├── logging 日誌 └── util 工具類 

在這裏面,channelhandler兩部分比較複雜。咱們不妨與Netty官方的結構圖對照一下,來了解其功能。

components in Netty

具體的解釋能夠看這裏:http://netty.io/3.7/guide/#architecture。圖中能夠看到,除了以前說到的事件驅動機制以外,Netty的核心功能還包括兩部分:

  • Zero-Copy-Capable Rich Byte Buffer

    零拷貝的Buffer。爲何叫零拷貝?由於在數據傳輸時,最終處理的數據會須要對單個傳輸層的報文,進行組合或者拆分。NIO原生的ByteBuffer要作到這件事,須要對ByteBuffer內容進行拷貝,產生新的ByteBuffer,而Netty經過提供Composite(組合)和Slice(切分)兩種Buffer來實現零拷貝。這部分代碼在org.jboss.netty.buffer包中。

  • Universal Communication API

    統一的通信API。由於Java的Old I/O和New I/O,使用了互不兼容的API,而Netty則提供了統一的API(org.jboss.netty.channel.Channel)來封裝這兩種I/O模型。這部分代碼在org.jboss.netty.channel包中。

此外,Protocol Support功能經過handler機制實現。

接下來的文章,咱們會根據模塊,詳細的對Netty源碼進行分析。

最後附上Netty那點事系列文章/代碼的Github地址:https://github.com/code4craft/netty-learning

參考資料:

 

Netty那點事(二)Netty中的buffer

上一篇文章咱們概要介紹了Netty的原理及結構,下面幾篇文章咱們開始對Netty的各個模塊進行比較詳細的分析。Netty的結構最底層是buffer機制,這部分也相對獨立,咱們就先從buffer講起。

What: buffer二三事

buffer中文名又叫緩衝區,按照維基百科的解釋,是"在數據傳輸時,在內存裏開闢的一塊臨時保存數據的區域」。它實際上是一種化同步爲異步的機制,能夠解決數據傳輸的速率不對等以及不穩定的問題。

根據這個定義,咱們能夠知道涉及I/O(特別是I/O寫)的地方,基本會有buffer的存在。就Java來講,咱們很是熟悉的Old I/O–InputStream&OutputStream系列API,基本都是在內部使用到了buffer。Java課程老師就教過,必須調用OutputStream.flush(),才能保證數據寫入生效!

而NIO中則直接將buffer這個概念封裝成了對象,其中最經常使用的大概是ByteBuffer了。因而使用方式變爲了:將數據寫入Buffer,flip()一下,而後將數據讀出來。因而,buffer的概念更加深刻人心了!

Netty中的buffer也不例外。不一樣的是,Netty的buffer專爲網絡通信而生,因此它又叫ChannelBuffer(好吧其實沒有什麼因果關係…)。咱們下面就來說講Netty中的buffer。固然,關於Netty,咱們必須講講它的所謂"Zero-Copy-Capable"機制。

When & Where: TCP/IP協議與buffer

TCP/IP協議是目前的主流網絡協議。它是一個多層協議,最下層是物理層,最上層是應用層(HTTP協議等),而在Java開發中,通常只接觸TCP以上,即傳輸層和應用層的內容。這也是Netty的主要應用場景。

TCP報文有個比較大的特色,就是它傳輸的時候,會先把應用層的數據項拆開成字節,而後按照本身的傳輸須要,選擇合適數量的字節進行傳輸。什麼叫"本身的傳輸須要」?首先TCP包有最大長度限制,那麼太大的數據項確定是要拆開的。其次由於TCP以及下層協議會附加一些協議頭信息,若是數據項過小,那麼可能報文大部分都是沒有價值的頭信息,這樣傳輸是很不划算的。所以有了收集必定數量的小數據,並打包傳輸的Nagle算法(這個東東在HTTP協議裏會很討厭,Netty裏能夠用setOption(「tcpNoDelay」, true)關掉它)。

這麼說可能太學院派了一點,咱們舉個例子吧:

發送時,咱們這樣分3次寫入('|'表示兩個buffer的分隔):

+-----+-----+-----+ | ABC | DEF | GHI | +-----+-----+-----+ 

接收時,可能變成了這樣:

+----+-------+---+---+ | AB | CDEFG | H | I | +----+-------+---+---+ 

很好懂吧?但是,說了這麼多,跟buffer有個什麼關係呢?別急,咱們來看下面一部分。

Why: buffer中的分層思想

咱們先回到以前的messageReceived方法:

public void messageReceived(
        ChannelHandlerContext ctx, MessageEvent e) { // Send back the received message to the remote peer. transferredBytes.addAndGet(((ChannelBuffer) e.getMessage()).readableBytes()); e.getChannel().write(e.getMessage()); } 

這裏MessageEvent.getMessage()默認的返回值是一個ChannelBuffer。咱們知道,業務中須要的"Message」,實際上是一條應用層級別的完整消息,而通常的buffer工做在傳輸層,與"Message"是不能對應上的。那麼這個ChannelBuffer是什麼呢?

來一個官方給的圖,我想這個答案就很明顯了:

virtual buffer in Netty

這裏能夠看到,TCP層HTTP報文被分紅了兩個ChannelBuffer,這兩個Buffer對咱們上層的邏輯(HTTP處理)是沒有意義的。可是兩個ChannelBuffer被組合起來,就成爲了一個有意義的HTTP報文,這個報文對應的ChannelBuffer,纔是能稱之爲"Message"的東西。這裏用到了一個詞"Virtual Buffer」,也就是所謂的"Zero-Copy-Capable Byte Buffer"了。頓時以爲豁然開朗了有沒有!

我這裏總結一下,若是說NIO的Buffer和Netty的ChannelBuffer最大的區別的話,就是前者僅僅是傳輸上的Buffer,然後者實際上是傳輸Buffer和抽象後的邏輯Buffer的結合。延伸開來講,NIO僅僅是一個網絡傳輸框架,而Netty是一個網絡應用框架,包括網絡以及應用的分層結構。

固然,在Netty裏,默認使用ChannelBuffer表示"Message」,不失爲一個比較實用的方法,可是MessageEvent.getMessage()是能夠存放一個POJO的,這樣子抽象程度又高了一些,這個咱們在之後講到ChannelPipeline的時候會說到。

How: Netty中的ChannelBuffer及實現

好了,終於來到了代碼實現部分。之因此囉嗦了這麼多,由於我以爲,關於"Zero-Copy-Capable Rich Byte Buffer」,理解爲何須要它,比理解它是怎麼實現的,可能要更重要一點。

我想可能不少朋友跟我同樣,喜歡"順藤摸瓜"式讀代碼–找到一個入口,而後順着查看它的調用,直到理解清楚。很幸運,ChannelBuffers(注意有s!)就是這樣一根"藤」,它是全部ChannelBuffer實現類的入口,它提供了不少靜態的工具方法來建立不一樣的Buffer,靠「順藤摸瓜」式讀代碼方式,大體能把各類ChannelBuffer的實現類摸個遍。先列一下ChannelBuffer相關類圖。

channel buffer in Netty

此外還有WrappedChannelBuffer系列也是繼承自AbstractChannelBuffer,圖放到了後面。

ChannelBuffer中的readerIndex和writerIndex

開始覺得Netty的ChannelBuffer是對NIO ByteBuffer的一個封裝,其實不是的,它是把ByteBuffer從新實現了一遍

以最經常使用的HeapChannelBuffer爲例,其底層也是一個byte[],與ByteBuffer不一樣的是,它是能夠同時進行讀和寫的,而不須要使用flip()進行讀寫切換。ChannelBuffer讀寫的核心代碼在AbstactChannelBuffer裏,這裏經過readerIndex和writerIndex兩個整數,分別指向當前讀的位置和當前寫的位置,而且,readerIndex老是小於writerIndex的。貼兩段代碼,讓你們能看的更明白一點:

public void writeByte(int value) { setByte(writerIndex ++, value); } public byte readByte() { if (readerIndex == writerIndex) { throw new IndexOutOfBoundsException("Readable byte limit exceeded: " + readerIndex); } return getByte(readerIndex ++); } public int writableBytes() { return capacity() - writerIndex; } public int readableBytes() { return writerIndex - readerIndex; } 

我卻是以爲這樣的方式很是天然,比單指針與flip()要更加好理解一些。AbstactChannelBuffer還有兩個相應的mark指針markedReaderIndexmarkedWriterIndex,跟NIO的原理是同樣的,這裏再也不贅述了。

字節序Endianness與HeapChannelBuffer

在建立Buffer時,咱們注意到了這樣一個方法:public static ChannelBuffer buffer(ByteOrder endianness, int capacity);,其中ByteOrder是什麼意思呢?

這裏有個很基礎的概念:字節序(ByteOrder/Endianness)。它規定了多餘一個字節的數字(int啊long什麼的),如何在內存中表示。BIG_ENDIAN(大端序)表示高位在前,整型數12會被存儲爲0 0 0 12四字節,而LITTLE_ENDIAN則正好相反。可能搞C/C++的程序員對這個會比較熟悉,而Javaer則比較陌生一點,由於Java已經把內存給管理好了。可是在網絡編程方面,根據協議的不一樣,不一樣的字節序也可能會被用到。目前大部分協議仍是採用大端序,可參考RFC1700

瞭解了這些知識,咱們也很容易就知道爲何會有BigEndianHeapChannelBufferLittleEndianHeapChannelBuffer了!

DynamicChannelBuffer

DynamicChannelBuffer是一個很方便的Buffer,之因此叫Dynamic是由於它的長度會根據內容的長度來擴充,你能夠像使用ArrayList同樣,無須關心其容量。實現自動擴容的核心在於ensureWritableBytes方法,算法很簡單:在寫入前作容量檢查,容量不夠時,新建一個容量x2的buffer,跟ArrayList的擴容是相同的。貼一段代碼吧(爲了代碼易懂,這裏我刪掉了一些邊界檢查,只保留主邏輯):

public void writeByte(int value) { ensureWritableBytes(1); super.writeByte(value); } public void ensureWritableBytes(int minWritableBytes) { if (minWritableBytes <= writableBytes()) { return; } int newCapacity = capacity(); int minNewCapacity = writerIndex() + minWritableBytes; while (newCapacity < minNewCapacity) { newCapacity <<= 1; } ChannelBuffer newBuffer = factory().getBuffer(order(), newCapacity); newBuffer.writeBytes(buffer, 0, writerIndex()); buffer = newBuffer; } 

CompositeChannelBuffer

CompositeChannelBuffer是由多個ChannelBuffer組合而成的,能夠看作一個總體進行讀寫。這裏有一個技巧:CompositeChannelBuffer並不會開闢新的內存並直接複製全部ChannelBuffer內容,而是直接保存了全部ChannelBuffer的引用,並在子ChannelBuffer裏進行讀寫,從而實現了"Zero-Copy-Capable"了。來段簡略版的代碼吧:

public class CompositeChannelBuffer{ //components保存全部內部ChannelBuffer private ChannelBuffer[] components; //indices記錄在整個CompositeChannelBuffer中,每一個components的起始位置 private int[] indices; //緩存上一次讀寫的componentId private int lastAccessedComponentId; public byte getByte(int index) { //經過indices中記錄的位置索引到對應第幾個子Buffer int componentId = componentId(index); return components[componentId].getByte(index - indices[componentId]); } public void setByte(int index, int value) { int componentId = componentId(index); components[componentId].setByte(index - indices[componentId], value); } } 

查找componentId的算法再次不做介紹了,你們本身實現起來也不會太難。值得一提的是,基於ChannelBuffer連續讀寫的特性,使用了順序查找(而不是二分查找),而且用lastAccessedComponentId來進行緩存。

ByteBufferBackedChannelBuffer

前面說ChannelBuffer是本身的實現的,其實只說對了一半。ByteBufferBackedChannelBuffer就是封裝了NIO ByteBuffer的類,用於實現堆外內存的Buffer(使用NIO的DirectByteBuffer)。固然,其實它也能夠放其餘的ByteBuffer的實現類。代碼實現就不說了,也沒啥可說的。

WrappedChannelBuffer

virtual buffer in Netty

WrappedChannelBuffer都是幾個對已有ChannelBuffer進行包裝,完成特定功能的類。代碼不貼了,實現都比較簡單,列一下功能吧。

類名 入口 功能
SlicedChannelBuffer ChannelBuffer.slice()
ChannelBuffer.slice(int,int)
某個ChannelBuffer的一部分
TruncatedChannelBuffer ChannelBuffer.slice()
ChannelBuffer.slice(int,int)
某個ChannelBuffer的一部分, 能夠理解爲其實位置爲0的SlicedChannelBuffer
DuplicatedChannelBuffer ChannelBuffer.duplicate() 與某個ChannelBuffer使用一樣的存儲, 區別是有本身的index
ReadOnlyChannelBuffer ChannelBuffers 
.unmodifiableBuffer(ChannelBuffer)
只讀,你懂的

能夠看到,關於實現方面,Netty 3.7的buffer相關內容仍是比較簡單的,也沒有太多費腦細胞的地方。

而Netty 4.0以後就不一樣了。4.0,ChannelBuffer更名ByteBuf,成了單獨項目buffer,而且爲了性能優化,加入了BufferPool之類的機制,已經變得比較複雜了(本質倒沒怎麼變)。性能優化是個很複雜的事情,研究源碼時,建議先避開這些東西,除非你對算法情有獨鍾。舉個例子,Netty4.0裏爲了優化,將Map換成了Java 8裏6000行的ConcurrentHashMapV8,大家感覺一下…

下篇文章咱們開始講Channel。

參考資料:

 

 

 


Netty那點事(三)Channel與Pipeline

Channel是理解和使用Netty的核心。Channel的涉及內容較多,這裏我使用由淺入深的介紹方法。在這篇文章中,咱們主要介紹Channel部分中Pipeline實現機制。爲了不枯燥,借用一下《盜夢空間》的「夢境」概念,但願你們喜歡。

一層夢境:Channel實現概覽

在Netty裏,Channel是通信的載體,而ChannelHandler負責Channel中的邏輯處理。

那麼ChannelPipeline是什麼呢?我以爲能夠理解爲ChannelHandler的容器:一個Channel包含一個ChannelPipeline,全部ChannelHandler都會註冊到ChannelPipeline中,並按順序組織起來。

在Netty中,ChannelEvent是數據或者狀態的載體,例如傳輸的數據對應MessageEvent,狀態的改變對應ChannelStateEvent。當對Channel進行操做時,會產生一個ChannelEvent,併發送到ChannelPipeline。ChannelPipeline會選擇一個ChannelHandler進行處理。這個ChannelHandler處理以後,可能會產生新的ChannelEvent,並流轉到下一個ChannelHandler。

channel pipeline

例如,一個數據最開始是一個MessageEvent,它附帶了一個未解碼的原始二進制消息ChannelBuffer,而後某個Handler將其解碼成了一個數據對象,並生成了一個新的MessageEvent,並傳遞給下一步進行處理。

到了這裏,能夠看到,其實Channel的核心流程位於ChannelPipeline中。因而咱們進入ChannelPipeline的深層夢境裏,來看看它具體的實現。

二層夢境:ChannelPipeline的主流程

Netty的ChannelPipeline包含兩條線路:Upstream和Downstream。Upstream對應上行,接收到的消息、被動的狀態改變,都屬於Upstream。Downstream則對應下行,發送的消息、主動的狀態改變,都屬於Downstream。ChannelPipeline接口包含了兩個重要的方法:sendUpstream(ChannelEvent e)sendDownstream(ChannelEvent e),就分別對應了Upstream和Downstream。

對應的,ChannelPipeline裏包含的ChannelHandler也包含兩類:ChannelUpstreamHandlerChannelDownstreamHandler。每條線路的Handler是互相獨立的。它們都很簡單的只包含一個方法:ChannelUpstreamHandler.handleUpstreamChannelDownstreamHandler.handleDownstream

Netty官方的javadoc裏有一張圖(ChannelPipeline接口裏),很是形象的說明了這個機制(我對原圖進行了一點修改,加上了ChannelSink,由於我以爲這部分對理解代碼流程會有些幫助):

channel pipeline

什麼叫ChannelSink呢?ChannelSink包含一個重要方法ChannelSink.eventSunk,能夠接受任意ChannelEvent。「sink"的意思是"下沉」,那麼"ChannelSink"好像能夠理解爲"Channel下沉的地方」?實際上,它的做用確實是這樣,也能夠換個說法:「處於末尾的萬能Handler」。最初讀到這裏,也有些困惑,這麼理解以後,就感受簡單許多。只有Downstream包含ChannelSink,這裏會作一些創建鏈接、綁定端口等重要操做。爲何UploadStream沒有ChannelSink呢?我只能認爲,一方面,不符合"sink"的意義,另外一方面,也沒有什麼處理好作的吧!

這裏有個值得注意的地方:在一條「流」裏,一個ChannelEvent並不會主動的"流"經全部的Handler,而是由上一個Handler顯式的調用ChannelPipeline.sendUp(Down)stream產生,並交給下一個Handler處理。也就是說,每一個Handler接收到一個ChannelEvent,並處理結束後,若是須要繼續處理,那麼它須要調用sendUp(Down)stream新發起一個事件。若是它再也不發起事件,那麼處理就到此結束,即便它後面仍然有Handler沒有執行。這個機制能夠保證最大的靈活性,固然對Handler的前後順序也有了更嚴格的要求。

順便說一句,在Netty 3.x裏,這個機制會致使大量的ChannelEvent對象建立,所以Netty 4.x版本對此進行了改進。twitter的finagle框架實踐中,就提到從Netty 3.x升級到Netty 4.x,能夠大大下降GC開銷。有興趣的能夠看看這篇文章:https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead

下面咱們從代碼層面來對這裏面發生的事情進行深刻分析,這部分涉及到一些細節,須要打開項目源碼,對照來看,會比較有收穫。

三層夢境:深刻ChannelPipeline內部

DefaultChannelPipeline的內部結構

ChannelPipeline的主要的實現代碼在DefaultChannelPipeline類裏。列一下DefaultChannelPipeline的主要字段:

public class DefaultChannelPipeline implements ChannelPipeline { private volatile Channel channel; private volatile ChannelSink sink; private volatile DefaultChannelHandlerContext head; private volatile DefaultChannelHandlerContext tail; private final Map<String, DefaultChannelHandlerContext> name2ctx = new HashMap<String, DefaultChannelHandlerContext>(4); } 

這裏須要介紹一下ChannelHandlerContext這個接口。顧名思義,ChannelHandlerContext保存了Netty與Handler相關的的上下文信息。而我們這裏的DefaultChannelHandlerContext,則是對ChannelHandler的一個包裝。一個DefaultChannelHandlerContext內部,除了包含一個ChannelHandler,還保存了"next"和"prev"兩個指針,從而造成一個雙向鏈表。

所以,在DefaultChannelPipeline中,咱們看到的是對DefaultChannelHandlerContext的引用,而不是對ChannelHandler的直接引用。這裏包含"head"和"tail"兩個引用,分別指向鏈表的頭和尾。而name2ctx則是一個按名字索引DefaultChannelHandlerContext用戶的一個map,主要在按照名稱刪除或者添加ChannelHandler時使用。

sendUpstream和sendDownstream

前面提到了,ChannelPipeline接口的兩個重要的方法:sendUpstream(ChannelEvent e)sendDownstream(ChannelEvent e)全部事件的發起都是基於這兩個方法進行的。Channels類有一系列fireChannelBound之類的fireXXXX方法,其實都是對這兩個方法的facade包裝。

下面來看一下這兩個方法的實現(對代碼作了一些簡化,保留主邏輯):

public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext head = getActualUpstreamContext(this.head); head.getHandler().handleUpstream(head, e); } private DefaultChannelHandlerContext getActualUpstreamContext(DefaultChannelHandlerContext ctx) { DefaultChannelHandlerContext realCtx = ctx; while (!realCtx.canHandleUpstream()) { realCtx = realCtx.next; if (realCtx == null) { return null; } } return realCtx; } 

這裏最終調用了ChannelUpstreamHandler.handleUpstream來處理這個ChannelEvent。有意思的是,這裏咱們看不到任何"將Handler向後移一位"的操做,可是咱們總不能每次都用同一個Handler來進行處理啊?實際上,咱們更爲經常使用的是ChannelHandlerContext.handleUpstream方法(實現是DefaultChannelHandlerContext.sendUpstream方法):

public void sendUpstream(ChannelEvent e) { DefaultChannelHandlerContext next = getActualUpstreamContext(this.next); DefaultChannelPipeline.this.sendUpstream(next, e); } 

能夠看到,這裏最終仍然調用了ChannelPipeline.sendUpstream方法,可是它會將Handler指針後移

咱們接下來看看DefaultChannelHandlerContext.sendDownstream:

public void sendDownstream(ChannelEvent e) { DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev); if (prev == null) { try { getSink().eventSunk(DefaultChannelPipeline.this, e); } catch (Throwable t) { notifyHandlerException(e, t); } } else { DefaultChannelPipeline.this.sendDownstream(prev, e); } } 

與sendUpstream好像不大相同哦?這裏有兩點:一是到達末尾時,就如夢境二所說,會調用ChannelSink進行處理;二是這裏指針是往前移的,因此咱們知道了:

UpstreamHandler是從前日後執行的,DownstreamHandler是從後往前執行的。在ChannelPipeline裏添加時須要注意順序了!

DefaultChannelPipeline裏還有些機制,像添加/刪除/替換Handler,以及ChannelPipelineFactory等,比較好理解,就不細說了。

回到現實:Pipeline解決的問題

好了,深刻分析完代碼,有點頭暈了,咱們回到最開始的地方,來想想,Netty的Pipeline機制解決了什麼問題?

我認爲至少有兩點:

一是提供了ChannelHandler的編程模型,基於ChannelHandler開發業務邏輯,基本不須要關心網絡通信方面的事情,專一於編碼/解碼/邏輯處理就能夠了。Handler也是比較方便的開發模式,在不少框架中都有用到。

二是實現了所謂的"Universal Asynchronous API」。這也是Netty官方標榜的一個功能。用過OIO和NIO的都知道,這兩套API風格相差極大,要從一個遷移到另外一個成本是很大的。即便是NIO,異步和同步編程差距也很大。而Netty屏蔽了OIO和NIO的API差別,經過Channel提供對外接口,並經過ChannelPipeline將其鏈接起來,所以替換起來很是簡單。

universal API

理清了ChannelPipeline的主流程,咱們對Channel部分的大體結構算是弄清楚了。但是到了這裏,咱們依然對一個鏈接具體怎麼處理沒有什麼概念,下篇文章,咱們會分析一下,在Netty中,捷徑如何處理鏈接的創建、數據的傳輸這些事情。

PS: Pipeline這部分拖了兩個月,終於寫完了。中間寫的實在緩慢,寫個高質量(至少是自認爲吧!)的文章不容易,可是仍不忍心這部分就此爛尾。中間參考了一些優秀的文章,還本身使用netty開發了一些應用。之後這類文章,仍是要集中時間來寫無缺了。

參考資料:

相關文章
相關標籤/搜索