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