看懂通信协议:自定义通信协议设计之TLV编码应用
為什么80%的碼農都做不了架構師?>>> ??
因為之前從事過電信信令類工作,接觸較多的則是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 tagrawTag = frameType | dataType | tagValue;} else {// mutli byte tagrawTag = 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的應用,假設該協議消息定義如下:
| 公共字段定義 | ||||
| 名稱 | 字段 | 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)進行約定,以便通信雙方以一致的方式進行數據轉換,這也作為協議制定的一部分
基本數據類型約定
| 布爾 | 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 協議消息約定
| 設備故障碼 | DEVICE_FAULT_1 | 1 |
5.3 示例
通過三層TLV嵌套,完成協議消息的封包
- 第一層:與協義消息對應
- 第二層:與消息字段對應
- 第三層:與字段值對應,包括其值的類型信息
Tips:每層嵌套都有2個或以上的字節增加(Tag和Length),一般通信雙方可以按照協議對數據類型進行推定,所以大家可以根據實際需要,決定是否省略第三層的Tag和Length,即可通過配置文件或其它方式讓程序了解字段的類型,從而降低數據包的大小,節省流量。
6 總結
從上面可以看出,TLV是一種與業務無關的編碼方式,可以較容易用來實現自定義協議
轉載于:https://my.oschina.net/maxid/blog/206546
總結
以上是生活随笔為你收集整理的看懂通信协议:自定义通信协议设计之TLV编码应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 多线程原理(一)
- 下一篇: Oracle CRS stack is