java tlv协议_看懂通信协议:自定义通信协议设计之TLV编码应用

因为之前从事过电信信令类工作,接触较多的则是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结构,正因为这种嵌套的特性,可以让我们用来包装协议的实现。

TLV.png?psid=1

以下将分别针对Tag、Length、Value进行解说:

4.1. Tag 描述Value的数据类型,TLV嵌套时可以用于描述消息的类型

TLV_DISC.png?psid=1

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将采用后续字节进行描述。

TAG_FB.png?psid=1

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_NB.png?psid=1

以下提供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之间的。

LENGTH-S.png?psid=1

长形式:

即Value类型的长度大于127时,Length需要多个字节来描述,这时第一个字节的第7位置为1,0~6位用来描述Length值占用的字节数,然后直将Length值转为byte后附在其后,如: Value大小占234个字节(11101010),由于大于127,这时Length需要使用两个字节来描述,10000001 11101010

LENGTH-L.png?psid=1

以下提供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结尾。这种方式使得可以在编码没有完全结束的情况下,可以先发送部分数据给对方。

0b7b43262735a08a47e89ddd74bcc28d.png

4.3. Value 描述数据的值

由一个或多个值组成 ,值可以是一个原始数据类型(Primitive Data),也可以是一个TLV结构(Constructed Data)

1) Primitive Data 编码

P-DATA.png?psid=1

2) Constructed Data 编码

C-DATA.png?psid=1

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嵌套,完成协议消息的封包

第一层:与协义消息对应

第二层:与消息字段对应

第三层:与字段值对应,包括其值的类型信息

e9635f0d5a9b053b2e2b3879b1015e36.png

Tips:每层嵌套都有2个或以上的字节增加(Tag和Length),一般通信双方可以按照协议对数据类型进行推定,所以大家可以根据实际需要,决定是否省略第三层的Tag和Length,即可通过配置文件或其它方式让程序了解字段的类型,从而降低数据包的大小,节省流量。

6 总结

从上面可以看出,TLV是一种与业务无关的编码方式,可以较容易用来实现自定义协议

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值