《Netty實戰》-學習筆記1
Chapter 3 Netty的元件及設計
-
Channel
:Socket,對應一個連線。- 提供基本的 I/O 操作:如
bind()
、connect()
、read()
、write()
等,就是對 Java 中實現的 Socket 進行一定層次的封裝。
- 提供基本的 I/O 操作:如
-
EventLoop
:控制流、多執行緒處理、併發。- 在一個
EventLoopGroup
中包含一個或多個EventLoop
。 - 一個
EventLoop
在它的生命週期內只和一個Thread
繫結。 - 所有
EventLoop
處理的 I/O 事件都在它專有的Thread
上被處理。 - 一個
Channel
在它的生命週期內只註冊於一個EventLoop
。 - 一個
EventLoop
可能會被分配給一個或多個Channel
。
- 在一個
-
ChannelFuture
:非同步通知。- 在 Netty 中所有操作的是非同步的,呼叫時不會立即返回結果,但是返回一個
Future
。能夠在之後的某個時間點確定結果,對Future
新增Listener
註冊回撥,再操作完成後通知執行相應的回撥。
- 在 Netty 中所有操作的是非同步的,呼叫時不會立即返回結果,但是返回一個
-
ChannelHandler
:在Channel
上發生事件的處理器。- 由網路事件觸發的處理器,實際上可以響應任何型別的動作。
ChannelInboundHandler
:接收入站事件和資料。ChannelOutboundHandler
:資料出站處理器。
-
ChannelPipeline
:ChannelHandler
鏈的容器。ChannelPipeline
就是一些ChannelHandler
的編排順序,每個ChannelHandler
接收事件,執行所實現的處理邏輯,完畢後交給下個一個ChannelHandler
。- 從客戶端角度來說,傳送資料到伺服器就是出站的。則通過
ChannelPipeline
的編排,在一定次序下呼叫ChannelOutboundHandler
。響應的接收伺服器發來資料就是入站的,在一個ChannelPipeline
按順序呼叫ChannelInboundHandler
。 - 如下圖:客戶端作業系統從網路上接收伺服器發來資料放入快取中,接下來,在 Netty 中從
ChannelPipeline
中從頭部開始按順序呼叫ChannnelInboundHandler
。從客戶端傳送資料到伺服器則是從 ChannelPipeline 的尾部開始執行,直到傳送到網路上。
-
ChannelHandlerContext
:代表了ChannelHandler
和ChannelPipeline
之間的繫結。- 可以用於獲取底層
Channel
。
- 可以用於獲取底層
-
兩種傳送訊息的方式:
- 直接寫入
Channel
中:訊息從ChannelPipeline
的尾部開始流動。 - 寫入到
ChannelHandler
相關聯的ChannelHandlerContext
物件中: 訊息從ChannelPipeline
的下一個ChannelHandler
開始流動。
- 直接寫入
Chapter 4 傳輸API
Channel
每個Channel
都會被分配一個 ChannelPipeline
和 ChannelConfig
。
ChannelConfig
:對應Channel
的配置設定,支援熱更新。ChannelPipeline
: 持有所有將應用於入站和出站資料以及事件的ChannelHandler
例項,這些ChannelHandler
實現了應用程式用於處理狀態變化以及資料處理的邏輯。
內建傳輸
名稱 | 包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio | 使用 java.nio.channels 包作為基礎——基於選擇器(select) |
Epoll | io.netty.channel.epoll | 由 JNI 驅動的 epoll() 和非阻塞 IO。這個傳輸支援只有在Linux上可用的多種特性,如 SO_REUSEPORT,比NIO 傳輸更快,而且是完全非阻塞的 |
OIO | io.netty.channel.socket.oio | 使用 java.net 包作為基礎——使用阻塞流 |
Local | io.netty.channel.local | 可以在 VM 內部通過管道進行通訊的本地傳輸 |
Embedded | io.netty.channel.embedded | Embedded 傳輸,允許使用 ChannelHandler 而又不需要一個真正的基於網路的傳輸。這在測試 ChannelHandler 實現時非常有用 |
- NIO
基於選擇器,每個
Channel
在選擇器(Selector
)上註冊,當Selector
呼叫select()
的時候是阻塞的,直到已經註冊的Channel
中有相應的事件發生。
事件 | 描述 |
---|---|
OP_ACCEPT | 請求在接受新連線並建立 Channel 時獲得通知 |
OP_CONNECT | 請求在建立一個連線時獲得通知 |
OP_READ | 請求當資料已經就緒,可以從 Channel 中讀取時獲得通知 |
OP_WRITE | 請求當可以向 Channel 中寫更多的資料時獲得通知。這處理了套接字緩衝區被完全填滿時的情況,這種情況通常發生在資料的傳送速度比遠端節點可處理的速度更快的時候 |
處理模型:
-
Epoll NIO 是基於 Select 的,那麼 Epoll 如其名,則是基於 Linux 的 epoll 。好處不言而喻,提供比舊的 select 和 poll 更好的效能。 對應的則是
EpollEventLoopGroup
和EpollServerSocketChannel
。 -
OIO 建立在
java.net
包下,不是非同步的。能夠通過設定SO_TIMEOUT
來在單執行緒中處理多個連線。 -
Embedded Embedded 提供了可以將一組
ChannelHandler
作為幫助器嵌入到其他的ChannelHandler
內部。這樣可以擴充套件一個ChannelHandler
的功能,但又不需要修改其內部程式碼。
Chapter 5 ByteBuf
ByteBuf工作原理
維護了兩個不同的索引:readerIndex
和 writerIndex
,一個用於讀取,一個用於寫入。當從 ByteBuf
讀取時,readerIndex
遞增。同樣當從 ByteBuf
寫入時, writerIndex
遞增。
如果 readerIndex
遞增到和 writerIndex
相同的值,那麼到達“可讀取資料的末尾”,這時再進行讀取則會像讀取超出陣列末尾的資料一樣,丟擲 IndexOutOfBoundsException
。
另外,名稱以 read 或者 write 開頭的 ByteBuf
方法會推進相應的索引值,而以 set 或者 get 開頭的方法則不會(這樣只是在相應的索引值上進行操作)。
使用模式
在弄清楚 ByteBuf
的使用模式之前,應該理解 Java 中的直接緩衝區和非直接緩衝區:
-
直接緩衝區:緩衝區建立在實體記憶體中,可以提高效率。
-
非直接緩衝區:緩衝區建立在 JVM 的記憶體中。
-
堆緩衝區(backing 模式)
ByteBuf
資料儲存在 JVM 的堆空間(非直接緩衝區)中。這種模式也被稱為“支撐陣列(backing array)”,能夠在沒有使用池化的的情況下提供快速的分配和釋放。 -
直接緩衝區(direct 模式) 通過本地呼叫分配記憶體,直接分配的是實體記憶體。
-
複合緩衝區 為多個
ByteBuf
提供一個聚合檢視。CompositeByteBuf
:能夠提供將多個緩衝區(每個ByteBuf
可以是 backing 模式或者 direct 模式)表示為單個合併緩衝區的虛擬表示。
位元組級操作
- 隨機訪問索引
ByteBuf
的索引是從 0 開始到 capacity() - 1。 獲取每個位元組的方法則是:byte getByte(int i);
這樣倒不會改變readerIndex
和writerIndex
的值。 當然可以通過:ByteBuf readerIndex(int index)
和ByteBuf writerIndex(int index)
來手動移動讀取/寫入索引值。 - 順序訪問索引 有三個區域:
- 可丟棄位元組
通過呼叫
discardReadBytes()
方法,可以丟棄已經被讀過的位元組以回收空間。是的,呼叫discardReadBytes()
可以確保可寫分段的最大化,但是有可能導致記憶體複製(可讀位元組會移動到緩衝區的開始位置)。同樣的可丟棄位元組空間也只是相應的更改了writerIndex
的值,並不會保證空間中值的擦寫。 - 可讀位元組
ByteBuf
的可讀位元組分段(CONTENT)儲存了實際資料。任何名稱以 read 或者 skip 開頭的操作都將檢索或者跳過位於當前readerIndex
的資料,並且將它增加已讀位元組數。 - 可寫位元組 可寫位元組分段是指一個擁有未定義內容的、寫入就緒的記憶體區域。
- 索引管理
在 JDK 中
InputStream
定義了mark(int readlimit)
和reset()
方法,來標記流中指定位置,以及重置。在ByteBuf
中也有相應的方法來標記和重置ByteBuf
中的readerIndex
和writerIndex
:markReaderIndex()
markWriterIndex()
resetReaderIndex()
resetWriterIndex()
還有clear()
方法只是重置索引,且不會複製記憶體。
- 查詢操作
最簡單的,可以使用
indexOf()
方法。 或者使用ByteProcessor
類。 - 派生緩衝區
提供了專門的方法來呈現器內容的檢視:
duplicate()
slice()
slice(int , int)
Unpooled.unmodifiableBuffer(...)
order(ByteOrder)
readSlice(int)
以上每個方法都會返回一個新的ByteBuf
例項,具有自己的讀索引、寫索引和標記索引。但是內部儲存是共享的。所以如果修改了它的內容,也同時修改了其對應的源例項。 如果要深拷貝獲得真實副本,使用copy()
或者copy(int ,int)
函式。
- 讀/寫操作
兩種類別的讀/寫操作:
get()
和set()
操作,從給定的索引值開始,且保持索引值不變。read()
和write()
操作,從給定的索引開始,且會根據以及訪問過的位元組數堆索引進行調整。get()
方法:set()
方法:read()
方法:write()
方法:
- 更多的操作
ByteBuf分配
- 按需分配:
ByteBufAllocator
介面ByteBufAllocator
的方法: 可以通過Channel
或者ChannelHandlerContext
獲取到一個ByteBufAllocator
的引用。 Netty提供了兩種ByteBufAllocator
的實現:
PooledByteBufAllocator
:池化了ByteBuf
的例項以提高效能並且最大限度的減少記憶體碎片。UnpooledByteBufAllocator
:不池化ByteBuf
例項,但是每次被呼叫時會返回一個新的例項。
Unpooled
緩衝區 無法獲取到ByteBufAllocator
的引用時,有一個簡單的Unpooled
工具類,提供靜態的輔助方法來建立未池化的ByteBuf
例項。ByteBufUtil
類 這個類提供了用於操作ByteBuf
的靜態輔助方法。