開發(fā) | Netty快速入門,一看就懂!
很早以前就寫過關(guān)于 Netty 的使用,最近發(fā)現(xiàn)還有網(wǎng)友在看之前寫的那篇 Netty 文章,個人感覺那時候?qū)懙暮艽植?,怕影響同行的閱讀質(zhì)量,所以決定重新寫一些關(guān)于 Netty 的文章,補充以前的不足。
圖片來自 Pexels
Netty 能做啥
簡單說就是用來處理網(wǎng)絡(luò)編程,寫一款能進(jìn)行網(wǎng)絡(luò)通信的服務(wù)端和客戶端程序。如果沒有 Netty,在 Java 的世界中如何處理網(wǎng)絡(luò)編程呢?
Java 自帶的工具有:java.net 包,用于處理網(wǎng)絡(luò)通信,后面 Java 提供了 NIO 工具包用于提供非阻塞的通信。
與 Netty 同級別的第三方工具包:Mina,在設(shè)計上與 Netty 有些許不同,但是核心都是提供網(wǎng)絡(luò)通信的能力。
傳統(tǒng)網(wǎng)絡(luò)通信模型
說 Netty 之前還是先講一下傳統(tǒng)的網(wǎng)絡(luò)編程是什么樣子。傳統(tǒng)的 Socket 編程開發(fā)步驟很簡單,只需要使用 Socket 類創(chuàng)建客戶端和服務(wù)端即可。
但是為啥現(xiàn)在沒有人用它了呢?主要原因是它基于同步阻塞 IO 的線程模型去做的,在當(dāng)今時代完全不能滿足生產(chǎn)需要,自然被 Out。
同步阻塞 IO 模型
同步阻塞線程模型的問題在于一個請求必須綁定一個線程去處理,并且所有的請求都是同步操作,意味著該請求未處理完之前這個連接不會被釋放,如果并發(fā)高的情況必然會導(dǎo)致系統(tǒng)壓力過大。
Netty 的新線程模型
基于此,Java 新增了非阻塞的 IO 操作包 NIO, NIO 的線程模型采用了 Reactor 模式,即異步非阻塞的方式,解決了之前同步阻塞帶來的問題。
NIO 的全稱是 NoneBlocking IO,非阻塞 IO,區(qū)別與 BIO,BIO 的全稱是 Blocking IO,阻塞 IO。
那這個阻塞是什么意思呢?
- Accept 是阻塞的,只有新連接來了,Accept 才會返回,主線程才能繼。
- Read 是阻塞的,只有請求消息來了,Read 才能返回,子線程才能繼續(xù)處理。
- Write 是阻塞的,只有客戶端把消息收了,Write 才能返回,子線程才能繼續(xù)讀取下一個請求。
服務(wù)器在處理響應(yīng)的設(shè)計模式方面目前主要分為兩種:
- 線程驅(qū)動
- 事件驅(qū)動
同步阻塞就是線程驅(qū)動的模式,最明顯的例子就是 Tomcat;對于事件驅(qū)動來說,沒有必要為每一個連接都創(chuàng)建一個線程去維護(hù)。
參考觀察者模式,可以設(shè)置一個事件池,用一個單線程去循環(huán)監(jiān)聽當(dāng)前池中是否有完成的事件,如果有則取出該事件。
簡單說一下 Reactor 模式是如何解決線程等待問題的:在等待 IO 的時候,線程可以先退出不用一直等待 IO 操作。
但是如果不等待那么 IO 處理完成之后返回給誰呢?Reactor 模型采用了事件驅(qū)動機制,要求線程在退出前向 event loop 注冊回調(diào)函數(shù),這樣 IO 完成之后 event loop 就可以調(diào)用回調(diào)函數(shù)完成數(shù)據(jù)返回。
在 Reactor 中有 4 個角色,所有的數(shù)據(jù)流入的處理統(tǒng)一稱為 Channel,就像是一個水管,Reactor 模型將每一種事件拆分為一個 event,相同類型的 event 歸為一類,這一類的統(tǒng)一處理邏輯被稱為一個 handler。
那么怎么去讓一個或者多個線程去監(jiān)聽所有的 Channel 呢?所以就有 Selector。
Selector 就像是一個管理者,你可以將多個 Channel 注冊到 一個 Selector 線程上,它會使用一個阻塞方法去捕獲當(dāng)前 Channel 上是否有事件發(fā)生,如果有則取出事件交給對應(yīng)的 Handler 去處理。
Netty 是建立在 NIO 之上的,并且 Netty 在 NIO 上面又提供了更多高層次 API 的封裝。
為什么不用 JDK 提供的 NIO
JDK 已經(jīng)給我們提供了 NIO 的包,也是使用了 Reactor 模型來實現(xiàn)的異步非阻塞模式,那我們?yōu)樯对谌粘i_發(fā)中沒有聽到誰直接使用 NIO 來開發(fā)網(wǎng)絡(luò)編程呢?
實際上大家不使用的原因是因為它太難控制。Java NIO 類庫中主要提供的功能包括:
- 緩沖區(qū) Buffer
- 通道 Channel
- 多路復(fù)用器 Selector
緩沖區(qū) Buffer 其實就是一個對象,即所有流入或者流出的數(shù)據(jù)都在 Buffer 中存在。
新 IO 與老的面向流 IO 的區(qū)別在于老 IO 直接面向字節(jié)流進(jìn)行處理,新 IO 是面向緩沖區(qū)進(jìn)行處理,讀寫數(shù)據(jù)都是先讀寫到緩沖區(qū)中。
緩沖區(qū)實質(zhì)上是一個字節(jié)數(shù)組,NIO 提供了對緩沖區(qū)數(shù)據(jù)讀寫位置維護(hù)的操作能力。
Buffer 和 Channel 的關(guān)系
Channel 通道,所有 Buffer 內(nèi)的數(shù)據(jù)都會往 Channel 上流,數(shù)據(jù)通過 Channel 流向處理邏輯,通過 Channel 將處理過的數(shù)據(jù)返回給客戶端。
所以 Channel 是全雙工的,可以支持讀寫,這是它與 Stream 的區(qū)別。如果你使用 Stream,讀數(shù)據(jù)只能使用 InputStream 進(jìn)行操作,寫數(shù)據(jù)只能使用 OutputStream 進(jìn)行操作。
用現(xiàn)實世界中的事物比喻的話,傳統(tǒng) IO 猶如水管,水流只能沿著管道往下流;NIO 猶如一條雙向公路,兩個方向都可以行車。
另外也正是因為 Buffer 的引入我們才能隨意的控制每次傳輸讀多少數(shù)據(jù),如果上次讀取失敗,那么應(yīng)該從多少偏移量重新讀取,這是傳統(tǒng) I/O 流無法比擬的。
Selector 選擇器,它是 NIO 的核心,一個 Selector 就是一個線程,NIO 允許一個 Selector 管理多個 Channel,即將 Channel 注冊到 Selector 上。
Selector 會去監(jiān)聽注冊的 Channel 上是否有事件準(zhǔn)備就緒,如果有就取出處理。
Selector 的輪詢監(jiān)聽
關(guān)于 NIO 的代碼我就不寫了,是很龐大的一堆,大家百度一下就能看到??傊谶@個思想來進(jìn)行網(wǎng)絡(luò)編程肯定是面對當(dāng)今流量洪峰的最佳方式。而正好 Netty 底層基于 NIO 去做的封裝,已經(jīng)給你屏蔽了這一大坨操作。
網(wǎng)絡(luò)編程還有一個問題就是跨平臺性,NIO 底層是依賴系統(tǒng)的 IO API,不同的系統(tǒng)可能對 IO API 的實現(xiàn)也是不一樣的,這里如何你使用 NIO 那么就需要考慮系統(tǒng)兼容性問題了。
另外還有一個問題就是 NIO 有個很著名的 bug,JDK 的 NIO 底層由 epoll 實現(xiàn),若 Selector 的輪詢結(jié)果為空,也沒有 wakeup 或新消息處理,則發(fā)生空輪詢,CPU 使用率 100%。
這個 Bug 官方聲明已經(jīng)修復(fù),事實上沒有被 Fix, 只是出現(xiàn)的概率會降低一些。
Netty 也對該 Bug 進(jìn)行了處理:對 Selector 的 Select 操作周期進(jìn)行統(tǒng)計,每完成一次空的 Select 操作進(jìn)行一次計數(shù),若在某個周期內(nèi)連續(xù)發(fā)生 N 次空輪詢,則觸發(fā)了 Epoll 死循環(huán) Bug。
那么這個時候就重建 Selector,判斷是否是其他線程發(fā)起的重建請求,若不是則將原 SocketChannel 從舊的 Selector 上去除注冊,重新注冊到新的 Selector 上,并將原來的 Selector 關(guān)閉。
網(wǎng)絡(luò)編程應(yīng)該注意什么
既然說要學(xué)習(xí) Netty, 它本身是基于 NIO 的封裝用于網(wǎng)絡(luò)通信,那么在編寫一段用于網(wǎng)絡(luò)通信的代碼我們應(yīng)該注意一些什么呢?弄清楚這些問題,我們大概就知道 Netty 都做了什么。
談到網(wǎng)絡(luò)就不能避免說到 OSI 7 層模型和 TCP / IP 4 層模型:
OSI 模型和對應(yīng)的網(wǎng)絡(luò)協(xié)議
Java 網(wǎng)絡(luò)編程主要使用的是 Socket 套接字編程,基于 4 層協(xié)議的網(wǎng)絡(luò)編程,即基于 TCP/ UDP 協(xié)議的封裝。
編寫一個 Socket 通信都有哪些步驟呢?
- 創(chuàng)建一個 ServerSocket,監(jiān)聽并綁定一個端口。
- 一系列客戶端來請求這個端口。
- 服務(wù)器使用 Accept,獲得一個來自客戶端的 Socket 連接對象。
- 啟動一個新線程處理連接:①讀 Socket,得到字節(jié)流;②解碼協(xié)議,得到 HTTP 請求對象;③處理 HTTP 請求,得到一個結(jié)果,封裝成一個 HTTPResponse 對象;④編碼協(xié)議,將結(jié)果序列化字節(jié)流;④寫 Socket,將字節(jié)流發(fā)給客戶端。
- 繼續(xù)循環(huán)步驟 3。
根據(jù)以上的數(shù)據(jù)傳輸流程,我們可以提出一些問題:
- 如何約定字節(jié)流長度格式,以保證每次讀到的字節(jié)流都是最新的而不會和上次重復(fù)。
- 傳輸字節(jié)流的編解碼問題。
- 一個服務(wù)端肯定會有多個客戶端鏈接,如何管理眾多的客戶端鏈接,比如如何維護(hù)斷線重連,連接超時以及關(guān)閉機制。
上面這些問題我們在接下來的 Netty 學(xué)習(xí)中都會找到答案。
Netty 核心組件
在還未入門 Netty 之前我們先了解一下 Netty 里面都有哪些類,做到有的放矢,后面學(xué)習(xí)帶著這些關(guān)鍵信息不回亂。
①Bootstrap、ServerBootstrap
一個 Netty 應(yīng)用通常由一個 Bootstrap 開始,主要作用是配置整個 Netty 程序,串聯(lián)各個組件,Netty 中 Bootstrap 類是客戶端程序的啟動引導(dǎo)類,ServerBootstrap 是服務(wù)端啟動引導(dǎo)類。
②Future、ChannelFuture
在 Netty 中所有的 IO 操作都是異步的,不會立刻知道某個事件是否完成處理。
但是可以過一會等它執(zhí)行完成或者直接注冊一個監(jiān)聽,具體的實現(xiàn)就是通過 Future 和 ChannelFutures,用來注冊一個監(jiān)聽,當(dāng)操作執(zhí)行成功或失敗時監(jiān)聽會自動觸發(fā)注冊的監(jiān)聽事件。
③Channel
Netty 網(wǎng)絡(luò)通信的組件,能夠用于執(zhí)行網(wǎng)絡(luò) I/O 操作。
Channel 為用戶提供:
- 當(dāng)前網(wǎng)絡(luò)連接的通道的狀態(tài)(例如是否打開,是否已連接)。
- 網(wǎng)絡(luò)連接的配置參數(shù) (例如接收緩沖區(qū)大小)。
- 提供異步的網(wǎng)絡(luò) I/O 操作(如建立連接,讀寫,綁定端口),異步調(diào)用意味著任何 I/O 調(diào)用都將立即返回,并且不保證在調(diào)用結(jié)束時所請求的 I/O 操作已完成。
- 調(diào)用立即返回一個 ChannelFuture 實例,通過注冊監(jiān)聽器到 ChannelFuture 上,可以 I/O 操作成功、失敗或取消時回調(diào)通知調(diào)用方。
- 支持關(guān)聯(lián) I/O 操作與對應(yīng)的處理程序。
不同協(xié)議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應(yīng)。
下面是一些常用的 Channel 類型:
- NioSocketChannel,異步的客戶端 TCP Socket 連接。
- NioServerSocketChannel,異步的服務(wù)器端 TCP Socket 連接。
- NioDatagramChannel,異步的 UDP 連接。
- NioSctpChannel,異步的客戶端 Sctp 連接。
- NioSctpServerChannel,異步的 Sctp 服務(wù)器端連接,這些通道涵蓋了 UDP 和 TCP 網(wǎng)絡(luò) IO 以及文件 IO。
④Selector
Netty 基于 Selector 對象實現(xiàn) I/O 多路復(fù)用,通過 Selector 一個線程可以監(jiān)聽多個連接的 Channel 事件。
當(dāng)向一個 Selector 中注冊 Channel 后,Selector 內(nèi)部的機制就可以自動不斷地查詢(Select)這些注冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網(wǎng)絡(luò)連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel 。
⑤NioEventLoop
NioEventLoop 中維護(hù)了一個線程和任務(wù)隊列,支持異步提交執(zhí)行任務(wù),線程啟動時會調(diào)用 NioEventLoop 的 run 方法,執(zhí)行 I/O 任務(wù)和非 I/O 任務(wù):
- I/O 任務(wù),即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法觸發(fā)。
- 非 IO 任務(wù),添加到 taskQueue 中的任務(wù),如 register0、bind0 等任務(wù),由 runAllTasks 方法觸發(fā)。
兩種任務(wù)的執(zhí)行時間比由變量 ioRatio 控制,默認(rèn)為 50,則表示允許非 IO 任務(wù)執(zhí)行的時間與 IO 任務(wù)的執(zhí)行時間相等。
⑥NioEventLoopGroup
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解為一個線程池,內(nèi)部維護(hù)了一組線程,每個線程(NioEventLoop)負(fù)責(zé)處理多個 Channel 上的事件,而一個 Channel 只對應(yīng)于一個線程。
⑦ChannelHandler
ChannelHandler 是一個接口,處理 I/O 事件或攔截 I/O 操作,并將其轉(zhuǎn)發(fā)到其 ChannelPipeline(業(yè)務(wù)處理鏈)中的下一個處理程序。
ChannelHandler 本身并沒有提供很多方法,因為這個接口有許多的方法需要實現(xiàn),方便使用期間,可以繼承它的子類:
- ChannelInboundHandler 用于處理入站 I/O 事件。
- ChannelOutboundHandler 用于處理出站 I/O 操作。
或者使用以下適配器類:
- ChannelInboundHandlerAdapter 用于處理入站 I/O 事件。
- ChannelOutboundHandlerAdapter 用于處理出站 I/O 操作。
- ChannelDuplexHandler 用于處理入站和出站事件。
⑧ChannelHandlerContext
保存 Channel 相關(guān)的所有上下文信息,同時關(guān)聯(lián)一個 ChannelHandler 對象。
⑨ChannelPipline
保存 ChannelHandler 的 List,用于處理或攔截 Channel 的入站事件和出站操作。
它實現(xiàn)了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應(yīng)。
關(guān)于 Netty 的簡介就先說這么多,后面會帶著 Socket 通信應(yīng)該解決的問題和上面提到的 Netty 關(guān)鍵組件講解 Netty 是如何實現(xiàn)高性能網(wǎng)絡(luò)通信的。
作者:rickiyang
編輯:陶家龍
出處:https://www.cnblogs.com/rickiyang/