如何設計一個好的通訊網路協議
閱讀目錄
當網路中兩個程式需要通訊時,我們往往會使用
Socket
來實現。
Socket
都不陌生。當三次握手成功後,客戶端與服務端就能通訊,並且,彼此之間通訊的資料包格式都是二進位制,由
TCP/IP
協議負責傳輸。
當客戶端和服務端取得了二進位制資料包後,我們往往需要『萃取』出想要的資料,這樣才能更好的執行業務邏輯。所以,我們需要定義好資料結構來描述這些二進位制資料的格式,這就是通訊網路協議。簡單講,就是需要約定好二進位制資料包中每一段位元組的含義,比如從第 n 位元組開始的 m 長度是核心資料,有了這樣的約定後,我們就能解碼出想要的資料,執行業務邏輯,這樣我們就能暢通無阻的通訊了。
網路協議的設計
概要劃分
一個最基本的網路協議必須包含
- 資料的長度
- 資料
瞭解
TCP
協議的同學一定聽說過
粘包、拆包
這兩個術語。因為
TCP
協議是資料流協議,它的底層根據二進位制緩衝區的實際情況進行包的劃分。所以,不可避免的會出現
粘包,拆包
現象 。為了解決它們,我們的網路協議往往會使用一個 4 位元組的
int
型別來表示資料的大小。比如,
Netty
就為我們提供了
LengthFieldBasedFrameDecoder
解碼器,它可以有效的使用自定義長度幀來解決上述問題。
同時一個好的網路協議,還會將動作和業務資料分離。試想一下,
HTTP
協議的分為請求頭,請求體——
- 請求頭:定義了介面地址、
Http Method
、HTTP
版本 - 請求體:定義了需要傳遞的資料
這就是一種分離關注點的思想。所以自定義的網路協議也可以包含:
- 動作指令:比如定義
code
來分門別類的代表不同的業務邏輯 - 序列化演算法:描述了
JAVA
物件和二進位制之間轉換的形式,提供多種序列化/反序列化方式。比如json
、protobuf
等等,甚至是自定義演算法。比如:rocketmq
等等。
同時,協議的開頭可以定義一個約定的
魔數
。這個固定值(4位元組),一般用來判斷當前的資料包是否合法。比如,當我們使用
telnet
傳送錯誤的資料包時,很顯然,它不合法,會導致解碼失敗。所以,為了減輕伺服器的壓力,我們可以取出資料包的前
4
個位元組與固定的
魔數
對比,如果是非法的格式,直接關閉連線,不繼續解碼。
網路協議結構如下所示:
+--------------+-----------+------------+-----------+----------+ | 魔數(4) | code(1) |序列化演算法(1) |資料長度(4) |資料(n) | +--------------+-----------+------------+-----------+----------+
RocketMQ 通訊網路協議的實現
RocketMQ 網路協議
這一小節,我們從
RocketMQ
中,分析優秀通訊網路協議的實現。
RocketMQ
專案中,客戶端和服務端的通訊是基於 Netty 之上構建的。同時,為了更加有效的通訊,往往需要對傳送的訊息自定義網路協議。
RocketMQ
的網路協議,從資料分類的角度上看,可分為兩大類
- 訊息頭資料(Header Data)
- 訊息體資料(Body Data)
從左到右
-
第一段:4 個位元組整數,等於2、3、4 長度總和
-
第二段:4 個位元組整數,等於3 的長度。特別的
byte[0]
代表序列化演算法,byte[1~3]
才是真正的長度 -
第三段:代表訊息頭資料,結構如下
{ "code":0, "language":"JAVA", "version":0, "opaque":0, "flag":1, "remark":"hello, I am respponse /127.0.0.1:27603", "extFields":{ "count":"0", "messageTitle":"HelloMessageTitle" } }
- 第四段:代表訊息體資料
RocketMQ 訊息頭協議詳細如下:
Header 欄位名 | 型別 | Request | Response |
---|---|---|---|
code | 整數 | 請求操作程式碼,請求接收方根據不同的程式碼做不同的操作 | 應答結果程式碼,0表示成功,非0表示各種錯誤程式碼 |
language | 字串 | 請求發起方實現語言,預設JAVA | 應答接收方實現語言 |
version | 整數 | 請求發起方程式版本 | 應答接收方程式版本 |
opaque | 整數 | 請求發起方在同一連線上不同的請求標識程式碼,多執行緒連線複用使用 | 應答方不做修改,直接返回 |
flag | 整數 | 通訊層的標誌位 | 通訊層的標誌位 |
remark | 字串 | 傳輸自定義文字資訊 | 錯誤詳細描述資訊 |
extFields | HashMap<String,String> | 請求自定義欄位 | 應答自定義欄位 |
編碼過程
RocketMQ
的通訊模組是基於
Netty
的。透過定義
NettyEncoder
來實現對每一個
Channel
的 出棧資料進行編碼,如下所示:
@ChannelHandler.Sharablepublic class NettyEncoder extends MessageToByteEncoder<RemotingCommand> { @Override public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out) throws Exception { try { ByteBuffer header = remotingCommand.encodeHeader(); out.writeBytes(header); byte[] body = remotingCommand.getBody(); if (body != null) { out.writeBytes(body); } } catch (Exception e) { ... } } }
其中,核心的編碼過程位於
RemotingCommand
物件中,
encodeHeader
階段,需要統計出訊息總長度,即:
-
定義訊息頭長度,一個整數表示:佔4個位元組
-
定義訊息頭資料,並計算其長度
-
定義訊息體資料,並計算其長度
-
額外再加 4是因為需要加入訊息總長度,一個整數表示:佔4個位元組
public ByteBuffer encodeHeader(final int bodyLength) { // 1> 訊息頭長度,一個整數表示:佔4個位元組 int length = 4; // 2> 訊息頭資料 byte[] headerData; headerData = this.headerEncode(); // 再加訊息頭資料長度 length += headerData.length; // 3> 再加訊息體資料長度 length += bodyLength; // 4> 額外加 4是因為需要加入訊息總長度,一個整數表示:佔4個位元組 ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength); // 5> 將訊息總長度加入 ByteBuffer result.putInt(length); // 6> 將訊息的頭長度加入 ByteBuffer result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC)); // 7> 將訊息頭資料加入 ByteBuffer result.put(headerData); result.flip(); return result; }
其中,
encode
階段會將
CommandCustomHeader
資料轉換
HashMap<String,String>
,方便序列化
public void makeCustomHeaderToNet() { if (this.customHeader != null) { Field[] fields = getClazzFields(customHeader.getClass()); if (null == this.extFields) { this.extFields = new HashMap<String, String>(); } for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers())) { String name = field.getName(); if (!name.startsWith("this")) { Object value = null; try { field.setAccessible(true); value = field.get(this.customHeader); } catch (Exception e) { log.error("Failed to access field [{}]", name, e); } if (value != null) { this.extFields.put(name, value.toString()); } } } } } }
特別的,訊息頭序列化支援兩種演算法:
-
JSON
-
RocketMQ
private byte[] headerEncode() { this.makeCustomHeaderToNet(); if (SerializeType.ROCKETMQ == serializeTypeCurrentRPC) { return RocketMQSerializable.rocketMQProtocolEncode(this); } else { return RemotingSerializable.encode(this); } }
這兒需要值得注意的是,
encode
階段將當前
RPC
型別和
headerData
長度編碼到一個
byte[4]
陣列中,
byte[0]
位序列化型別。
public static byte[] markProtocolType(int source, SerializeType type) { byte[] result = new byte[4]; result[0] = type.getCode(); result[1] = (byte) ((source >> 16) & 0xFF); result[2] = (byte) ((source >> 8) & 0xFF); result[3] = (byte) (source & 0xFF); return result; }
其中,透過與運算
& 0xFF
取低八位資料。
所以, 最終
length
長度等於序列化型別 + header length + header data + body data 的位元組的長度。
解碼過程
RocketMQ
解碼透過
NettyDecoder
來實現,它繼承自
LengthFieldBasedFrameDecoder
,其中呼叫了父類
LengthFieldBasedFrameDecoder
的建構函式
super(FRAME_MAX_LENGTH, 0, 4, 0, 4);
這些引數設定
4
個位元組代表
length
總長度,同時解碼時跳過最開始的
4
個位元組:
frame = (ByteBuf) super.decode(ctx, in);
所以,得到的
frame
= 序列化型別 + header length + header data + body data 。解碼如下所示:
public static RemotingCommand decode(final ByteBuffer byteBuffer) { //總長度 int length = byteBuffer.limit(); //原始的 header length,4位 int oriHeaderLen = byteBuffer.getInt(); //真正的 header data 長度。忽略 byte[0]的 serializeType int headerLength = getHeaderLength(oriHeaderLen); byte[] headerData = new byte[headerLength]; byteBuffer.get(headerData); RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen)); int bodyLength = length - 4 - headerLength; byte[] bodyData = null; if (bodyLength > 0) { bodyData = new byte[bodyLength]; byteBuffer.get(bodyData); } cmd.body = bodyData; return cmd; }private static RemotingCommand headerDecode(byte[] headerData, SerializeType type) { switch (type) { case JSON: RemotingCommand resultJson = RemotingSerializable.decode(headerData, RemotingCommand.class); resultJson.setSerializeTypeCurrentRPC(type); return resultJson; case ROCKETMQ: RemotingCommand resultRMQ = RocketMQSerializable.rocketMQProtocolDecode(headerData); resultRMQ.setSerializeTypeCurrentRPC(type); return resultRMQ; default: break; } return null; }
其中,
getProtocolType
,右移
24
位,拿到
serializeType
:
public static SerializeType getProtocolType(int source) { return SerializeType.valueOf((byte) ((source >> 24) & 0xFF)); }
getHeaderLength
拿到 0-24 位代表的
headerData
length:
public static int getHeaderLength(int length) { return length & 0xFFFFFF; }
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69969697/viewspace-2683841/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 網路通訊協議協議
- 網路通訊協議-ICMP協議詳解!協議
- 網路通訊協議-TCP協議詳解!協議TCP
- 網路通訊協議-HTTP協議詳解!協議HTTP
- 網路通訊協議-SMTP協議詳解!協議
- 一個簡單混合協議通訊列子,物聯網和網際網路通訊。協議
- 快速理解網路通訊協議協議
- 通訊協議和網路協議有什麼區別協議
- 造輪子 | 如何設計一個面向協議的 iOS 網路請求庫協議iOS
- 如何動手實現一個自定義的通訊協議?協議
- UDP協議網路Socket程式設計(java實現C/S通訊案例)UDP協議程式設計Java
- 網路通訊協議基礎(ISIS)——入門協議
- Java:基於TCP協議網路socket程式設計(實現C/S通訊)JavaTCP協議程式設計
- 無線通訊協議設計的幾點要素協議
- 通訊協議協議
- ??面試官:你會如何設計QQ中的網路協議?面試協議
- 網路通訊程式設計程式設計
- 計算機網路中的通訊子網:架構、協議與技術簡介計算機網路架構協議
- Visual C++設計UDP協議通訊示例C++UDP協議
- 工業通訊協議(一)- CAN協議
- Redis 通訊協議Redis協議
- HTTP通訊協議HTTP協議
- Mysql通訊協議MySql協議
- MQ通訊協議MQ協議
- web通訊協議Web協議
- 物聯網通訊協議介紹協議
- 如何從零開始定義一個類似websocket的即時通訊協議Web協議
- Android程式設計師必知必會的網路通訊傳輸層協議——UDP和TCPAndroid程式設計師協議UDPTCP
- 網站建設-如何才能設計好一個網站網站
- 網路通訊協議自動轉換之thrift到http協議HTTP
- HTTP協議的通訊框架HTTP協議框架
- WLAN常用的通訊協議協議
- Python 基於 TCP 傳輸協議的網路通訊實現PythonTCP協議
- 網路協議之:WebSocket的訊息格式協議Web
- 通過故事引申網路協議TCP協議TCP
- 物聯網常見通訊協議梳理協議
- Dubbo-通訊協議協議
- 串列埠通訊協議串列埠協議