看懂通信協議:自定義通信協議設計之TLV編碼應用

因爲之前從事過電信信令類工作,接觸較多的則是ASN.1中的BER、PER編碼,其中BER是基於TLV方式進行編碼,本文主要介紹一下TLV在自定義協議中的應用。

通過該文章,你可以肉眼看懂一些類似二進制通信協議,並可以嘗試封裝自己的通信協議

1. 通信協議

協議可以使雙方不需要了解對方的實現細節的情況下進行通信,因此雙方可以是異構的,server可以是c++,client可以是java,基於相同的協議,我們可以用自己熟識的語言工具來實現。

協議一般由一個或多個消息組成,簡單的來說,消息就像是一個Table,由表頭(消息的字段定義,包括名稱與數據類型)與行(字段值)組成。

2. 自定義通信協議

約定好雙方交換數據的編解碼方式,包括一致的基本數據類型,業務類型,字節序、消息內容等。

3. 編碼方式

可以跟據業務需要進行定製,如對編解碼速度、網絡帶寬、用戶量等進行考量

3.1. 基於字符串編碼

報頭(4字節描述數據體長度)+數據(字符串+分隔符或直接使用JSON),該方式實現簡單,在編解碼階段成本低、但在數據類型轉時成本較高,同時可能會較佔用帶寬。

3.2. 基於二進制編碼

將協議以特定格式編碼爲字節數組,該種方式相較字符串編碼方式實現要求要高一些,但帶寬佔用相對小一些,本文主要介紹其中一種較常用的編碼方式TLV,即Tag\Length\Value。

4. TLV編碼介紹( 其中一種實現介紹 )

TLV:TLV是指由數據的類型Tag,數據的長度Length,數據的值Value組成的結構體,幾乎可以描任意數據類型,TLV的Value也可以是一個TLV結構,正因爲這種嵌套的特性,可以讓我們用來包裝協議的實現。

以下將分別針對Tag、Length、Value進行解說:

4.1. Tag 描述Value的數據類型,TLV嵌套時可以用於描述消息的類型

Tag由一個或多個字節組成,上圖描述首字節0~7位的具體含義

1) Tag首節字說明

  • 第6~7位:表示TLV的類型,00表示TLV描述的是基本數據類型(Primitive Frame, int,string,long...),01表示用戶自定義類型(Private Frame,常用於描述協議中的消息)。
  • 第5位:表示Value的編碼方式,分別支持Primitive及Constructed兩種編碼方式, Primitive指以原始數據類型進行編碼,Constructed指以TLV方式進行編碼,0表示以Primitive方式編碼,1表示以Constructed方式編碼。
  • 第0~4位:當Tag Value小於0x1F(31)時,首字節0~4位用來描述Tag Value,否則0~4位全部置1,作爲存在後續字節的標誌,Tag Value將採用後續字節進行描述。

2) Tag後續字節說明

後續字節採用每個字節的0~6位(即7bit)來存儲Tag Value, 第7位用來標識是否還有後續字節。

  • 第7位:描述是否還有後續字節,1表示有後續字節,0表示沒有後續字節,即結束字節。
  • 第0~6位:填充Tag Value的對應bit(從低位到高位開始填充),如:Tag Value爲:0000001 11111111 11111111 (10進制:131071), 填充後實際字節內容爲:10000111 11111111 01111111。

以下提供Tag編碼的JAVA實現

    /**
     * 生成 Tag ByteArray
     *
     * @param tagValue  Tag 值,即協議中定義的交易類型 或 基本數據類型
     * @param frameType TLV類型,Tag首字節最左兩bit爲00:基本類型,01:私有類型(自定義類型)
     * @param dataType  數據類型,Tag首字節第5位爲0:基本數據類型,1:結構類型(TLV類型,即TLV的V爲一個TLV結構)
     * @return Tag ByteArray
     */
    public byte[] parseTag(int tagValue, int frameType, int dataType) {
        int size = 1;
        rawTag = frameType | dataType | tagValue;
        if (tagValue < 0x1F) {
            // 1 byte tag
            rawTag = frameType | dataType | tagValue;
        } else {
            // mutli byte tag
            rawTag = frameType | dataType | 0x1F;
            if (tagValue < 0x80) {
                rawTag <<= 8;
                rawTag |= tagValue & 0x7F;
            } else if (tagValue < 0x3FFF) {
                rawTag <<= 16;
                rawTag |= (((tagValue & 0x3FFF) >> 7 & 0x7F) | 0x80) << 8;
                rawTag |= ((tagValue & 0x3FFF) & 0x7F);
            } else if (tagValue < 0x3FFFF) {
                rawTag <<= 24;
                rawTag |= (((tagValue & 0x3FFFF) >> 14 & 0x7F) | 0x80) << 16;
                rawTag |= (((tagValue & 0x3FFFF) >> 7 & 0x7F) | 0x80) << 8;
                rawTag |= ((tagValue & 0x3FFFF) & 0x7F);
            }
        }
        return intToByteArray(rawTag);
    }

4.2. Length 描述Value的長度

描述Value部分所佔字節的個數,編碼格式分兩類:定長方式(DefiniteForm)和不定長方式(IndefiniteForm),其中定長方式又包括短形式與長形式。

1) 定長方式

定長方式中,按長度是否超過一個八位,又分爲短、長兩種形式,編碼方式如下:

  • 短形式: 字節第7位爲0,表示Length使用1個字節即可滿足Value類型長度的描述,範圍在0~127之間的。

  • 長形式:
    即Value類型的長度大於127時,Length需要多個字節來描述,這時第一個字節的第7位置爲1,0~6位用來描述Length值佔用的字節數,然後直將Length值轉爲byte後附在其後,如: Value大小佔234個字節(11101010),由於大於127,這時Length需要使用兩個字節來描述,10000001 11101010

以下提供Length定長方式的JAVA實現

    public byte[] parseLength(int length) {
        if (length < 0) {
            throw new IllegalArgumentException();
        } else
        // 短形式
        if (length < 128) {
            byte[] actual = new byte[1];
            actual[0] = (byte) length;
            return actual;
        } else
        // 長形式
        if (length < 256) {
            byte[] actual = new byte[2];
            actual[0] = (byte) 0x81;
            actual[1] = (byte) length;
            return actual;
        } else if (length < 65536) {
            byte[] actual = new byte[3];
            actual[0] = (byte) 0x82;
            actual[1] = (byte) (length >> 8);
            actual[2] = (byte) length;
            return actual;
        } else if (length < 16777126) {
            byte[] actual = new byte[4];
            actual[0] = (byte) 0x83;
            actual[1] = (byte) (length >> 16);
            actual[2] = (byte) (length >> 8);
            actual[3] = (byte) length;
            return actual;
        } else {
            byte[] actual = new byte[5];
            actual[0] = (byte) 0x84;
            actual[1] = (byte) (length >> 24);
            actual[2] = (byte) (length >> 16);
            actual[3] = (byte) (length >> 8);
            actual[4] = (byte) length;
            return actual;
        }
    }

2) 不定長方式

Length所在八位組固定編碼爲0x80,但在Value編碼結束後以兩個0x00結尾。這種方式使得可以在編碼沒有完全結束的情況下,可以先發送部分數據給對方。

4.3. Value 描述數據的值

由一個或多個值組成 ,值可以是一個原始數據類型(Primitive Data),也可以是一個TLV結構(Constructed Data)

1) Primitive Data 編碼

2) Constructed Data 編碼

5. TLV編碼應用

如果各位看官充分消化了第4點TLV的描述,自然可以很容易將其應用到自定義協議之中,其實我們只要定製各種TLV自定義類型(Private Frame)與協議中的消息一一對應更行了

下面將以一個簡單的協議來描述TLV的應用,假設該協議消息定義如下:

消息名稱 設備故障碼(DEVICE_FAULT_1) Tag值 1  
公共字段定義        
名稱 字段 Tag值 長度 類型
設備編號 DeviceNo 1 4 Integer
設備版本號 DeviceVersion 2 12 String
請求定義        
名稱 字段 Tag值 長度 類型
錯誤碼 FaultCode 3 4 Integer
響應定義        
名稱 字段 Tag值 長度 類型
響應碼 ResponseCode 3 4 Integer
響應信息 ResponseMsg 4 -1 String

5.1 基本數據類型約定

這時需要對基本數據類型(Primitive Data)進行約定,以便通信雙方以一致的方式進行數據轉換,這也作爲協議制定的一部分

基本數據類型約定

名稱 類型 標記:Tag 長度:Length 值範圍:Value
布爾 Boolean 10進制:1, 2進制:00000001 1 1:true .. 0:false
小整型 Tiny 10進制:2, 2進制:00000010 1 -127 .. 127
無符號小整型 UTiny 10進制:3, 2進制:00000011 1 0 .. 255
短整型 Short 10進制:4, 2進制:00000100 2 -32768 .. 32767
無符號短整型 UShort 10進制:5, 2進制:00000101 2 0 .. 65535
整型 Integer 10進制:6, 2進制:00000110 4 -2147483648 .. 2147483648
無符號整型 UInteger 10進制:7, 2進制:00000111 4 0 .. 4294967295
長整型 Long 10進制:8, 2進制:00001000 8 -2^64 .. 2^64
無符號長整型 ULong 10進制:9, 2進制:00001001 8 0 .. 2^128-1
單精浮點類型 Float 10進制:10, 2進制:00001010 4 -2^128 .. 2^128
雙精浮點類型 Double 10進制:11, 2進制:00001011 8 -2^1024 .. 2^1024
字符類型 Char 10進制:12, 2進制:00001100 1 ASCII
字符串類型 String 10進制:13, 2進制:00001101 可變 由一個或多個Char組成
組合類型 Complex 10進制:14, 2進制:00001110 可變 由一個或多個基本類型1~9組成,由協議兩端雙方進行約定編解碼
空類型 Null 10進制:15, 2進制:00001111 0  

上表需要關注的是數據類型對應的Tag值與Length值

5.2 協議消息約定

名稱 消息 標記:Tag
設備故障碼 DEVICE_FAULT_1 1

5.3 示例

通過三層TLV嵌套,完成協議消息的封包

  • 第一層:與協義消息對應
  • 第二層:與消息字段對應
  • 第三層:與字段值對應,包括其值的類型信息

Tips:每層嵌套都有2個或以上的字節增加(Tag和Length),一般通信雙方可以按照協議對數據類型進行推定,所以大家可以根據實際需要,決定是否省略第三層的Tag和Length,即可通過配置文件或其它方式讓程序瞭解字段的類型,從而降低數據包的大小,節省流量。

6 總結

從上面可以看出,TLV是一種與業務無關的編碼方式,可以較容易用來實現自定義協議

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章