由于以前从事过电信信令类工做,接触较多的则是ASN.1中的BER、PER编码,其中BER是基于TLV方式进行编码,本文主要介绍一下TLV在自定义协议中的应用。java
经过该文章,你能够肉眼看懂一些相似二进制通讯协议,并能够尝试封装本身的通讯协议c++
协议可使双方不须要了解对方的实现细节的状况下进行通讯,所以双方能够是异构的,server能够是c++,client能够是java,基于相同的协议,咱们能够用本身熟识的语言工具来实现。数组
协议通常由一个或多个消息组成,简单的来讲,消息就像是一个Table,由表头(消息的字段定义,包括名称与数据类型)与行(字段值)组成。网络
约定好双方交换数据的编解码方式,包括一致的基本数据类型,业务类型,字节序、消息内容等。工具
能够跟据业务须要进行定制,如对编解码速度、网络带宽、用户量等进行考量编码
报头(4字节描述数据体长度)+数据(字符串+分隔符或直接使用JSON),该方式实现简单,在编解码阶段成本低、但在数据类型转时成本较高,同时可能会较占用带宽。code
将协议以特定格式编码为字节数组,该种方式相较字符串编码方式实现要求要高一些,但带宽占用相对小一些,本文主要介绍其中一种较经常使用的编码方式TLV,即Tag\Length\Value。orm
TLV:TLV是指由数据的类型Tag,数据的长度Length,数据的值Value组成的结构体,几乎能够描任意数据类型,TLV的Value也能够是一个TLV结构,正由于这种嵌套的特性,可让咱们用来包装协议的实现。server
如下将分别针对Tag、Length、Value进行解说:blog
Tag由一个或多个字节组成,上图描述首字节0~7位的具体含义
后续字节采用每一个字节的0~6位(即7bit)来存储Tag Value, 第7位用来标识是否还有后续字节。
如下提供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); }
描述Value部分所占字节的个数,编码格式分两类:定长方式(DefiniteForm)和不定长方式(IndefiniteForm),其中定长方式又包括短形式与长形式。
定长方式中,按长度是否超过一个八位,又分为短、长两种形式,编码方式以下:
如下提供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; } }
Length所在八位组固定编码为0x80,但在Value编码结束后以两个0x00结尾。这种方式使得能够在编码没有彻底结束的状况下,能够先发送部分数据给对方。
由一个或多个值组成 ,值能够是一个原始数据类型(Primitive Data),也能够是一个TLV结构(Constructed Data)
若是各位看官充分消化了第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 |
这时须要对基本数据类型(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值
名称 | 消息 | 标记:Tag |
---|---|---|
设备故障码 | DEVICE_FAULT_1 | 1 |
经过三层TLV嵌套,完成协议消息的封包
Tips:每层嵌套都有2个或以上的字节增长(Tag和Length),通常通讯双方能够按照协议对数据类型进行推定,因此你们能够根据实际须要,决定是否省略第三层的Tag和Length,便可经过配置文件或其它方式让程序了解字段的类型,从而下降数据包的大小,节省流量。
从上面能够看出,TLV是一种与业务无关的编码方式,能够较容易用来实现自定义协议