之前在看rocketmq源碼時,發現底層用了Netty,順便學習了一下,網上不少博客講的有錯誤之處,而且大部分一模一樣,估計大部分都是復制別人的。為了不被誤導,我專門買了本《Netty權威指南》,仔細閱讀了一遍,而且微信請教了鋒哥(李林鋒),遂整理出這篇分享。
本人一直秉承原則:寧願不寫、少寫,也盡量不寫錯的知識!以免誤人子弟!
希望轉載的同學,標出原文鏈接。謝謝!同時非常歡迎指出錯誤,本人及時修正!
版本說明
Netty3(3.x)版本是比較舊的版本。
Netty4(4.x)版本是當前官方推薦的,目前一直在維護中。跟3.x版本相比變化比較大,特別是API。
Netty5(5.x)是被舍棄的版本,官方不推薦使用!
Netty5舍棄的官方解釋:
1. netty5中使用了 ForkJoinPool,增加了代碼的復雜度,但是對性能的改善卻不明顯
2. 多個分支的代碼同步工作量很大
3. 作者覺得當下還不到發布一個新版本的時候
4. 在發布版本之前,還有更多問題需要調查一下,比如是否應該廢棄 exceptionCaught,是否暴露EventExecutorChooser等等。
參考:https://github.com/netty/netty/issues/4466
1. Netty基本概念
Netty是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持,作為一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。
作為當前最流行的NIO框架,Netty在互聯網領域、大數據分布式計算領域、游戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。如:Dubbo、 RocketMQ、Hadoop的Avro、Spark等。
Netty需要學習的內容: 編解碼器、TCP粘包/拆包及Netty如何解決、ByteBuf、Channel和Unsafe、ChannelPipeline和ChannelHandler、EventLoop和EventLoopGroup、Future等。
編解碼器:
Java序列化的目的主要有兩個:
- 網絡傳輸
- 對象持久化
Java序列化僅僅是Java編解碼技術的一種,由於他的種種缺陷,衍生出多種編解碼技術和框架。
Java序列化的缺點:
- 無法跨語言
- 序列化后的碼流太大
- 序列化性能太低
業界主流的編解碼框架:
- Google Protobuf:支持Java、C++、Python三種語言,高效的編碼性能,結構化數據存儲格式(XML,JSON等)
- Facebook Thrift:適用於靜態的數據交換,需要先確定好它的數據結構。結構變化后需要重新編譯IDL文件,這也是Thrift的弱項。
- JBoss Marshalling:是一個Java對象的序列化API,修正了JDK自帶的序列化包的很多問題,但又保持跟java.io.Serializable接口的兼容。
- Hessian:一個輕量級的remoting onhttp工具,使用簡單的方法提供了RMI的功能,采用的是二進制RPC協議。
TCP粘包/拆包:
TCP是個“流”協議,所謂流,就是沒有界限的一串數據。TCP底層並不了解上層業務數據的具體含義,它會根據TCP緩存區的實際情況進行包的划分,所以在業務上的一個完整的包,可能被TCP拆分成多個包進行發送,也可能把多個小包封裝成一個大的數據包發送,這就是TCP粘包和拆包問題。
有如下幾種情況:
正常情況
粘包
粘包和拆包同時發生
2. Netty線程模型
在JAVA NIO方面Selector給Reactor模式提供了基礎,Netty結合Selector和Reactor模式設計了高效的線程模型。
關於Java NIO 構造Reator模式,Doug Lea在《Scalable IO in Java》中給了很好的闡述,這里截取PPT對Reator模式的實現進行說明。
(1)Reactor單線程模型
這是最簡單的Reactor單線程模型,由於Reactor模式使用的是異步非阻塞IO,所有的IO操作都不會被阻塞,理論上一個線程可以獨立處理所有的IO操作。這時Reactor線程是個多面手,負責多路分離套接字,Accept新連接,並分發請求到處理鏈中。
對於一些小容量應用場景,可以使用到單線程模型。但對於高負載,大並發的應用卻不合適,主要原因如下:
1. 當一個NIO線程同時處理成百上千的鏈路,性能上無法支撐,即使NIO線程的CPU負荷達到100%,也無法完全處理消息。
2. 當NIO線程負載過重后,處理速度會變慢,會導致大量客戶端連接超時,超時之后往往會重發,更加重了NIO線程的負載。
3. 可靠性低,一個線程意外死循環,會導致整個通信系統不可用。
為了解決這些問題,出現了Reactor多線程模型。
(2)Reactor多線程模型
相比上一種模式,該模型在處理鏈部分采用了多線程(線程池)。
在絕大多數場景下,該模型都能滿足性能需求。但是,在一些特殊的應用場景下,如服務器會對客戶端的握手消息進行安全認證。這類場景下,單獨的一個Acceptor線程可能會存在性能不足的問題。為了解決這些問題,產生了第三種Reactor線程模型。
(3)Reactor主從模型
該模型相比第二種模型,是將Reactor分成兩部分,mainReactor負責監聽server socket,accept新連接;並將建立的socket分派給subReactor。subReactor負責多路分離已連接的socket,讀寫網絡數據,對業務處理功能,其扔給worker線程池完成。通常,subReactor個數上可與CPU個數等同。
利用主從NIO線程模型,可以解決一個服務端監聽線程無法有效處理所有客戶端連接的性能不足問題,因此,在Netty的官方Demo中,推薦使用該線程模型。
(4)Netty模型
Netty的線程模型並不是一成不變,它實際取決於用戶的啟動參數配置。通過設置不同的啟動參數,Netty可以同時支持Reactor單線程模型、多線程模型和主從模型。
前面介紹完 Netty 相關一些理論,下面從功能特性、模塊組件來介紹 Netty 的架構設計。
3. Netty功能特性
下圖是Netty的功能特性:
4. Netty模塊組件
Netty主要有下面一些組件:
- Selector
- NioEventLoop
- NioEventLoopGroup
- ChannelHandler
- ChannelHandlerContext
- ChannelPipeline
Selector
Netty 基於 Selector 對象實現 I/O 多路復用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件。
NioEventLoop
其中維護了一個線程和任務隊列,支持異步提交執行任務,線程啟動時會調用 NioEventLoop 的 run 方法,執行 I/O 任務和非 I/O 任務。
NioEventLoopGroup
主要管理 eventLoop 的生命周期,可以理解為一個線程池,內部維護了一組線程,每個線程(NioEventLoop)負責處理多個 Channel 上的事件,而一個 Channel 只對應於一個線程。
ChannelHandler
是一個接口,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。
ChannelHandlerContext
保存 Channel 相關的所有上下文信息,同時關聯一個 ChannelHandler 對象。
ChannelPipeline 是保存 ChannelHandler 的 List,用於處理或攔截 Channel 的入站事件和出站操作。實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
ChannelPipeline對事件流的攔截和處理流程:
Netty中的事件分為Inbond事件和Outbound事件。
Inbound事件通常由I/O線程觸發,如TCP鏈路建立事件、鏈路關閉事件、讀事件、異常通知事件等。
Outbound事件通常是用戶主動發起的網絡I/O操作,如用戶發起的連接操作、綁定操作、消息發送等。
在 Netty中,Channel 、ChannelHandler、ChannelHandlerContext、 ChannelPipeline的關系如下圖:
一個 Channel 包含了一個 ChannelPipeline,而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表,並且每個 ChannelHandlerContext 中又關聯着一個 ChannelHandler。
入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表 head 往后傳遞到最后一個入站的 handler,出站事件會從鏈表 tail 往前傳遞到最前一個出站的 handler,兩種類型的 handler 互不干擾。
以上是Netty的主要原理介紹,Netty源碼分析的話,后續有時間會繼續分享出來。