天天看點

自定義通信協定設計之TLV編碼應用

摘要: 在網絡通信開發過程中,很可能需要使用到自定義通信協定,因為之前曾從事過電信信令類工作,接觸較多的則是ASN.1中的BER、PER編碼,其中BER是基于TLV方式進行編碼,本文主要介紹一下TLV編碼及其應用。

因為之前從事過電信信令類工作,接觸較多的則是[ASN.1](http://zh.wikipedia.org/zh-cn/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結構,正因為這種嵌套的特性,可以讓我們用來包裝協定的實作。

![](https://ahq02g.dm1.livefilestore.com/y2pGVRwLpxg1OC06Gsg7iN_tG0sHAzb83R45u43PaLfZDshVot43rvfzX62n89oZmaKkjhNFoH0DShGWgsBC6qKHqr5WBmY6XpAfjkOvpxyPIw/TLV.png?psid=1)

以下将分别針對Tag、Length、Value進行解說:

####4.1. Tag 描述Value的資料類型,TLV嵌套時可以用于描述消息的類型

![](https://ahq02g.dm2303.livefilestore.com/y2pE8maaJOVi2hTlZv13O7S6LxqLsbTzFf7HCG-J-Rnxhg2UWvmKHMTT2tvFMs3zjJGEb7WIdgQE3d8Wu6HroKynVJG2n1j_yFr4ckHlad1-7w/TLV_DISC.png?psid=1)

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将采用後續位元組進行描述。

![](https://ahq02g.dm2302.livefilestore.com/y2ps4GDdL09TgISfrKPQk3Y3px1l-EH0YhcDg7tPR5Nme7OAKXRYZDUKqXGL7gYD8nnN8DG2qwNIJDOICKsL3szoZzGYNsT-V4lTpdfX2t1_vY/TAG_FB.png?psid=1)

#####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。

![](https://ahq02g.dm1.livefilestore.com/y2p0tGpzvc24_EpddfBEZsdEqUBHaRIMFSom4izyJN4ryrf2boD7g4FfqyVtiSqmd5UOc9TuNxHwmsCmkm2JFD8hL-HlOYIcixa6BMgc9_RbgY/TAG_NB.png?psid=1)

以下提供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之間的。

![](https://ahq02g.dm2303.livefilestore.com/y2pTUflngsP_OT4c4ReAdXLsOjD88bI2HcMc1nKHN6bouH9UDCYdGhKk33-EVJ-66Ms2zFv56R724HjvFb1OwB1_DBt1HxA40dtO6qKAfzTpJI/LENGTH-S.png?psid=1)

* 長形式:

即Value類型的長度大于127時,Length需要多個位元組來描述,這時第一個位元組的第7位置為1,0~6位用來描述Length值占用的位元組數,然後直将Length值轉為byte後附在其後,如: Value大小占234個位元組(11101010),由于大于127,這時Length需要使用兩個位元組來描述,10000001 11101010

![](https://ahq02g.dm2302.livefilestore.com/y2pPaMKjeIKEYAljAyvYv2qXf-zukGgyLXdqTgHVOp3e-J7PyObfa_uLeTJPHa7Ny5gPMEeE-LB-_AnOE1YVIC_gA08rP8vfh17yQuw7ngjow8/LENGTH-L.png?psid=1)

以下提供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結尾。這種方式使得可以在編碼沒有完全結束的情況下,可以先發送部分資料給對方。

![](https://ahq02g.dm2301.livefilestore.com/y2p8bAu4O1EEq4cCoORp0uogPl7-CCyC2k31Rdimj1MyNQHVFp47GgO-0oJdsMhshg8zZND53TsNP6lcigss-FvdC8OD_zu4icx49H5NyCzU8w/LENGHT-D.png?psid=1)

####4.3. Value 描述資料的值

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

#####1) Primitive Data 編碼

![](https://ahq02g.dm2303.livefilestore.com/y2pr4FIH8dqYIpQvawEBEajtRXuWPFHRb9zS3EeMttlyi_TJjWTIQgg9MQw2v_qVr740-w6kcn_e6RseACqeUlIeYXiTozKo6lT-1HYuv6rdYY/P-DATA.png?psid=1)

#####2) Constructed Data 編碼

![](https://ahq02g.dm2304.livefilestore.com/y2pafCW8TjTzhOrF86tdHK7Qrfl_01j4lZFrKYObH_Y1ACBcMmo1dat9Eohp30bJKLuDVxo_Y_nwN1wy93gddHzgVh_SbJcXTQD48At8DE2SQI/C-DATA.png?psid=1)

###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嵌套,完成協定消息的封包

* 第一層:與協義消息對應

* 第二層:與消息字段對應

* 第三層:與字段值對應,包括其值的類型資訊

![](https://static.oschina.net/uploads/img/201511/12090646_KY7E.png)

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

###6 總結

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

預告:将來或許會提供一個Go版本的實作

因為之前從事過電信信令類工作,接觸較多的則是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結構,正因為這種嵌套的特性,可以讓我們用來包裝協定的實作。
自定義通信協定設計之TLV編碼應用

以下将分别針對Tag、Length、Value進行解說:

4.1. Tag 描述Value的資料類型,TLV嵌套時可以用于描述消息的類型

自定義通信協定設計之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将采用後續位元組進行描述。
自定義通信協定設計之TLV編碼應用
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。
自定義通信協定設計之TLV編碼應用

以下提供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 = ;
        rawTag = frameType | dataType | tagValue;
        if (tagValue < ) {
            // 1 byte tag
            rawTag = frameType | dataType | tagValue;
        } else {
            // mutli byte tag
            rawTag = frameType | dataType | ;
            if (tagValue < ) {
                rawTag <<= ;
                rawTag |= tagValue & ;
            } else if (tagValue < ) {
                rawTag <<= ;
                rawTag |= (((tagValue & ) >>  & ) | ) << ;
                rawTag |= ((tagValue & ) & );
            } else if (tagValue < ) {
                rawTag <<= ;
                rawTag |= (((tagValue & ) >>  & ) | ) << ;
                rawTag |= (((tagValue & ) >>  & ) | ) << ;
                rawTag |= ((tagValue & ) & );
            }
        }
        return intToByteArray(rawTag);
    }
                

4.2. Length 描述Value的長度

描述Value部分所占位元組的個數,編碼格式分兩類:定長方式(DefiniteForm)和不定長方式(IndefiniteForm),其中定長方式又包括短形式與長形式。
1) 定長方式
定長方式中,按長度是否超過一個八位,又分為短、長兩種形式,編碼方式如下:
  • 短形式: 位元組第7位為0,表示Length使用1個位元組即可滿足Value類型長度的描述,範圍在0~127之間的。
自定義通信協定設計之TLV編碼應用
  • 長形式:

    即Value類型的長度大于127時,Length需要多個位元組來描述,這時第一個位元組的第7位置為1,0~6位用來描述Length值占用的位元組數,然後直将Length值轉為byte後附在其後,如: Value大小占234個位元組(11101010),由于大于127,這時Length需要使用兩個位元組來描述,10000001 11101010

自定義通信協定設計之TLV編碼應用

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

public byte[] parseLength(int length) {
        if (length < ) {
            throw new IllegalArgumentException();
        } else
        // 短形式
        if (length < ) {
            byte[] actual = new byte[];
            actual[] = (byte) length;
            return actual;
        } else
        // 長形式
        if (length < ) {
            byte[] actual = new byte[];
            actual[] = (byte) ;
            actual[] = (byte) length;
            return actual;
        } else if (length < ) {
            byte[] actual = new byte[];
            actual[] = (byte) ;
            actual[] = (byte) (length >> );
            actual[] = (byte) length;
            return actual;
        } else if (length < ) {
            byte[] actual = new byte[];
            actual[] = (byte) ;
            actual[] = (byte) (length >> );
            actual[] = (byte) (length >> );
            actual[] = (byte) length;
            return actual;
        } else {
            byte[] actual = new byte[];
            actual[] = (byte) ;
            actual[] = (byte) (length >> );
            actual[] = (byte) (length >> );
            actual[] = (byte) (length >> );
            actual[] = (byte) length;
            return actual;
        }
    }
           
2) 不定長方式
Length所在八位組固定編碼為0x80,但在Value編碼結束後以兩個0x00結尾。這種方式使得可以在編碼沒有完全結束的情況下,可以先發送部分資料給對方。
自定義通信協定設計之TLV編碼應用

4.3. Value 描述資料的值

由一個或多個值組成 ,值可以是一個原始資料類型(Primitive Data),也可以是一個TLV結構(Constructed Data)
1) Primitive Data 編碼
自定義通信協定設計之TLV編碼應用
2) Constructed Data 編碼
自定義通信協定設計之TLV編碼應用

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
上表需要關注的是資料類型對應的Tag值與Length值

5.2 協定消息約定

名稱 消息 标記:Tag
裝置故障碼 DEVICE_FAULT_1 1

5.3 示例

通過三層TLV嵌套,完成協定消息的封包
  • 第一層:與協義消息對應
  • 第二層:與消息字段對應
  • 第三層:與字段值對應,包括其值的類型資訊
自定義通信協定設計之TLV編碼應用
Tips:每層嵌套都有2個或以上的位元組增加(Tag和Length),一般通信雙方可以按照協定對資料類型進行推定,是以大家可以根據實際需要,決定是否省略第三層的Tag和Length,即可通過配置檔案或其它方式讓程式了解字段的類型,進而降低資料包的大小,節省流量。

6 總結

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

預告:将來或許會提供一個Go版本的實作

繼續閱讀