摘要: 在網絡通信開發過程中,很可能需要使用到自定義通信協定,因為之前曾從事過電信信令類工作,接觸較多的則是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結構,正因為這種嵌套的特性,可以讓我們用來包裝協定的實作。
以下将分别針對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 = ;
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之間的。
-
長形式:
即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 < ) {
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結尾。這種方式使得可以在編碼沒有完全結束的情況下,可以先發送部分資料給對方。
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 |
上表需要關注的是資料類型對應的Tag值與Length值
5.2 協定消息約定
名稱 | 消息 | 标記:Tag |
---|---|---|
裝置故障碼 | DEVICE_FAULT_1 | 1 |
5.3 示例
通過三層TLV嵌套,完成協定消息的封包
- 第一層:與協義消息對應
- 第二層:與消息字段對應
- 第三層:與字段值對應,包括其值的類型資訊
Tips:每層嵌套都有2個或以上的位元組增加(Tag和Length),一般通信雙方可以按照協定對資料類型進行推定,是以大家可以根據實際需要,決定是否省略第三層的Tag和Length,即可通過配置檔案或其它方式讓程式了解字段的類型,進而降低資料包的大小,節省流量。
6 總結
從上面可以看出,TLV是一種與業務無關的編碼方式,可以較容易用來實作自定義協定
預告:将來或許會提供一個Go版本的實作