Netty源码学习6——netty编码解码器&粘包半包问题的解决
系列文章目錄和關(guān)于我
零丶引入
經(jīng)過《Netty源碼學習4——服務(wù)端是處理新連接的&netty的reactor模式和《Netty源碼學習5——服務(wù)端是如何讀取數(shù)據(jù)的》的學習,我們了解了服務(wù)端是如何處理新連接并讀取客戶端發(fā)送的數(shù)據(jù)的:
- netty的reactor:主reactor中的NioEventLoop監(jiān)聽accept事件,然后調(diào)用NioServerSocketChannel#Unsafe讀取數(shù)據(jù)——依賴JDK ServerSockectChannel#accept,獲取到新連接——SockectChannel后,會包裝為NioSocketChannel然后調(diào)用channelRead,隨后ServerBootstrapAcceptor 會負載均衡的選擇一個子reactor 注冊NioSocketChannel對read事件感興趣
- read事件:子reactor中的NioEventLoop會監(jiān)聽read事件,調(diào)用NioSocketChannel讀取客戶端發(fā)送數(shù)據(jù)(依賴JDK SocketChannel#read(ByteBuffer)),netty會使用ByteBufAllocator優(yōu)化ByteBuf的分配,使用AdaptiveRecvByteBufAllocator對ByteBuf進行擴容縮容,以及控制是否繼續(xù)讀取。
——至此數(shù)據(jù)以及讀取到了ByteBuf中,服務(wù)端需要先解碼ByteBuf中的數(shù)據(jù),然后我們業(yè)務(wù)處理器才能根據(jù)發(fā)送的消息進行響應(yīng),業(yè)務(wù)執(zhí)行結(jié)果還需要進行編碼才能發(fā)送,so 這一篇和大家一起學習以下Netty中的編碼解碼。
一丶看看其他開源框架是如何使用Netty的編碼解碼的
1.Dubbo
Apache Dubbo 是一款 RPC 服務(wù)開發(fā)框架,用于解決微服務(wù)架構(gòu)下的服務(wù)治理與通信問題,使用 Dubbo 開發(fā)的微服務(wù)原生具備相互之間的遠程地址發(fā)現(xiàn)與通信能力, 利用 Dubbo 提供的豐富服務(wù)治理特性,可以實現(xiàn)諸如服務(wù)發(fā)現(xiàn)、負載均衡、流量調(diào)度等服務(wù)治理訴求。
Dubbo 中的網(wǎng)絡(luò)通信可以基于Netty,Dubbo 官方源碼如下
可以看到Dubbo會向ChannelPipeline中加入decoder和encoder,負責編碼解碼。
2.Sentinel
Sentinel 是面向分布式、多語言異構(gòu)化服務(wù)架構(gòu)的流量治理組件,主要以流量為切入點,從流量路由、流量控制、流量整形、熔斷降級、系統(tǒng)自適應(yīng)過載保護、熱點流量防護等多個維度來幫助開發(fā)者保障微服務(wù)的穩(wěn)定性。(詳細學習:《Sentinel基本使用與源碼分析》)
sentinel提供了集群限流的能力,本質(zhì)是服務(wù)端控制令牌的下發(fā),客戶端通過網(wǎng)絡(luò)通信申請令牌,如下是集群限流中,使用netty實現(xiàn)服務(wù)端的源碼:
可以看到sentinel集群限流會向ChannelPipeline中增加
-
LengthFieldBasedFrameDecoder:基于長度字段的解碼器——一級解碼器,根據(jù)frame中的長度字段,解碼出消息
-
NettyRequestDecoder:請求解碼器——二次解碼器,將一次解碼器解碼出的消息,反序列化為請求對象
-
LengthFieldPrepender:長度放在frame頭部的編碼器,將服務(wù)端響應(yīng)的消息添加上長度信息
-
NettyResponseEncoder:將服務(wù)端處理返回的java對象,編碼成ByteBuf
3.對比Dubbo和Sentinel對netty的使用
相比于Sentinel,Dubbo的使用更加簡潔,直接將編碼解碼的邏輯封裝到自己的adapter之中
Sentinel的使用也是非常標準,也利于我們理解netty的編解碼運行機制——即編碼解碼其實是ChannelHandler的一種實現(xiàn),通過將編碼解碼加入到ChannelPipline中實現(xiàn)數(shù)據(jù)的逐環(huán)處理。
二丶什么是編碼,解碼器,為什么需要編碼解碼器
netty中的編碼解碼器是負責將應(yīng)用程序的數(shù)據(jù)格式轉(zhuǎn)換為可以在網(wǎng)絡(luò)中傳輸?shù)淖止?jié)流,以及將接收到的字節(jié)流轉(zhuǎn)換回為應(yīng)用程序可以處理的數(shù)據(jù)格式的組件。編解碼器是網(wǎng)絡(luò)通信的關(guān)鍵組件,因為它們抽象掉了網(wǎng)絡(luò)層和應(yīng)用層之間的復(fù)雜轉(zhuǎn)換細節(jié)。
主要作用有:
-
數(shù)據(jù)序列化與反序列化:
- 編碼(序列化):將應(yīng)用數(shù)據(jù)結(jié)構(gòu)(如對象、消息)轉(zhuǎn)換成字節(jié)流,以便能夠通過網(wǎng)絡(luò)發(fā)送。
- 解碼(反序列化):將網(wǎng)絡(luò)中接收到的字節(jié)流轉(zhuǎn)換回應(yīng)用數(shù)據(jù)結(jié)構(gòu)。
-
協(xié)議實現(xiàn):
編解碼器實現(xiàn)了網(wǎng)絡(luò)通信中所需遵守的特定協(xié)議規(guī)則,如 HTTP、WebSocket,SMTP。
它們確保數(shù)據(jù)符合協(xié)議格式,并能夠正確地被發(fā)送和接收方理解。
處理流控制問題: -
對于面向流的協(xié)議(如 TCP),解決粘包和半包等問題,確保數(shù)據(jù)的完整性。
-
解耦應(yīng)用與網(wǎng)絡(luò)層&擴展性與靈活性:
編解碼器允許開發(fā)者專注于業(yè)務(wù)邏輯,而無需關(guān)心底層的字節(jié)處理。應(yīng)用邏輯可以與網(wǎng)絡(luò)傳輸邏輯分離,使得代碼更加清晰和可維護。
應(yīng)用開發(fā)者也可以隨機的切換不同的編碼解碼器,提升擴展性和靈活性。
三丶Netty解決tcp粘包,半包的編解碼器
1.tcp是基于流的協(xié)議&為什么會出現(xiàn)粘包,半包
TCP 傳輸?shù)臄?shù)據(jù)被視為一個連續(xù)的、無邊界的字節(jié)流。網(wǎng)絡(luò)上的兩個應(yīng)用程序通過建立一個 TCP 連接來交換數(shù)據(jù),而這個數(shù)據(jù)流就像是從一個地方倒水到另一個地方,水(數(shù)據(jù))會連續(xù)不斷地流動,而不是一杯一杯分開倒(即不像獨立的消息或數(shù)據(jù)包)。
-
TCP 數(shù)據(jù)發(fā)送:
當應(yīng)用程序要發(fā)送數(shù)據(jù)時,它會
將數(shù)據(jù)寫入到 TCP 套接字的發(fā)送緩沖區(qū)。這個寫入操作通常是通過像 write() 或 send() 這樣的系統(tǒng)調(diào)用完成的。TCP 協(xié)議會從發(fā)送緩沖區(qū)中取出數(shù)據(jù),
并將數(shù)據(jù)分割成合適大小的段,此大小受多個因素影響,包括最大傳輸單元(MTU)和網(wǎng)絡(luò)擁塞窗口(congestion window)。然后,TCP 將每個段封裝在一個 TCP 數(shù)據(jù)包中,并加上 TCP 頭部,其中包含序列號等信息,再將數(shù)據(jù)包發(fā)送到網(wǎng)絡(luò)中。這里的關(guān)鍵點是,
TCP 不關(guān)心應(yīng)用程序傳遞給它的數(shù)據(jù)是一條消息還是多條消息,它只是簡單地將這些數(shù)據(jù)作為字節(jié)序列處理。因此,即使應(yīng)用程序以多個 write() 調(diào)用發(fā)送多條消息,TCP 仍可能將它們合并成一個數(shù)據(jù)包發(fā)送,這就可能導致粘包問題。 -
TCP 數(shù)據(jù)接收:
在接收端,
TCP 數(shù)據(jù)包到達后,TCP 協(xié)議會解析 TCP 頭部信息,并根據(jù)序列號將數(shù)據(jù)放入接收緩沖區(qū)中的正確位置。接收端的應(yīng)用程序通過 read() 或 recv() 等系統(tǒng)調(diào)用從 TCP 套接字的接收緩沖區(qū)中讀取數(shù)據(jù)。這里也是不考慮消息邊界的,應(yīng)用程序可能一次讀取任意大小的數(shù)據(jù),這可能導致一次讀取操作包含了多條消息(粘包),或只有部分消息(半包)。
2.netty是怎么解決粘包,半包問題的
解決粘包,半包問題的關(guān)系,是如何分辨那一部分是一條完整的消息。
Netty 通過提供一系列編解碼器(Decoder 和 Encoder)來解決 TCP 粘包和半包問題。這些編解碼器位于 Netty 的管道(ChannelPipeline)中,它們對進出的數(shù)據(jù)流進行處理,確保數(shù)據(jù)的完整性和邊界的正確性。
-
FixedLengthFrameDecoder:
這個解碼器按照固定的長度對接收到的數(shù)據(jù)進行分割。如果發(fā)送的數(shù)據(jù)小于固定長度,那么發(fā)送方需要進行填充。
-
LineBasedFrameDecoder:
這個解碼器基于換行符(\n 或 \r\n)拆分數(shù)據(jù)流。它適用于文本協(xié)議,如 SMTP 或 POP3。 -
DelimiterBasedFrameDecoder:
這個解碼器根據(jù)指定的分隔符來拆分數(shù)據(jù)流。分隔符可以是任意的字節(jié)序列,如特定的字符或者字符串。 -
LengthFieldBasedFrameDecoder:
這是一個更加通用和靈活的解碼器,它基于消息頭的長度字段來確定每個消息的長度。發(fā)送方在消息頭中指定了消息體的長度,接收方通過解碼器讀取指定長度的數(shù)據(jù),從而確保完整性。 -
LengthFieldPrepender:
這個編碼器在發(fā)送消息的前面添加長度字段,與 LengthFieldBasedFrameDecoder 配合使用,可確保粘包和半包問題不會發(fā)生
3.源碼學習
可以看到解碼器都是ByteToMessageDecoder的子類,編碼器只有LengthFieldPrepender是MessageToMessageEncoder的子類(和LengthFieldBasedFrameDecoder是一對)
3.1 ByteToMessageDecoder
以類似流的方式將字節(jié)從一個ByteBuf解碼為另一個消息類型,是一個ChannelInboundHandler,意味著可以處理入站事件
其中最關(guān)鍵的是channelRead方法
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 只處理ByteBuf類型
if (msg instanceof ByteBuf) {
selfFiredChannelRead = true;
// List的一種實現(xiàn) clear方法不會清空內(nèi)容,recycle方法會清空
// newInstance方法使用FastThreadLocal緩存已有對象,避免重復(fù)構(gòu)造
CodecOutputList out = CodecOutputList.newInstance();
try {
first = cumulation == null;
// cumulation累積器 ,第一次會把傳入的byteBuf和空buf累計
// 后續(xù)會和原有的內(nèi)容進行累計
cumulation = cumulator.cumulate(ctx.alloc(),
first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
// 調(diào)用子類進行解碼
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
try {
// 省略資源釋放部分
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
// 編碼后內(nèi)容觸發(fā)channelRead
fireChannelRead(ctx, out, size);
} finally {
// 釋放資源
out.recycle();
}
}
} else {
// 只處理ByteBuf類型
ctx.fireChannelRead(msg);
}
}
-
netty使用了CodecOutputList來記錄解碼生成的內(nèi)容,也就是說子類實現(xiàn)decode方法時,如果得到了完整的消息,需要將消息加入到CodecOutputList中,CodecOutputList#newInstance是從FastThreadLocal中獲取的,線程安全,每一個線程進行復(fù)用
-
Cumulator:累積器,由于TCP存在粘包,半包的情況,NioSockectChannel在讀取的時候不一定可以讀取到一個完整的消息,所有需要使用Cumulator進行累計,netty提供了兩種累積器的實現(xiàn)
-
合并:顧名思義,會將已經(jīng)積攢的ByteBuf和當前需要累計的ByteBuf進行合并,是真真切切發(fā)生內(nèi)存拷貝的
-
組合:這種策略下,會將已經(jīng)積攢的ByteBuf和當前需要累計的ByteBuf進行組合——生成一個邏輯視圖:CompositeByteBuf
-
-
模板模式:ByteToMessageDecoder將累積的過程進行了抽象,子類只需要實現(xiàn)decode將解碼生成的消息寫入到CodecOutputList中即可
3.1 FixedLengthFrameDecoder 定長消息
使用子類進行解碼,需要保證發(fā)送來的消息長度是一致的!其使用字段frameLength記錄完整消息的長度
如下是解碼源碼:
3.2 LineBasedFrameDecoder 換行符解碼器
顧名思義就是找到換行符所在的位置,分割出一條消息
這個累有點雞肋,因為不支持自定義換行符,如果換行符需要支持指定可以使用DelimiterBasedFrameDecoder
3.3 DelimiterBasedFrameDecoder 支持自定義分割符的解碼器
原理和LineBasedFrameDecoder 類似,內(nèi)部使用delimiters數(shù)組記錄分割符是什么
3.4 LengthFieldBasedFrameDecoder
基于消息頭的長度字段來確定每個消息的長度來解碼出消息,相比于上面幾種,它使用更加廣泛的解碼器(消息定長如果消息太短需要補齊,浪費網(wǎng)絡(luò)資源,換行和分割符解碼同樣會浪費一些網(wǎng)絡(luò)資源)
此類源碼上的注釋詳細解釋了如何使用,它有如下幾個重要的參數(shù):
- maxFrameLength : 發(fā)送的數(shù)據(jù)包最大長度;
- lengthFieldOffset :長度域偏移量,指的是長度域位于整個數(shù)據(jù)包字節(jié)數(shù)組中的下標;
- lengthFieldLength :長度域的自己的字節(jié)數(shù)長度。
- lengthAdjustment :長度域的偏移量矯正。 如果長度域的值,除了包含有效數(shù)據(jù)域的長度外,還包含了其他域(如長度域自身)長度,那么,就需要進行矯正。矯正的值為:包長 - 長度域的值 – 長度域偏移 – 長度域長。
- initialBytesToStrip :丟棄的起始字節(jié)數(shù)。丟棄處于有效數(shù)據(jù)前面的字節(jié)數(shù)量。比如前面有4個節(jié)點的長度域,則它的值為4。
例子:
3.5 LengthFieldPrepender
在發(fā)送消息的前面添加長度字段,與 LengthFieldBasedFrameDecoder 配合使用,可確保粘包和半包問題不會發(fā)生。
因此它是一個ChannelOutboundHandler,其原理也比較簡單,在發(fā)送消息前加上長度信息
四丶總結(jié)&啟下
這一篇我們學習了netty是如何解決TCP協(xié)議中粘包半包的問題,以及粘包半包問題為何會出現(xiàn),并學習netty中常用的編碼解碼器源碼
其實netty對于其他協(xié)議,如:udp,websockect,http,smtp都有對應(yīng)的實現(xiàn),這也是為啥開發(fā)者喜歡使用netty的原因——不需要重復(fù)造*
另外netty還支持多種序列化反序列化方式:json,xml,Protobuf等
后續(xù)應(yīng)該會更新netty追求卓越性能打造的一些*,如FastThreadLocal,對象池,內(nèi)存池,時間輪。以及和學習交流群的小伙伴們一起基于netty寫一個簡陋的rpc框架,鞏固一下netty的使用。
總結(jié)
以上是生活随笔為你收集整理的Netty源码学习6——netty编码解码器&粘包半包问题的解决的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java开发者的Python快速进修指南
- 下一篇: 平稳扩展:可支持RevenueCat每日