因為之前從事過電信信令類工作,接觸較多的則是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實作
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
上表需要關注的是資料類型對應的Tag值與Length值
5.2 協定消息約定
名稱
消息
标記:Tag
裝置故障碼
DEVICE_FAULT_1
1
5.3 示例
通過三層TLV嵌套,完成協定消息的封包
第一層:與協義消息對應
第二層:與消息字段對應
第三層:與字段值對應,包括其值的類型資訊
Tips:每層嵌套都有2個或以上的位元組增加(Tag和Length),一般通信雙方可以按照協定對資料類型進行推定,是以大家可以根據實際需要,決定是否省略第三層的Tag和Length,即可通過配置檔案或其它方式讓程式了解字段的類型,進而降低資料包的大小,節省流量。
6 總結
從上面可以看出,TLV是一種與業務無關的編碼方式,可以較容易用來實作自定義協定