看懂通訊協議:自定義通訊協議設計之TLV編碼應用

由於以前從事過電信信令類工做,接觸較多的則是ASN.1中的BER、PER編碼,其中BER是基於TLV方式進行編碼,本文主要介紹一下TLV在自定義協議中的應用。java

經過該文章,你能夠肉眼看懂一些相似二進制通訊協議,並能夠嘗試封裝本身的通訊協議c++

1. 通訊協議

協議可使雙方不須要了解對方的實現細節的狀況下進行通訊,所以雙方能夠是異構的,server能夠是c++,client能夠是java,基於相同的協議,咱們能夠用本身熟識的語言工具來實現。數組

協議通常由一個或多個消息組成,簡單的來講,消息就像是一個Table,由表頭(消息的字段定義,包括名稱與數據類型)與行(字段值)組成。網絡

2. 自定義通訊協議

約定好雙方交換數據的編解碼方式,包括一致的基本數據類型,業務類型,字節序、消息內容等。工具

3. 編碼方式

能夠跟據業務須要進行定製,如對編解碼速度、網絡帶寬、用戶量等進行考量編碼

3.1. 基於字符串編碼

報頭(4字節描述數據體長度)+數據(字符串+分隔符或直接使用JSON),該方式實現簡單,在編解碼階段成本低、但在數據類型轉時成本較高,同時可能會較佔用帶寬。code

3.2. 基於二進制編碼

將協議以特定格式編碼爲字節數組,該種方式相較字符串編碼方式實現要求要高一些,但帶寬佔用相對小一些,本文主要介紹其中一種較經常使用的編碼方式TLV,即Tag\Length\Value。orm

4. TLV編碼介紹( 其中一種實現介紹 )

TLV:TLV是指由數據的類型Tag,數據的長度Length,數據的值Value組成的結構體,幾乎能夠描任意數據類型,TLV的Value也能夠是一個TLV結構,正由於這種嵌套的特性,可讓咱們用來包裝協議的實現。server

如下將分別針對Tag、Length、Value進行解說:blog

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是一種與業務無關的編碼方式,能夠較容易用來實現自定義協議

相關文章
相關標籤/搜索