一、傳統的BIO編程
網絡編程的基本模型是 Client/Server 模型,也就是兩個進程之間進行相互通信,其中服務端提供位置信息(綁定的 IP 地址和監聽端口),客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連
接,如果連接建立成功,雙方就可以通過網絡套接字(Socket)進行通信。
在基於傳統同步阻塞模型開發中,ServerSocket 負責綁定 IP 地址,啟動監聽端口;Socket 負責發起連接操作。連接成功之后,雙方通過輸入和輸出流進行同步阻塞式通信。
首先,我們通過下圖所示的通信模型圖來熟悉下 BIO 的服務端通信模型:采用 BIO 通信模型的服務端,通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接,它接收到客戶端連接請求之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的一請求一應答通信模型
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端並發訪問量增加后,服務端的線程個數和客戶端並發訪問數呈 1:1 的正比關系,由於線程是 Java 虛擬機非常寶貴的系統資源,當線程數膨脹之后,系統的性能將急劇下降,隨着並發訪問量的繼續增大,系統會發生線程堆棧溢出、創建新線程失敗等問題,並最終導致進程宕機或者僵死,不能對外提供服務。
二、偽異步的I/O編程
為了解決同步阻塞 I/O 面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進行了優化,后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數 M:線程池最大線程數 N 的比例關系,其中 M 可以遠遠大於 N,通過線程池可以靈活的調配線程資源,設置線程的最大值,防止由於海量並發接入導致線程耗盡。
當有新的客戶端接入的時候,將客戶端的 Socket 封裝成一個 Task(該任務實現 java.lang.Runnable 接口)投遞到后端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程對消息隊列中的任務進行處理。由於線程池可以設置消息隊列的大小和最大線程數,因此,它的資源占用是可控的,無論多少個客戶端並發訪問,都不會導致資源的耗盡和宕機。
偽異步 I/O 實際上僅僅只是對之前 I/O 線程模型的一個簡單優化,它無法從根本上解決同步 I/O 導致的通信線程阻塞問題。下面我們就簡單分析下如果通信對方返回應答時間過長,會引起的級聯故障。
-
服務端處理緩慢,返回應答消息耗費60s,平時只需要10ms。
-
采用偽異步I/O的線程正在讀取故障服務節點的響應,由於讀取輸入流是阻塞的,因此,它將會被同步阻塞60s。
-
假如所有的可用線程都被故障服務器阻塞,那后續所有的I/O消息都將在隊列中排隊。
-
由於線程池采用阻塞隊列實現,當隊列積滿之后,后續入隊列的操作將被阻塞。
-
由於前端只有一個Accptor線程接收客戶端接入,它被阻塞在線程池的同步阻塞隊列之后,新的客戶端請求消息將被拒絕,客戶端會發生大量的連接超時。
-
由於幾乎所有的連接都超時,調用者會認為系統已經崩潰,無法接收新的請求消息。
三、NIO編程
NIO即New I/O,即較java傳統支持的I/O是新增的非阻塞I/O。也被稱為Non-block I/O。
與 Socket 類和 ServerSocket 類相對應,NIO 也提供了 SocketChannel 和ServerSocketChannel 兩種不同的套接字通道實現。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是性能和可靠性都不好,非阻塞模式則正好相反。開發人員一般可以根據自己的需要來選擇合適的模式,一般來說,低負載、低並發的應用程序可以選擇同步阻塞 I/O 以降低編程復雜度,但是對於高負載、高並發的網絡應用,需要使用 NIO 的非阻塞模式進行開發。
四、AIO編程
NIO2.0 引入了新的異步通道的概念,並提供了異步文件通道和異步套接字通道的實現。異步通道提供兩種方式獲取獲取操作結果:
-
通過java.util.concurrent.Future類來表示異步操作的結果;
-
在執行異步操作的時候傳入一個java.nio.channels;
-
CompletionHandler接口的實現類作為操作完成的回調。
NIO2.0 的異步套接字通道是真正的異步非阻塞 I/O,它對應 UNIX 網絡編程中的事件驅動 I/O(AIO),它不需要通過多路復用器(Selector)對注冊的通道進行輪詢操作即可實現異步讀寫,從而簡化了 NIO 的編程模型。
五、I/O模型對比
六、主流NIO框架
目前,業界主流的 NIO 框架主要有兩款:Mina 和 Netty,兩者都使用 Apache LICENSE-2.0 進行開源。不同之處是Mina 是 Apache 基金會的官方 NIO 框架,Netty 之前是 Jboss 的 NIO 框架,后來脫離 Jboss 獨立申請了 netty.io 域名,與 Jboss 脫離關系,並對版本進行了重構,導致 API 無法向上兼容。
Mina 和 Netty 還 有 一 段 歷 史 淵 源,Mina 最 初 版 本 的 架 構 師 是 Trustin Lee,后來,由於種種原因,Trustin Lee 離開了 Mina 社區加入到了 Netty 團隊,重新設計並開發了 Netty。很多讀者會發現 Netty 中透着 Mina 的影子,兩個框架的架構理念也有很多相似之處,甚至一些代碼都非常相似,原因就在這里。
目前,Mina 和 Netty 的應用已經非常廣泛,很多開源框架都使用兩者做底層的 NIO 框架,例如 Hadoop 的通信組件 Avro 使用 Netty 做底層的通信框架,Openfire 則使用 Mina 做底層通信框架,相比於 Mina,Netty 社區目前更活躍,版本應用范圍也更廣。
七、為什么學習Netty
7.1 不選擇Java原生NIO的原因
-
NIO的類庫和API繁雜,使用麻煩,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
-
需要具備其他的額外技能做鋪墊,例如熟悉Java多線程編程。這是因為NIO編程涉及到Reactor模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的NIO程序。
-
可靠性能力補齊,工作量和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等問題,NIO編程的特點是功能開發相對容易,但是可靠性能力補齊的工作量和難度都非常大
7.2 選擇Netty的理由
-
API使用簡單,開發門檻低;
-
功能強大,預置了多種編解碼功能,支持多種主流協議;
-
定制能力強,可以通過ChannelHandler對通信框架進行靈活地擴展;
-
性能高,通過與其他業界主流的NIO框架對比,Netty的綜合性能最優;
-
成熟、穩定,Netty修復了已經發現的所有JDK NIO BUG,業務開發人員不需要再為NIO的BUG而煩惱;
-
社區活躍,版本迭代周期短,發現的BUG可以被及時修復,同時,更多的新功能會加入;
-
經歷了大規模的商業應用考驗,質量得到驗證。在互聯網、大數據、網絡游戲、企業應用、電信軟件等眾多行業得到成功商用,證明了它已經完全能夠滿足不同行業的商業應用了。
八、Netty開發環境搭建
8.1 Netty 的官網
8.2 Maven倉庫地址
https://mvnrepository.com/artifact/io.netty/netty-all
8.3 Maven依賴
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.66.Final</version>
</dependency>
8.4 Gradle依賴
// https://mvnrepository.com/artifact/io.netty/netty-all
implementation group: 'io.netty', name: 'netty-all', version: '4.1.66.Final'