因爲之前從事過電信信令類工作,接觸較多的則是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是一種與業務無關的編碼方式,可以較容易用來實現自定義協議