看懂通信协议:自定义通信协议设计之TLV编码应用
為什么80%的碼農(nóng)都做不了架構(gòu)師?>>> ??
因?yàn)橹皬氖逻^電信信令類工作,接觸較多的則是ASN.1中的BER、PER編碼,其中BER是基于TLV方式進(jìn)行編碼,本文主要介紹一下TLV在自定義協(xié)議中的應(yīng)用。
通過該文章,你可以肉眼看懂一些類似二進(jìn)制通信協(xié)議,并可以嘗試封裝自己的通信協(xié)議
1. 通信協(xié)議
協(xié)議可以使雙方不需要了解對(duì)方的實(shí)現(xiàn)細(xì)節(jié)的情況下進(jìn)行通信,因此雙方可以是異構(gòu)的,server可以是c++,client可以是java,基于相同的協(xié)議,我們可以用自己熟識(shí)的語(yǔ)言工具來實(shí)現(xiàn)。
協(xié)議一般由一個(gè)或多個(gè)消息組成,簡(jiǎn)單的來說,消息就像是一個(gè)Table,由表頭(消息的字段定義,包括名稱與數(shù)據(jù)類型)與行(字段值)組成。
2. 自定義通信協(xié)議
約定好雙方交換數(shù)據(jù)的編解碼方式,包括一致的基本數(shù)據(jù)類型,業(yè)務(wù)類型,字節(jié)序、消息內(nèi)容等。
3. 編碼方式
可以跟據(jù)業(yè)務(wù)需要進(jìn)行定制,如對(duì)編解碼速度、網(wǎng)絡(luò)帶寬、用戶量等進(jìn)行考量
3.1. 基于字符串編碼
報(bào)頭(4字節(jié)描述數(shù)據(jù)體長(zhǎng)度)+數(shù)據(jù)(字符串+分隔符或直接使用JSON),該方式實(shí)現(xiàn)簡(jiǎn)單,在編解碼階段成本低、但在數(shù)據(jù)類型轉(zhuǎn)時(shí)成本較高,同時(shí)可能會(huì)較占用帶寬。
3.2. 基于二進(jìn)制編碼
將協(xié)議以特定格式編碼為字節(jié)數(shù)組,該種方式相較字符串編碼方式實(shí)現(xiàn)要求要高一些,但帶寬占用相對(duì)小一些,本文主要介紹其中一種較常用的編碼方式TLV,即Tag\Length\Value。
4. TLV編碼介紹( 其中一種實(shí)現(xiàn)介紹 )
TLV:TLV是指由數(shù)據(jù)的類型Tag,數(shù)據(jù)的長(zhǎng)度Length,數(shù)據(jù)的值Value組成的結(jié)構(gòu)體,幾乎可以描任意數(shù)據(jù)類型,TLV的Value也可以是一個(gè)TLV結(jié)構(gòu),正因?yàn)檫@種嵌套的特性,可以讓我們用來包裝協(xié)議的實(shí)現(xiàn)。
以下將分別針對(duì)Tag、Length、Value進(jìn)行解說:
4.1. Tag 描述Value的數(shù)據(jù)類型,TLV嵌套時(shí)可以用于描述消息的類型
Tag由一個(gè)或多個(gè)字節(jié)組成,上圖描述首字節(jié)0~7位的具體含義
1) Tag首節(jié)字說明
- 第6~7位:表示TLV的類型,00表示TLV描述的是基本數(shù)據(jù)類型(Primitive Frame, int,string,long...),01表示用戶自定義類型(Private Frame,常用于描述協(xié)議中的消息)。
- 第5位:表示Value的編碼方式,分別支持Primitive及Constructed兩種編碼方式, Primitive指以原始數(shù)據(jù)類型進(jìn)行編碼,Constructed指以TLV方式進(jìn)行編碼,0表示以Primitive方式編碼,1表示以Constructed方式編碼。
- 第0~4位:當(dāng)Tag Value小于0x1F(31)時(shí),首字節(jié)0~4位用來描述Tag Value,否則0~4位全部置1,作為存在后續(xù)字節(jié)的標(biāo)志,Tag Value將采用后續(xù)字節(jié)進(jìn)行描述。
2) Tag后續(xù)字節(jié)說明
后續(xù)字節(jié)采用每個(gè)字節(jié)的0~6位(即7bit)來存儲(chǔ)Tag Value, 第7位用來標(biāo)識(shí)是否還有后續(xù)字節(jié)。
- 第7位:描述是否還有后續(xù)字節(jié),1表示有后續(xù)字節(jié),0表示沒有后續(xù)字節(jié),即結(jié)束字節(jié)。
- 第0~6位:填充Tag Value的對(duì)應(yīng)bit(從低位到高位開始填充),如:Tag Value為:0000001 11111111 11111111 (10進(jìn)制:131071), 填充后實(shí)際字節(jié)內(nèi)容為:10000111 11111111 01111111。
以下提供Tag編碼的JAVA實(shí)現(xiàn)
/*** 生成 Tag ByteArray** @param tagValue Tag 值,即協(xié)議中定義的交易類型 或 基本數(shù)據(jù)類型* @param frameType TLV類型,Tag首字節(jié)最左兩bit為00:基本類型,01:私有類型(自定義類型)* @param dataType 數(shù)據(jù)類型,Tag首字節(jié)第5位為0:基本數(shù)據(jù)類型,1:結(jié)構(gòu)類型(TLV類型,即TLV的V為一個(gè)TLV結(jié)構(gòu))* @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的長(zhǎng)度
描述Value部分所占字節(jié)的個(gè)數(shù),編碼格式分兩類:定長(zhǎng)方式(DefiniteForm)和不定長(zhǎng)方式(IndefiniteForm),其中定長(zhǎng)方式又包括短形式與長(zhǎng)形式。
1) 定長(zhǎng)方式
定長(zhǎng)方式中,按長(zhǎng)度是否超過一個(gè)八位,又分為短、長(zhǎng)兩種形式,編碼方式如下:
- 短形式: 字節(jié)第7位為0,表示Length使用1個(gè)字節(jié)即可滿足Value類型長(zhǎng)度的描述,范圍在0~127之間的。
- 長(zhǎng)形式:
即Value類型的長(zhǎng)度大于127時(shí),Length需要多個(gè)字節(jié)來描述,這時(shí)第一個(gè)字節(jié)的第7位置為1,0~6位用來描述Length值占用的字節(jié)數(shù),然后直將Length值轉(zhuǎn)為byte后附在其后,如: Value大小占234個(gè)字節(jié)(11101010),由于大于127,這時(shí)Length需要使用兩個(gè)字節(jié)來描述,10000001 11101010
以下提供Length定長(zhǎng)方式的JAVA實(shí)現(xiàn)
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// 長(zhǎng)形式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) 不定長(zhǎng)方式
Length所在八位組固定編碼為0x80,但在Value編碼結(jié)束后以兩個(gè)0x00結(jié)尾。這種方式使得可以在編碼沒有完全結(jié)束的情況下,可以先發(fā)送部分?jǐn)?shù)據(jù)給對(duì)方。
4.3. Value 描述數(shù)據(jù)的值
由一個(gè)或多個(gè)值組成 ,值可以是一個(gè)原始數(shù)據(jù)類型(Primitive Data),也可以是一個(gè)TLV結(jié)構(gòu)(Constructed Data)
1) Primitive Data 編碼
2) Constructed Data 編碼
5. TLV編碼應(yīng)用
如果各位看官充分消化了第4點(diǎn)TLV的描述,自然可以很容易將其應(yīng)用到自定義協(xié)議之中,其實(shí)我們只要定制各種TLV自定義類型(Private Frame)與協(xié)議中的消息一一對(duì)應(yīng)更行了
下面將以一個(gè)簡(jiǎn)單的協(xié)議來描述TLV的應(yīng)用,假設(shè)該協(xié)議消息定義如下:
| 公共字段定義 | ||||
| 名稱 | 字段 | Tag值 | 長(zhǎng)度 | 類型 |
| 設(shè)備編號(hào) | DeviceNo | 1 | 4 | Integer |
| 設(shè)備版本號(hào) | DeviceVersion | 2 | 12 | String |
| 請(qǐng)求定義 | ||||
| 名稱 | 字段 | Tag值 | 長(zhǎng)度 | 類型 |
| 錯(cuò)誤碼 | FaultCode | 3 | 4 | Integer |
| 響應(yīng)定義 | ||||
| 名稱 | 字段 | Tag值 | 長(zhǎng)度 | 類型 |
| 響應(yīng)碼 | ResponseCode | 3 | 4 | Integer |
| 響應(yīng)信息 | ResponseMsg | 4 | -1 | String |
5.1 基本數(shù)據(jù)類型約定
這時(shí)需要對(duì)基本數(shù)據(jù)類型(Primitive Data)進(jìn)行約定,以便通信雙方以一致的方式進(jìn)行數(shù)據(jù)轉(zhuǎn)換,這也作為協(xié)議制定的一部分
基本數(shù)據(jù)類型約定
| 布爾 | Boolean | 10進(jìn)制:1, 2進(jìn)制:00000001 | 1 | 1:true .. 0:false |
| 小整型 | Tiny | 10進(jìn)制:2, 2進(jìn)制:00000010 | 1 | -127 .. 127 |
| 無符號(hào)小整型 | UTiny | 10進(jìn)制:3, 2進(jìn)制:00000011 | 1 | 0 .. 255 |
| 短整型 | Short | 10進(jìn)制:4, 2進(jìn)制:00000100 | 2 | -32768 .. 32767 |
| 無符號(hào)短整型 | UShort | 10進(jìn)制:5, 2進(jìn)制:00000101 | 2 | 0 .. 65535 |
| 整型 | Integer | 10進(jìn)制:6, 2進(jìn)制:00000110 | 4 | -2147483648 .. 2147483648 |
| 無符號(hào)整型 | UInteger | 10進(jìn)制:7, 2進(jìn)制:00000111 | 4 | 0 .. 4294967295 |
| 長(zhǎng)整型 | Long | 10進(jìn)制:8, 2進(jìn)制:00001000 | 8 | -2^64 .. 2^64 |
| 無符號(hào)長(zhǎng)整型 | ULong | 10進(jìn)制:9, 2進(jìn)制:00001001 | 8 | 0 .. 2^128-1 |
| 單精浮點(diǎn)類型 | Float | 10進(jìn)制:10, 2進(jìn)制:00001010 | 4 | -2^128 .. 2^128 |
| 雙精浮點(diǎn)類型 | Double | 10進(jìn)制:11, 2進(jìn)制:00001011 | 8 | -2^1024 .. 2^1024 |
| 字符類型 | Char | 10進(jìn)制:12, 2進(jìn)制:00001100 | 1 | ASCII |
| 字符串類型 | String | 10進(jìn)制:13, 2進(jìn)制:00001101 | 可變 | 由一個(gè)或多個(gè)Char組成 |
| 組合類型 | Complex | 10進(jìn)制:14, 2進(jìn)制:00001110 | 可變 | 由一個(gè)或多個(gè)基本類型1~9組成,由協(xié)議兩端雙方進(jìn)行約定編解碼 |
| 空類型 | Null | 10進(jìn)制:15, 2進(jìn)制:00001111 | 0 |
上表需要關(guān)注的是數(shù)據(jù)類型對(duì)應(yīng)的Tag值與Length值
5.2 協(xié)議消息約定
| 設(shè)備故障碼 | DEVICE_FAULT_1 | 1 |
5.3 示例
通過三層TLV嵌套,完成協(xié)議消息的封包
- 第一層:與協(xié)義消息對(duì)應(yīng)
- 第二層:與消息字段對(duì)應(yīng)
- 第三層:與字段值對(duì)應(yīng),包括其值的類型信息
Tips:每層嵌套都有2個(gè)或以上的字節(jié)增加(Tag和Length),一般通信雙方可以按照協(xié)議對(duì)數(shù)據(jù)類型進(jìn)行推定,所以大家可以根據(jù)實(shí)際需要,決定是否省略第三層的Tag和Length,即可通過配置文件或其它方式讓程序了解字段的類型,從而降低數(shù)據(jù)包的大小,節(jié)省流量。
6 總結(jié)
從上面可以看出,TLV是一種與業(yè)務(wù)無關(guān)的編碼方式,可以較容易用來實(shí)現(xiàn)自定義協(xié)議
轉(zhuǎn)載于:https://my.oschina.net/maxid/blog/206546
總結(jié)
以上是生活随笔為你收集整理的看懂通信协议:自定义通信协议设计之TLV编码应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 多线程原理(一)
- 下一篇: Oracle CRS stack is