經過《Netty原始碼學習4——伺服器端是處理新連線的&netty的reactor模式和《Netty原始碼學習5——伺服器端是如何讀取資料的》的學習,我們瞭解了伺服器端是如何處理新連線並讀取使用者端傳送的資料的:
——至此資料以及讀取到了ByteBuf中,伺服器端需要先解碼ByteBuf中的資料,然後我們業務處理器才能根據傳送的訊息進行響應,業務執行結果還需要進行編碼才能傳送,so 這一篇和大家一起學習以下Netty中的編碼解碼。
Apache Dubbo 是一款 RPC 服務開發框架,用於解決微服務架構下的服務治理與通訊問題,使用 Dubbo 開發的微服務原生具備相互之間的遠端地址發現與通訊能力, 利用 Dubbo 提供的豐富服務治理特性,可以實現諸如服務發現、負載均衡、流量排程等服務治理訴求。
Dubbo 中的網路通訊可以基於Netty,Dubbo 官方原始碼如下
可以看到Dubbo會向ChannelPipeline中加入decoder和encoder,負責編碼解碼。
Sentinel 是面向分散式、多語言異構化服務架構的流量治理元件,主要以流量為切入點
,從流量路由
、流量控制
、流量整形
、熔斷降級
、系統自適應過載保護
、熱點流量防護
等多個維度來幫助開發者保障微服務的穩定性。(詳細學習:《Sentinel基本使用與原始碼分析》)
sentinel提供了叢集限流的能力,本質是伺服器端控制令牌的下發,使用者端通過網路通訊申請令牌,如下是叢集限流中,使用netty實現伺服器端的原始碼:
可以看到sentinel叢集限流會向ChannelPipeline中增加
LengthFieldBasedFrameDecoder:基於長度欄位的解碼器——一級解碼器,根據frame中的長度欄位,解碼出訊息
NettyRequestDecoder:請求解碼器——二次解碼器,將一次解碼器解碼出的訊息,反序列化為請求物件
LengthFieldPrepender:長度放在frame頭部的編碼器,將伺服器端響應的訊息新增上長度資訊
NettyResponseEncoder:將伺服器端處理返回的java物件,編碼成ByteBuf
相比於Sentinel,Dubbo的使用更加簡潔,直接將編碼解碼的邏輯封裝到自己的adapter之中
Sentinel的使用也是非常標準,也利於我們理解netty的編解碼執行機制——即編碼解碼其實是ChannelHandler的一種實現,通過將編碼解碼加入到ChannelPipline中實現資料的逐環處理。
netty中的編碼解碼器是負責將應用程式的資料格式轉換為可以在網路中傳輸的位元組流
,以及將接收到的位元組流轉換回為應用程式可以處理的資料格式的元件
。編解碼器是網路通訊的關鍵元件,因為它們抽象掉了網路層和應用層之間的複雜轉換細節。
主要作用有:
資料序列化與反序列化:
協定實現:
編解碼器實現了網路通訊中所需遵守的特定協定規則,如 HTTP、WebSocket,SMTP。
它們確保資料符合協定格式,並能夠正確地被傳送和接收方理解。
處理流控制問題:
對於面向流的協定(如 TCP),解決粘包和半包等問題,確保資料的完整性。
解耦應用與網路層&擴充套件性與靈活性:
編解碼器允許開發者專注於業務邏輯,而無需關心底層的位元組處理。應用邏輯可以與網路傳輸邏輯分離,使得程式碼更加清晰和可維護。
應用開發者也可以隨機的切換不同的編碼解碼器,提升擴充套件性和靈活性。
TCP 傳輸的資料被視為一個連續的、無邊界的位元組流。網路上的兩個應用程式通過建立一個 TCP 連線來交換資料,而這個資料流就像是從一個地方倒水到另一個地方,水(資料)會連續不斷地流動,而不是一杯一杯分開倒(即不像獨立的訊息或封包)。
TCP 資料傳送:
當應用程式要傳送資料時,它會將資料寫入到 TCP 通訊端的傳送緩衝區
。這個寫入操作通常是通過像 write() 或 send() 這樣的系統呼叫完成的。
TCP 協定會從傳送緩衝區中取出資料,並將資料分割成合適大小的段
,此大小受多個因素影響,包括最大傳輸單元(MTU)和網路擁塞視窗(congestion window)。然後,TCP 將每個段封裝在一個 TCP 封包中,並加上 TCP 頭部,其中包含序列號等資訊,再將封包傳送到網路中。
這裡的關鍵點是,TCP 不關心應用程式傳遞給它的資料是一條訊息還是多條訊息,它只是簡單地將這些資料作為位元組序列處理
。因此,即使應用程式以多個 write() 呼叫傳送多條訊息,TCP 仍可能將它們合併成一個封包傳送,這就可能導致粘包問題
。
TCP 資料接收:
在接收端,TCP 封包到達後,TCP 協定會解析 TCP 頭部資訊,並根據序列號將資料放入接收緩衝區中的正確位置
。
接收端的應用程式通過 read() 或 recv() 等系統呼叫從 TCP 通訊端的接收緩衝區中讀取資料。這裡也是不考慮訊息邊界的,應用程式可能一次讀取任意大小的資料,這可能導致一次讀取操作包含了多條訊息(粘包),或只有部分訊息(半包)
。
解決粘包,半包問題的關係,是如何分辨那一部分是一條完整的訊息。
Netty 通過提供一系列編解碼器(Decoder 和 Encoder)來解決 TCP 粘包和半包問題。這些編解碼器位於 Netty 的管道(ChannelPipeline)中,它們對進出的資料流進行處理,確保資料的完整性和邊界的正確性。
FixedLengthFrameDecoder:
這個解碼器按照固定的長度對接收到的資料進行分割。如果傳送的資料小於固定長度,那麼傳送方需要進行填充。
LineBasedFrameDecoder:
這個解碼器基於換行符(\n 或 \r\n)拆分資料流。它適用於文字協定,如 SMTP 或 POP3。
DelimiterBasedFrameDecoder:
這個解碼器根據指定的分隔符來拆分資料流。分隔符可以是任意的位元組序列,如特定的字元或者字串。
LengthFieldBasedFrameDecoder:
這是一個更加通用和靈活的解碼器,它基於訊息頭的長度欄位來確定每個訊息的長度。傳送方在訊息頭中指定了訊息體的長度,接收方通過解碼器讀取指定長度的資料,從而確保完整性。
LengthFieldPrepender:
這個編碼器在傳送訊息的前面新增長度欄位,與 LengthFieldBasedFrameDecoder 配合使用,可確保粘包和半包問題不會發生
可以看到解碼器都是ByteToMessageDecoder的子類,編碼器只有LengthFieldPrepender是MessageToMessageEncoder的子類(和LengthFieldBasedFrameDecoder是一對)
以類似流的方式將位元組從一個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中即可
使用子類進行解碼,需要保證傳送來的訊息長度是一致的!其使用欄位frameLength記錄完整訊息的長度
如下是解碼原始碼:
顧名思義就是找到換行符所在的位置,分割出一條訊息
這個累有點雞肋,因為不支援自定義換行符,如果換行符需要支援指定可以使用DelimiterBasedFrameDecoder
原理和LineBasedFrameDecoder 類似,內部使用delimiters陣列記錄分割符是什麼
基於訊息頭的長度欄位來確定每個訊息的長度來解碼出訊息,相比於上面幾種,它使用更加廣泛的解碼器(訊息定長如果訊息太短需要補齊,浪費網路資源,換行和分割符解碼同樣會浪費一些網路資源)
此類原始碼上的註釋詳細解釋瞭如何使用,它有如下幾個重要的引數:
例子:
在傳送訊息的前面新增長度欄位,與 LengthFieldBasedFrameDecoder 配合使用,可確保粘包和半包問題不會發生。
因此它是一個ChannelOutboundHandler,其原理也比較簡單,在傳送訊息前加上長度資訊
這一篇我們學習了netty是如何解決TCP協定中粘包半包的問題,以及粘包半包問題為何會出現,並學習netty中常用的編碼解碼器原始碼
其實netty對於其他協定,如:udp,websockect,http,smtp都有對應的實現,這也是為啥開發者喜歡使用netty的原因——不需要重複造輪子
另外netty還支援多種序列化反序列化方式:json,xml,Protobuf等
後續應該會更新netty追求卓越效能打造的一些輪子,如FastThreadLocal,物件池,記憶體池,時間輪。以及和學習交流群的小夥伴們一起基於netty寫一個簡陋的rpc框架,鞏固一下netty的使用。