Netty:我為啥這么受歡迎你們不知道嗎?
前言
上文《Netty 編程看上去懵懵的...》通過比較 Java NIO 和 Netty 的工作流程,并結(jié)合 Netty 的源碼,可以更加清晰地理解Netty。本文將結(jié)合源碼詳細(xì)解析Netty的高效和強(qiáng)大功能的設(shè)計(jì)原理,學(xué)習(xí) Netty 是如何實(shí)現(xiàn)其卓越的性能和功能特性,也希望可以在日后工作中利用到 Netty 的設(shè)計(jì)思想。
Netty 解決的問題
我們先看看使用 Netty 在網(wǎng)絡(luò)編程中幫助我們解決了什么問題。
簡(jiǎn)化網(wǎng)絡(luò)編程
首先,基于 Netty 初次編碼的直觀體驗(yàn)來講,開發(fā)者不用手動(dòng)處理網(wǎng)絡(luò)通信細(xì)節(jié),包括線程管理、I/O 處理、協(xié)議解析等,可以專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。也正是因?yàn)槿绱?,在學(xué)習(xí) Netty 時(shí)比較抽象難懂 。
如下圖,可以看到 Java NIO 的代碼大概有 80 行,而且還沒有實(shí)現(xiàn) HTTP 協(xié)議,并且還是單線程,沒有復(fù)雜的線程管理,更不用說性能什么的。
而 Netty 實(shí)現(xiàn)的代碼只有 30 多行,其中的差別一目了然。
圖片
粘包和拆包
我們一般說粘包和拆包都是說 TCP 協(xié)議的問題,因?yàn)楫?dāng)用戶消息通過 UDP 協(xié)議傳輸時(shí),操作系統(tǒng)不會(huì)對(duì)消息進(jìn)行拆分,所以發(fā)送出去的一條 UDP 報(bào)文就是完整的用戶消息,也就是每個(gè) UDP 報(bào)文就是用戶消息的邊界。
而當(dāng)用戶消息通過 TCP 協(xié)議傳輸時(shí),消息可能會(huì)被操作系統(tǒng)分組成多個(gè)的 TCP 報(bào)文進(jìn)行傳輸,這個(gè)時(shí)候接收方收到多個(gè)報(bào)文后,由于不知道消息的邊界,也就無法讀出一個(gè)完整的消息。
舉個(gè)例子,當(dāng)發(fā)送方準(zhǔn)備發(fā)送 「Hi」和「I am Erdan」這兩個(gè)消息,由于MTU限制、緩沖區(qū)的大小等條件,可能會(huì)出現(xiàn)幾種情況:
第一種情況,兩條消息分到一個(gè)報(bào)文中,像這樣:
圖片
第二種情況,「I am Erdan」中的部分消息隨「Hi」被分到一個(gè)報(bào)文中,像這樣:
圖片
還可能會(huì)有第三、四...種情況。
當(dāng)接收方接收到第一種情況時(shí)我們稱之為粘包,第二種情況稱之為拆包。
上面的種種情況表明,一個(gè)用戶消息不能對(duì)應(yīng)一個(gè) TCP 報(bào)文,正因?yàn)檫@樣,所以 TCP 是面向字節(jié)流的協(xié)議。
粘包和拆包解決手段
解決粘包和拆包的根本手段就是找出消息的邊界,有幾種方式:
- 固定消息長度,這種方式靈活性不高,實(shí)際中很少用。
- 特殊字符作為邊界,HTTP 是一個(gè)非常好的例子,通過設(shè)置回車符、換行符作為 HTTP 報(bào)文協(xié)議的邊界。
- 自定義消息結(jié)構(gòu):消息頭消息體,可以自定義一個(gè)消息結(jié)構(gòu),由包頭和數(shù)據(jù)組成,其中包頭包是固定大小的,而且包頭里有一個(gè)字段來說明緊隨其后的數(shù)據(jù)有多大。
圖片
HTTP格式
Netty的編解碼器
Netty 提供了固定長度解碼器(FixedLengthFrameDecoder)、行分隔符解碼器(LineBasedFrameDecoder)、分隔符解碼器(DelimiterBasedFrameDecoder)、基于長度字段的解碼器(LengthFieldBasedFrameDecoder)幾種方式來解決粘包問題,可以結(jié)合 Netty 的 ChannelPipeline 來使用。
除此之外 Netty 也提供了 HTTP、WebSocket、TCP、UDP幾種協(xié)議的編解碼器,這也是 Netty 靈活擴(kuò)展強(qiáng)大之處。
高性能的設(shè)計(jì)
Netty 除了幫助開發(fā)人員解決了一些問題,還提高了網(wǎng)絡(luò)編程性能,體現(xiàn)如下
多線程調(diào)度
在網(wǎng)絡(luò)編程中如果使用單線程來處理,即便是IO多路復(fù)用,吞吐和性能也是會(huì)有局限的。
而 Netty 中通過 EventLoopGroup 管理線程池,每個(gè)線程就是一個(gè) EventLoop。EventLoop 內(nèi)部有一個(gè) Selector 負(fù)責(zé)處理一個(gè)或多個(gè) Channel 的注冊(cè)、讀寫和其他事件。
所以 Netty 通過 EventLoopGroup、EventLoop 和 Selector 的配合工作,實(shí)現(xiàn)了高效的并發(fā)處理能力。
圖片
線程安全保障
既然是多線程處理,肯定要去考慮線程安全以確保程序的正確性。
Netty 是如何保障線程安全的?
Netty 通過使用管道(ChannelPipeline)和處理器(ChannelHandler)的方式來實(shí)現(xiàn)數(shù)據(jù)的處理和流轉(zhuǎn)。而 ChannelHandler 會(huì)被分配給一個(gè) EventLoop 處理, EventLoop 內(nèi)部的數(shù)據(jù)結(jié)構(gòu)和狀態(tài)都是線程封閉的,不會(huì)被其他線程訪問或修改。
所以 Netty 通過合理地設(shè)計(jì)組件之間的關(guān)系,通過單線程執(zhí)行、無鎖設(shè)計(jì)等方式保證了在高并發(fā)情況下的線程安全性。
零拷貝
在傳統(tǒng)的網(wǎng)絡(luò)編程中,數(shù)據(jù)在進(jìn)行網(wǎng)絡(luò)傳輸之前需要從應(yīng)用層緩沖區(qū)復(fù)制到操作系統(tǒng)內(nèi)核的緩沖區(qū),然后再從內(nèi)核的緩沖區(qū)復(fù)制到網(wǎng)絡(luò)設(shè)備的緩沖區(qū)。這種復(fù)制操作會(huì)增加 CPU 的負(fù)載和內(nèi)存的開銷,如下圖
圖片
而 Netty 利用零拷貝技術(shù)來減少數(shù)據(jù)復(fù)制的次數(shù),提高了數(shù)據(jù)傳輸?shù)男省?/p>
零拷貝將數(shù)據(jù)從內(nèi)核空間直接傳輸?shù)骄W(wǎng)絡(luò)適配器,避免了數(shù)據(jù)在內(nèi)核空間和用戶空間之間的復(fù)制,從而減少了CPU的負(fù)擔(dān)。如下圖
圖片
Netty的零拷貝體現(xiàn)在以下幾個(gè)方面:
- 零拷貝文件傳輸:Netty 的 FileRegion 接口提供了直接在文件系統(tǒng)和網(wǎng)絡(luò)之間傳輸數(shù)據(jù)的功能。通過使用零拷貝技術(shù),數(shù)據(jù)可以直接從磁盤讀取并發(fā)送到網(wǎng)絡(luò)設(shè)備,避免了中間的緩沖區(qū)拷貝,提高了文件傳輸?shù)男阅堋?/li>
- 零拷貝內(nèi)存?zhèn)鬏敚篘etty 的 ByteBuf 類型支持零拷貝的內(nèi)存?zhèn)鬏?。?dāng)數(shù)據(jù)在應(yīng)用程序和內(nèi)核之間傳輸時(shí),Netty 使用直接內(nèi)存緩沖區(qū)(Direct ByteBuffer)來避免額外的數(shù)據(jù)拷貝操作,提高了內(nèi)存?zhèn)鬏數(shù)男省?/li>
通過以上方式,Netty 實(shí)現(xiàn)了零拷貝技術(shù)在網(wǎng)絡(luò)編程中的應(yīng)用,提高了數(shù)據(jù)傳輸?shù)男屎托阅?。這使得 Netty 在處理大量數(shù)據(jù)傳輸和高并發(fā)場(chǎng)景下具有更好的性能表現(xiàn)。
總結(jié)
總的來說,Netty 不論在功能、性能以及穩(wěn)定性來講都是一款很nice的網(wǎng)絡(luò)編程框架,很多知名的項(xiàng)目都將 Netty 作為其網(wǎng)絡(luò)通信的底層框架,比如Apache Kafka、Elasticsearch、gRPC、Dubbo等。熟悉這些框架的開發(fā)者通常都具備高并發(fā)開發(fā)經(jīng)驗(yàn),并且掌握 Netty 是理解這些框架的重要基礎(chǔ)之一。
本文轉(zhuǎn)載自微信公眾號(hào)「Hi程序員」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Hi程序員公眾號(hào)。