編程思想:如何設計一個好的通信網路協議

来源:https://www.cnblogs.com/OceanEyes/archive/2020/03/30/protocol_design.html
-Advertisement-
Play Games

當網路中兩個進程需要通信時,我們往往會使用 來實現。 都不陌生。當三次握手成功後,客戶端與服務端就能通信,並且,彼此之間通信的數據包格式都是二進位,由 協議負責傳輸。 當客戶端和服務端取得了二進位數據包後,我們往往需要『萃取』出想要的數據,這樣才能更好的執行業務邏輯。所以,我們需要定義好數據結構來描 ...


當網路中兩個進程需要通信時,我們往往會使用 Socket 來實現。Socket 都不陌生。當三次握手成功後,客戶端與服務端就能通信,並且,彼此之間通信的數據包格式都是二進位,由 TCP/IP 協議負責傳輸。

當客戶端和服務端取得了二進位數據包後,我們往往需要『萃取』出想要的數據,這樣才能更好的執行業務邏輯。所以,我們需要定義好數據結構來描述這些二進位數據的格式,這就是通信網路協議。簡單講,就是需要約定好二進位數據包中每一段位元組的含義,比如從第 n 位元組開始的 m 長度是核心數據,有了這樣的約定後,我們就能解碼出想要的數據,執行業務邏輯,這樣我們就能暢通無阻的通信了。

網路協議的設計

概要劃分

一個最基本的網路協議必須包含

  • 數據的長度
  • 數據

瞭解 TCP 協議的同學一定聽說過粘包、拆包 這兩個術語。因為TCP協議是數據流協議,它的底層根據二進位緩衝區的實際情況進行包的劃分。所以,不可避免的會出現粘包,拆包 現象 。為瞭解決它們,我們的網路協議往往會使用一個 4 位元組的 int 類型來表示數據的大小。比如,Netty 就為我們提供了 LengthFieldBasedFrameDecoder 解碼器,它可以有效的使用自定義長度幀來解決上述問題。

同時一個好的網路協議,還會將動作和業務數據分離。試想一下, HTTP 協議的分為請求頭,請求體——

  • 請求頭:定義了介面地址、Http MethodHTTP 版本
  • 請求體:定義了需要傳遞的數據

這就是一種分離關註點的思想。所以自定義的網路協議也可以包含:

  • 動作指令:比如定義 code 來分門別類的代表不同的業務邏輯
  • 序列化演算法:描述了 JAVA 對象和二進位之間轉換的形式,提供多種序列化/反序列化方式。比如 jsonprotobuf 等等,甚至是自定義演算法。比如: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.Sharable
public 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;
}

小結

對於諸多中間件而言,底層的網路通信模塊往往會使用 NettyNetty 提供了諸多的編解碼器,可以快速方便的上手。本文從如何設計一個網路協議入手,最終切入到 RocketMQ 底層網路協議的實現。可以看到,它並不複雜。仔細研讀幾遍變能理解其奧義。具體參考類NettyEncoderNettyDecoderRemotingCommand


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • number類型函數 random() 0-1 random(10) 0-10之間的整數 random(100) 0-100之間的整數 編譯後 list數組相關函數 編譯後 @debug 實時返回列印結果,併在控制台輸出 如現在演示的函數操作,沒有實際的css意義,可以直接在控制台輸出 輸出結果 字 ...
  • 盒模型分為標準盒模型和怪異盒模型,他們的區別在於計算內容區的不同。 我們來看input標簽 這是一個高度相同,邊框相同的搜索框和一個提交按鈕。給他們寫長、相同的高和一個1px的邊框。 看 他們在網頁中顯示的高度現在看起來是不一樣的。不止高度不一樣提交按鈕的長度現在也不是和我定的數值一樣。這個提交按鈕 ...
  • 1 <div style="width: 50%"> 2 <table class="layui-table"> 3 <tbody> 4 <tr> 5 <td>Bud</td> 6 <td>179</td> 7 <td>183</td> 8 <td>44</td> 9 <td>37</td> 10 ...
  • 1、ThymeLeaf+LayUI表格渲染錯誤 使用thymeleafhe+layui渲染表格時,出現錯誤org.thymeleaf.exceptions.TemplateProcessingException: Could not parse as expression: 這是因為[[]]是thy ...
  • 將指定數字插入到數組的末尾,返回值為 將數組的第一個元素刪除並返回,返回值為 ...
  • 簡單繼承: @extend 繼承 編譯後 關聯屬性繼承: 編譯後 鏈式繼承: 編譯後 偽類繼承: 編譯後 sass嵌套 編譯後 相同的屬性值首碼,也可以用嵌套 編譯後 sass條件控制 @if @else if @else 編譯後 條件判斷語句也可以寫在外面 編譯後 迴圈 @for $i from ...
  • 對前端稍微有點瞭解的初學者都知道,JavaScript是必不可少的工具。毫不誇張的說,大部分網頁都使用了JavaScript,想要成為一個優秀的前端工程師,做出漂亮令用戶滿意的網頁,熟練掌握JavaScript是一個必備技能。本文為新手整理了一篇JavaScript基礎教程入門指南,希望可以幫助編程 ...
  • number類型 編譯後 color類型: 編譯後 string類型 編譯後 list數組類型 nth(list, num) 獲取list數組中的下標為num的元素 註意num下標是從1開始的 編譯後 index(list, str) 返回str在list數組中的下標 編譯後 map 數組類型(有點 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...