很早以前其實(shí)就寫過(guò)關(guān)于 Netty 的使用糯崎,最近發(fā)現(xiàn)在CSDN上一直有人在看很早寫的 Netty 文章河泳,個(gè)人感覺(jué)那時(shí)候?qū)懙暮艽植诓鸹樱掠绊懲械拈喿x質(zhì)量纸兔,但是我也不知道為啥有這么多小伙伴關(guān)注Netty汉矿,所以決定重新寫一些關(guān)于Netty的文章洲拇,補(bǔ)充以前的不足吧赋续。
Netty能做啥
簡(jiǎn)單說(shuō)就是用來(lái)處理網(wǎng)絡(luò)編程,寫一款能進(jìn)行網(wǎng)絡(luò)通信的服務(wù)端和客戶端程序蛾绎。
如果沒(méi)有 Netty秘通,在 Java 的世界中如何處理網(wǎng)絡(luò)編程呢敛熬?
Java自帶的工具有:java.net
包应民,用于處理網(wǎng)絡(luò)通信,后面Java提供了 NIO 工具包用于提供非阻塞的通信繁仁。
與Netty同級(jí)別的第三方工具包:Mina黄虱,在設(shè)計(jì)上與Netty 有些許不同捻浦,但是核心都是提供網(wǎng)絡(luò)通信的能力朱灿。
傳統(tǒng)網(wǎng)絡(luò)通信模型
說(shuō)Netty之前還是先講一下傳統(tǒng)的網(wǎng)絡(luò)編程是什么樣子盗扒。傳統(tǒng)的Socket編程開發(fā)步驟很簡(jiǎn)單侣灶,只需要使用Socket類創(chuàng)建客戶端和服務(wù)端即可褥影。但是為啥現(xiàn)在沒(méi)有人用它了呢伪阶?主要原因是它基于同步阻塞 IO 的線程模型去做的栅贴,在當(dāng)今時(shí)代完全不能滿足生產(chǎn)需要檐薯,自然被out墓猎。
同步阻塞線程模型的問(wèn)題在于一個(gè)請(qǐng)求必須綁定一個(gè)線程去處理毙沾,并且所有的請(qǐng)求都是同步操作宠页,意味著該請(qǐng)求未處理完之前這個(gè)連接不會(huì)被釋放举户,如果并發(fā)高的情況必然會(huì)導(dǎo)致系統(tǒng)壓力過(guò)大。
Netty 的新線程模型
基于此俭嘁,Java新增了非阻塞的IO操作包 NIO拐云, NIO 的線程模型采用了Reactor 模式慨丐,即異步非阻塞的方式房揭,解決了之前同步阻塞帶來(lái)的問(wèn)題捅暴。
NIO 的全稱是 NoneBlocking IO蓬痒,非阻塞 IO梧奢,區(qū)別與 BIO,BIO 的全稱是 Blocking IO鸟顺,阻塞 IO。那這個(gè)阻塞是什么意思呢兆沙?
- Accept是阻塞的葛圃,只有新連接來(lái)了装悲,Accept才會(huì)返回尚氛,主線程才能繼洞渤;
- Read是阻塞的阅嘶,只有請(qǐng)求消息來(lái)了,Read才能返回载迄,子線程才能繼續(xù)處理讯柔;
- Write是阻塞的,只有客戶端把消息收了护昧,Write才能返回魂迄,子線程才能繼續(xù)讀取下一個(gè)請(qǐng)求。
服務(wù)器在處理響應(yīng)的設(shè)計(jì)模式方面目前主要分為兩種:線程驅(qū)動(dòng) 和 事件驅(qū)動(dòng)惋耙。同步阻塞就是線程驅(qū)動(dòng)的模式捣炬,最明顯的例子就是 Tomcat湿酸;對(duì)于事件驅(qū)動(dòng)來(lái)說(shuō),沒(méi)有必要為每一個(gè)連接都創(chuàng)建一個(gè)線程去維護(hù),參考觀察者模式,可以設(shè)置一個(gè)事件池襟铭,用一個(gè)單線程去循環(huán)監(jiān)聽(tīng)當(dāng)前池中是否有完成的事件哩都,如果有則取出該事件咐汞。
簡(jiǎn)單說(shuō)一下 Reactor 模式是如何解決線程等待問(wèn)題的:在等待IO的時(shí)候植阴,線程可以先退出不用一直等待IO操作。但是如果不等待那么IO處理完成之后返回給誰(shuí)呢?Reactor模型采用了事件驅(qū)動(dòng)機(jī)制,要求線程在退出前向event loop注冊(cè)回調(diào)函數(shù)剖淀,這樣IO完成之后 event loop 就可以調(diào)用回調(diào)函數(shù)完成數(shù)據(jù)返回捌刮。
在Reactor中有 4 個(gè)角色蛾派,所有的數(shù)據(jù)流入的處理統(tǒng)一稱為 Channel夜焦,就像是一個(gè)水管,Reactor 模型將每一種事件拆分為一個(gè) event,相同類型的 event 歸為一類,這一類的統(tǒng)一處理邏輯被稱為一個(gè) handler。那么怎么去讓一個(gè)或者多個(gè)線程去監(jiān)聽(tīng)所有的 Channel 呢查蓉? 所以就有 Selector鹃共,Selector 就像是一個(gè)管理者,你可以將多個(gè) Channel 注冊(cè)到 一個(gè)Selector 線程上,它會(huì)使用一個(gè)阻塞方法去捕獲當(dāng)前 Channel 上是否有事件發(fā)生,如果有則取出事件交給對(duì)應(yīng)的 handler 去處理毕泌。
Netty 是建立在 NIO 之上的辩诞,并且 Netty 在 NIO 上面又提供了更多高層次 API 的封裝。
為什么不用 JDK 提供的 NIO
JDK 已經(jīng)給我們提供了 NIO 的包伯顶,也是使用了 Reactor 模型來(lái)實(shí)現(xiàn)的異步非阻塞模式掐暮,那我們?yōu)樯对谌粘i_發(fā)中沒(méi)有聽(tīng)到誰(shuí)直接使用 NIO 來(lái)開發(fā)網(wǎng)絡(luò)編程呢瓢宦?實(shí)際上大家不使用的原因是因?yàn)樗y控制疲吸。Java NIO 類庫(kù)中主要提供的功能包括:
- 緩沖區(qū) Buffer
- 通道 Channel
- 多路復(fù)用器 Selector
緩沖區(qū) Buffer 其實(shí)就是一個(gè)對(duì)象舰绘,即所有流入或者流出的數(shù)據(jù)都在Buffer中存在孵运。
新 IO 與老的面向流 IO 的區(qū)別在于老 IO 直接面向字節(jié)流進(jìn)行處理,新 IO 是面向緩沖區(qū)進(jìn)行處理蹂空,讀寫數(shù)據(jù)都是先讀寫到緩沖區(qū)中糖声。緩沖區(qū)實(shí)質(zhì)上是一個(gè)字節(jié)數(shù)組并扇,NIO 提供了對(duì)緩沖區(qū)數(shù)據(jù)讀寫位置維護(hù)的操作能力鬼雀。
Channel 通道,所有 Buffer內(nèi)的數(shù)據(jù)都會(huì)往 Channel 上流,數(shù)據(jù)通過(guò) Channel 留向處理邏輯阿纤,通過(guò) Channel 將處理過(guò)的數(shù)據(jù)返回給客戶端。所以 Channel 是全雙工的,可以支持讀寫屈呕,這是它與 Stream 的區(qū)別岳守。如果你使用Stream俊卤,讀數(shù)據(jù)只能使用 InputStream 進(jìn)行操作,寫數(shù)據(jù)只能使用 OutputStream 進(jìn)行操作记盒。用現(xiàn)實(shí)世界中的事物比喻的話,傳統(tǒng) IO 猶如水管熙尉,水流只能沿著管道往下流爱态; NIO 猶如一條雙向公路缚态,兩個(gè)方向都可以行車。
另外也正是因?yàn)?Buffer 的引入我們才能隨意的控制每次傳輸讀多少數(shù)據(jù)祈匙,如果上次讀取失敗,那么應(yīng)該從多少偏移量重新讀取腰素,這是傳統(tǒng) I/O流無(wú)法比擬的姻政。
Selector 選擇器,它是 NIO 的核心品洛,一個(gè) Selector 就是一個(gè)線程士飒,NIO 允許一個(gè) Selector 管理多個(gè) Channel,即將 Channel 注冊(cè)到 Selector 上萌壳,Selector 會(huì)去監(jiān)聽(tīng)注冊(cè)的 Channel 上是否有事件準(zhǔn)備就緒,如果有就取出處理。
關(guān)于 NIO 的代碼我就不寫了,是很龐大的一堆矛缨,大家百度一下就能看到述召⌒杂總之基于這個(gè)思想來(lái)進(jìn)行網(wǎng)絡(luò)編程肯定是面對(duì)當(dāng)今流量洪峰的最佳方式忌愚。而正好 Netty 底層基于 NIO 去做的封裝腊徙,已經(jīng)給你屏蔽了這一大坨操作胶逢。
網(wǎng)絡(luò)編程還有一個(gè)問(wèn)題就是跨平臺(tái)性初坠,NIO 底層是依賴系統(tǒng)的 IO API吴菠,不同的系統(tǒng)可能對(duì) IO API 的實(shí)現(xiàn)也是不一樣的榨乎,這里如何你使用 NIO 那么就需要考慮系統(tǒng)兼容性問(wèn)題了袍患。
另外還有一個(gè)問(wèn)題就是 NIO 有個(gè)很著名的 bug竣付,JDK 的 NIO 底層由 epoll 實(shí)現(xiàn)诡延,若Selector的輪詢結(jié)果為空,也沒(méi)有wakeup或新消息處理古胆,則發(fā)生空輪詢肆良,CPU 使用率100%。這個(gè) bug 官方聲明已經(jīng)修復(fù)逸绎,事實(shí)上沒(méi)有被 fix惹恃, 只是出現(xiàn)的概率會(huì)降低一些。
Netty 也對(duì)該 bug 進(jìn)行了處理:對(duì) Selector 的 select 操作周期進(jìn)行統(tǒng)計(jì)棺牧,每完成一次空的 select 操作進(jìn)行一次計(jì)數(shù)巫糙,若在某個(gè)周期內(nèi)連續(xù)發(fā)生N次空輪詢,則觸發(fā)了 epoll 死循環(huán)bug颊乘。那么這個(gè)時(shí)候就重建 Selector参淹,判斷是否是其他線程發(fā)起的重建請(qǐng)求,若不是則將原 SocketChannel 從舊的 Selector上去除注冊(cè)乏悄,重新注冊(cè)到新的 Selector 上浙值,并將原來(lái)的 Selector 關(guān)閉。
網(wǎng)絡(luò)編程應(yīng)該注意什么
既然說(shuō)要學(xué)習(xí) Netty檩小, 它本身是基于 NIO 的封裝用于網(wǎng)絡(luò)通信开呐,那么在編寫一段用于網(wǎng)絡(luò)通信的代碼我們應(yīng)該注意一些什么呢?弄清楚這些問(wèn)題识啦,我們大概就知道 Netty 都做了什么负蚊。
談到網(wǎng)絡(luò)就不能避免說(shuō)到 OSI 7層模型 和 TCP / IP 4層模型。
Java 網(wǎng)絡(luò)編程主要使用的是 Socket 套接字編程颓哮,基于 4層 協(xié)議的網(wǎng)絡(luò)編程家妆,即基于 TCP/ UDP 協(xié)議的封裝。編寫一個(gè) Socket 通信都有哪些步驟呢冕茅?
創(chuàng)建一個(gè) ServerSocket伤极,監(jiān)聽(tīng)并綁定一個(gè)端口蛹找;
一系列客戶端來(lái)請(qǐng)求這個(gè)端口;
服務(wù)器使用 Accept哨坪,獲得一個(gè)來(lái)自客戶端的Socket連接對(duì)象庸疾;
-
啟動(dòng)一個(gè)新線程處理連接;
- 讀 Socket当编,得到字節(jié)流届慈,
- 解碼協(xié)議,得到Http請(qǐng)求對(duì)象忿偷,
- 處理 HTTP 請(qǐng)求金顿,得到一個(gè)結(jié)果,封裝成一個(gè) HttpResponse 對(duì)象鲤桥,
- 編碼協(xié)議揍拆,將結(jié)果序列化字節(jié)流,
- 寫 Socket茶凳,將字節(jié)流發(fā)給客戶端嫂拴,
繼續(xù)循環(huán)步驟3。
根據(jù)以上的數(shù)據(jù)傳輸流程贮喧,我們可以提出一些問(wèn)題:
- 如何約定字節(jié)流長(zhǎng)度格式筒狠,以保證每次讀到的字節(jié)流都是最新的而不會(huì)和上次重復(fù);
- 傳輸字節(jié)流的編解碼問(wèn)題塞淹;
- 一個(gè)服務(wù)端肯定會(huì)有多個(gè)客戶端鏈接窟蓝,如何管理眾多的客戶端鏈接,比如如何維護(hù)斷線重連饱普,連接超時(shí)以及關(guān)閉機(jī)制运挫;
上面這些問(wèn)題我們?cè)诮酉聛?lái)的 Netty 學(xué)習(xí)中都會(huì)找到答案。
Netty 核心組件
在還未入門 Netty 之前我們先了解一下 Netty 里面都有哪些類套耕,做到有的放矢谁帕,后面學(xué)習(xí)帶著這些關(guān)鍵信息不回亂。
Bootstrap冯袍、ServerBootstrap
一個(gè) Netty 應(yīng)用通常由一個(gè) Bootstrap 開始匈挖,主要作用是配置整個(gè) Netty 程序,串聯(lián)各個(gè)組件康愤,Netty 中 Bootstrap 類是客戶端程序的啟動(dòng)引導(dǎo)類儡循,ServerBootstrap 是服務(wù)端啟動(dòng)引導(dǎo)類。
Future征冷、ChannelFuture
在 Netty 中所有的 IO 操作都是異步的择膝,不會(huì)立刻知道某個(gè)事件是否完成處理。但是可以過(guò)一會(huì)等它執(zhí)行完成或者直接注冊(cè)一個(gè)監(jiān)聽(tīng)检激,具體的實(shí)現(xiàn)就是通過(guò) Future 和 ChannelFutures肴捉,用來(lái)注冊(cè)一個(gè)監(jiān)聽(tīng)腹侣,當(dāng)操作執(zhí)行成功或失敗時(shí)監(jiān)聽(tīng)會(huì)自動(dòng)觸發(fā)注冊(cè)的監(jiān)聽(tīng)事件。
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é)束時(shí)所請(qǐng)求的 I/O 操作已完成;
- 調(diào)用立即返回一個(gè) ChannelFuture 實(shí)例洛二,通過(guò)注冊(cè)監(jiān)聽(tīng)器到 ChannelFuture 上馋劈,可以 I/O 操作成功、失敗或取消時(shí)回調(diào)通知調(diào)用方晾嘶;
- 支持關(guān)聯(lián) I/O 操作與對(duì)應(yīng)的處理程序妓雾。
不同協(xié)議、不同的阻塞類型的連接都有不同的 Channel 類型與之對(duì)應(yīng)垒迂。
下面是一些常用的 Channel 類型:
1. NioSocketChannel械姻,異步的客戶端 TCP Socket 連接;
2. NioServerSocketChannel机断,異步的服務(wù)器端 TCP Socket 連接楷拳;
3. NioDatagramChannel,異步的 UDP 連接吏奸;
4. NioSctpChannel欢揖,異步的客戶端 Sctp 連接;
5. NioSctpServerChannel奋蔚,異步的 Sctp 服務(wù)器端連接她混,這些通道涵蓋了 UDP 和 TCP 網(wǎng)絡(luò) IO 以及文件 IO。
Selector
Netty 基于 Selector 對(duì)象實(shí)現(xiàn) I/O 多路復(fù)用泊碑,通過(guò) Selector 一個(gè)線程可以監(jiān)聽(tīng)多個(gè)連接的 Channel 事件坤按。
當(dāng)向一個(gè) Selector 中注冊(cè) Channel 后,Selector 內(nèi)部的機(jī)制就可以自動(dòng)不斷地查詢 (Select) 這些注冊(cè)的 Channel 是否有已就緒的 I/O 事件(例如可讀馒过,可寫臭脓,網(wǎng)絡(luò)連接完成等),這樣程序就可以很簡(jiǎn)單地使用一個(gè)線程高效地管理多個(gè) Channel 沉桌。
NioEventLoop
NioEventLoop 中維護(hù)了一個(gè)線程和任務(wù)隊(duì)列谢鹊,支持異步提交執(zhí)行任務(wù)算吩,線程啟動(dòng)時(shí)會(huì)調(diào)用 NioEventLoop 的 run 方法,執(zhí)行 I/O 任務(wù)和非 I/O 任務(wù):
1. I/O 任務(wù)佃扼,即 selectionKey 中 ready 的事件偎巢,如 accept、connect兼耀、read压昼、write 等,由 processSelectedKeys 方法觸發(fā)瘤运;
2. 非 IO 任務(wù)窍霞,添加到 taskQueue 中的任務(wù),如 register0拯坟、bind0 等任務(wù)但金,由 runAllTasks 方法觸發(fā)。
兩種任務(wù)的執(zhí)行時(shí)間比由變量 ioRatio 控制郁季,默認(rèn)為 50冷溃,則表示允許非 IO 任務(wù)執(zhí)行的時(shí)間與 IO 任務(wù)的執(zhí)行時(shí)間相等。
NioEventLoopGroup
NioEventLoopGroup梦裂,主要管理 eventLoop 的生命周期似枕,可以理解為一個(gè)線程池,內(nèi)部維護(hù)了一組線程年柠,每個(gè)線程 (NioEventLoop) 負(fù)責(zé)處理多個(gè) Channel 上的事件凿歼,而一個(gè) Channel 只對(duì)應(yīng)于一個(gè)線程。
ChannelHandler
ChannelHandler 是一個(gè)接口冗恨,處理 I/O 事件或攔截 I/O 操作答憔,并將其轉(zhuǎn)發(fā)到其 ChannelPipeline(業(yè)務(wù)處理鏈)中的下一個(gè)處理程序。
ChannelHandler 本身并沒(méi)有提供很多方法派近,因?yàn)檫@個(gè)接口有許多的方法需要實(shí)現(xiàn)攀唯,方便使用期間,可以繼承它的子類:
- ChannelInboundHandler 用于處理入站 I/O 事件渴丸;
- ChannelOutboundHandler 用于處理出站 I/O 操作侯嘀。
或者使用以下適配器類:
- ChannelInboundHandlerAdapter 用于處理入站 I/O 事件;
- ChannelOutboundHandlerAdapter 用于處理出站 I/O 操作谱轨;
- ChannelDuplexHandler 用于處理入站和出站事件戒幔。
ChannelHandlerContext
保存 Channel 相關(guān)的所有上下文信息,同時(shí)關(guān)聯(lián)一個(gè) ChannelHandler 對(duì)象土童。
ChannelPipline
保存 ChannelHandler 的 List诗茎,用于處理或攔截 Channel 的入站事件和出站操作。它實(shí)現(xiàn)了一種高級(jí)形式的攔截過(guò)濾器模式,使用戶可以完全控制事件的處理方式敢订,以及 Channel 中各個(gè)的 ChannelHandler 如何相互交互王污。在 Netty 中每個(gè) Channel 都有且僅有一個(gè) ChannelPipeline 與之對(duì)應(yīng)。
關(guān)于 Netty 的簡(jiǎn)介就先說(shuō)這么多楚午。下面的章節(jié)就帶著 Socket 通信應(yīng)該解決的問(wèn)題 和 上面提到的Netty關(guān)鍵組件我們一起看看Netty是如何實(shí)現(xiàn)高性能網(wǎng)絡(luò)通信的昭齐。