阿里二面:如何設計一款通訊協議

阿里二面:如何設計一款通訊協議

私信:專欄即可獲取

本文將重點探討網路程式設計中一種非常通用的協議設計方法論:

協議頭 + 訊息體

所謂的通訊協議就是通訊雙方共同遵循的一種“約定”,用於通訊傳送方將內容按照“通訊協議”所規定的格式組裝成

“二進位制流”

,通訊接收方按照“通訊協議”所規定的格式正確的從二進位制流中解碼出一個個原始請求。

那通訊協議如何設計呢?

溫馨提示:本文遵循的目錄結構:先提煉通訊協議設計的通用方法論,然後原始碼分析Netty提供的解決方案,最後給出最佳實踐,大家被錯過最後的實踐部分哦。。。

1、通用的協議設計方法論

在網路程式設計中,流行著一種經典的協議設計方法論:協議頭 + 訊息體。

阿里二面:如何設計一款通訊協議

其設計的關鍵點如下:

協議頭的長度是固定的

,通常為

識別

一個業務的最小長度

協議頭中會包含一個

長度欄位

,用來表示一個完整包的長度,用來表示長度欄位的位元組位數直接決定了一個包的最大長度,長度欄位通常被設計為4個位元組。

訊息體中儲存業務資料,例如如果是一個Dubbo協議,那訊息體中可能會包含請求引數、呼叫的服務名等,而且字串類的儲存通常會採取欄位長度、欄位內容的組織方式。

為了有一個更直觀的展示,我以一個簡單的RPC通訊場景為例,實現類似Dubbo服務的遠端服務呼叫,其通訊協議可以簡單設定成下圖所示:

阿里二面:如何設計一款通訊協議

基於 Header + Boby 的通訊協議設計模式後,通訊接收方就能很好的從二進位制流中非常容易的解碼出一條一條原始的請求資料包,解碼的基本套路如下(

在面試中面試官非常喜歡問的“粘包”問題的破解之道

首先判斷

累積快取區

中是否存在一個

完整的Head頭部

,例如上述示例中,一個包的Header的長度為6個位元組,那首先判斷累積快取中可讀位元組數是否大於等於6,

如果不足6個位元組,跳過本次處理,等待更多資料到達累積快取區

嘗試將頭部6個位元組讀取,並且

提取長度欄位中儲存的數值

,即包長度,然後判斷累積快取區中可讀位元組數大於等於整個包的長度,

如果累積快取區不包含一個完整的資料包,則跳過本次處理,等待更多資料到達累積快取區。

如果包含一個完整的包,則按照通訊協議的格式按序讀取相關的內容。

正是因為這種設計理念非常通用,Netty 對上述協議設計進行了統一封裝:LengthFieldBasedFrameDecoder 閃亮登場了,

接下來我們來看看Netty是如何進行封裝的,揭曉更多的實現細節,讓大家做到理論與實踐相結合。

2、LengthFieldBasedFrameDecoder 詳解

2。1 概述

阿里二面:如何設計一款通訊協議

接下來對其核心屬性進行一個詳細的解讀:

ByteOrder byteOrder

位元組序列,Netty預設使用大端序列(主要是針對int、long等數值型別),所謂的大端序列,通常可以這樣理解,接收端收到的位元組流的順序是從數值型別的高位元組。

int maxFrameLength

一條訊息最大的長度。

int lengthFieldOffset

代表長度欄位的開始偏移量。

int lengthFieldLength

代表長度欄位佔用的位元組長度。

int lengthFieldEndOffset

代表長度欄位的結束偏移量,等於lengthFieldOffset + lengthFieldLength。

int lengthAdjustment

長度適配適配值。該值表示協議中長度欄位與訊息體欄位直接的距離。

int initialBytesToStrip

跳過一個包中前面多少個位元組不處理,

通常是將協議頭部跳過,只將訊息體中內容傳輸到下游時使用

boolean failFast

是否快速失敗。

boolean discardingTooLongFrame

是否吞沒(跳過)大幀包。

long tooLongFrameLength

當前在處理吞沒大包的實際大小。

long bytesToDiscard

下一次解碼之前,需要先忽略的位元組數

,當遇到超過maxFrameLength的包時使用。

上面的屬性如果不太好理解,沒關係,

因為本節的最後會有兩張圖勾畫出協議的全貌(用圖示的方式勾畫出各個屬性的位置與含義)

2。2 decode 方法詳解

接下來我們來看一下其decode方法,透過閱讀原始碼的方法來理解其內部的工作原理。

LengthFieldBasedFrameDecoder#decode

阿里二面:如何設計一款通訊協議

Step1:跳過無效資料包的處理邏輯。如果

discardingTooLongFrame

為true,表示正在處理

大於maxFrameLength

的包,需要跳過這個超長的包,不對其解碼,由於資料是陸續到達累積快取區,並不能一次跳過整個無效包,故需引入 bytesToDiscard 變數,用於記錄本次能跳過的位元組,當 bytesToDiscard 為 0後表示一個無效包已全部跳過,需要處理正常資料包,此時discardingTooLongFrame 會重置為 false。

阿里二面:如何設計一款通訊協議

LengthFieldBasedFrameDecoder#decode

Step2:

如果累積緩衝區的可讀位元組大小小於length欄位的結束偏移量

,返回null,結束解碼,說明該累積快取區中的資料還不完整。

阿里二面:如何設計一款通訊協議

Step3:嘗試從累積快取區中獲取包的長度。其中表示 lengthFiedlOffset 表示長度欄位的其實偏移量,在結合長度欄位的長度 lengthFieldLength ,再結合位元組序列

(大端序列、小端序列)

阿里二面:如何設計一款通訊協議

Step4:這裡是包長度超過協議允許的最大包長度時的處理邏輯,在這裡大家先姑且跳過 lengthAdjustment 屬性的含義。

如果當前累積快取區中的可讀位元組大於 frameLength,大於當前包的長度,可以透過呼叫 skipBytes 方法跳過這包。

如果當前累積快取區的可讀自己小於 frmaeLength,需要分多次跳過,故先將累積區中的資料全部跳過,然後透過 bytesToDiscard 記錄還需要跳過的位元組數。

阿里二面:如何設計一款通訊協議

Step5:

如果累積快取區中的資料不包含一個完整的包,返回null,結束本次解碼,等待更多的資料包的到來。

阿里二面:如何設計一款通訊協議

Step6:透過 ByteBuf 的 slince 方法,提取一個完整的包長度,解碼出完整的資料包,完成一個數據包解碼。

2。3 圖解 LengthFieldBasedFrame 協議

在Netty 的 LengthFieldBasedFrameDecoder 中有一個 lengthAdjustment 屬性,其使用的程式碼片段如下:

frameLength += lengthAdjustment + lengthFieldEndOffset

lengthAdjustment 長度調整欄位,可以為正數,也可以為負數,主要的作用是進行包長度適配的,詳情請看如下分析。

1、lengthAdjustment > 0

阿里二面:如何設計一款通訊協議

2、lengthAdjustment < 0 在大多數情況下,length欄位表示訊息正文的長度,但是有些協議,其長度表示的是整個訊息的長度,故Netty為了適配這種情況,可以透過 lengthAdjustment 設定為負數,來調節資料幀的大小。

阿里二面:如何設計一款通訊協議

總結

:lengthAdjustment 的出現是Netty

為了適配現有的協議而設計出來的欄位

,即 Netty LengthFieldBasedFrameDecoder 是為了i給 header + body ,並且基於長度欄位的協議一種通用的解決方案,可以透過 lengthAdjustment 來準確表示資料幀(業務資料的長度),這裡是一種

逆向思維

3、協議設計子類的最佳實踐

最佳實踐:

LengthFieldBasedFrameDecoder 的 decode 方法的職責是從二進位制流中解碼出一個完整的資料包,其返回型別還是 ByteBuf,故自定義的編碼解碼器的 decode 方法就是先呼叫父類的 decode 方法 得到 ByteBuf ,然後對 ByteBuf 中的資料解碼出物件。

即 LengthFieldBasedFrameDecoder 並不負責將 ByteBuf 轉換為協議物件,而是從二進位制流中解碼出一個數據幀,而將ByteBuf 轉換為協議物件的職責由其子類實現,通常的編碼風格如下:

阿里二面:如何設計一款通訊協議

本文就介紹到這裡了,您的點贊、轉發、留言是對我最大的鼓勵。

分享筆者關於RocketMQ線上故障案例剖析的電子書,私信

回覆RMQPDF即可獲取。

阿里二面:如何設計一款通訊協議

私信回覆【

rmqpdf

】:可獲取千億級訊息流轉的RocketMQ叢集線上故障分析、運維經驗

私信回覆【技術群】:可以加入技術交流群,不求活躍,只求有問題能得到群友的互動交流

私信回覆【專欄】:可以獲取12個Java主流中介軟體原始碼分析專欄。

收藏是點讚的20幾倍,希望大家在收藏的時候,也順手點個贊,感謝。