一、前言
在网络数据传输过程中,TLV格式的消息比较常见。那么什么是TLV格式?
一个完整的TLV消息是由 消息头部+消息体(1)+消息体(2)+消息体(n) 组成。
如下图所示:
- TLV消息头
根据上图所示,TLV的消息头是由28个字节组成,且长度固定不变。
length
:消息的总长度,即TLV消息头长度 + TLV消息体长度。(4字节)
version
:消息的版本号,目前填2,且在使用中一般是固定不变的。(2字节)
commandId
:消息的命令类型,接受消息或者发送消息,根据需要填写。(2字节)
seqence
:消息序列号,范围在0x01 ~ 0x7FFFFFFF
之间。(4字节)
checkNumber
:消息校验值,一般保留不使用。(16字节) - TLV消息体
由上图得知,TLV消息体是由一个或者多个TLV消息单体组成,单个消息体长度是不定的,因为有的value是整数型,有的value是字符串等其它类型。
tag
:消息tag/消息id,一般接受TLV消息方可以通过此tag。有的地方将tag解释为type,其实无论怎么理解,tag所占长度是不变的。(2字节)
length
:单个消息体value的长度,整型一般为4位,字符串需要用strlen()极端。(2字节)
value
:消息值,字节数有数据类型决定。
二、TLV应用
下面我们来模拟用TLV格式进行网络数据传输,采用TCP短链发送消息。TCP服务端用网络调试助手来接受消息。Demo示例:Gitee
需要发送的数据如下:
-
实现tcp发送功能
tcp_socket.h
代码如下:// // Created by jerry on 2021/4/5. // #ifndef C_TLV_DEMO_TCP_SOCKET_H #define C_TLV_DEMO_TCP_SOCKET_H #define TCP_SERVER_IP "192.168.2.142" #define TCP_SERVER_PORT 8080 /* 需要发送的tcp消息 */ struct TCP_MSG { char msgLength; // 消息长度 char *msg; // 消息体 }; /* 通过tcp短链接发送消息 */ int send_message(struct TCP_MSG *tcpMsg); #endif //C_TLV_DEMO_TCP_SOCKET_H
tcp_socket.c
代码如下:// // Created by jerry on 2021/4/5. // #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include "tcp_socket.h" #define DEBUG 1 #define LOGD(fmt, ...) {if (DEBUG == 1 ) printf("[D][%s:%d] "fmt"\n", __FUNCTION__, __LINE__, ##__VA_ARGS__);} /* 关闭socket套接字 */ void close_tcp_socket(int fd) { close(fd); } /* 建立tcp链接 */ int open_tcp_socket_connect() { int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd <= 0) { LOGD("open socket failed!"); return -1; } const char *server_ip = TCP_SERVER_IP; unsigned short server_port = TCP_SERVER_PORT; struct sockaddr_in serverAddr; bzero(&serverAddr, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(server_port); inet_pton(AF_INET, server_ip, &serverAddr.sin_addr); if (0 != connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr))) { LOGD("tcp connect failed!"); close_tcp_socket(fd); return -1; } return fd; } /* 发送消息并关闭tcp链接 */ int send_message(struct TCP_MSG *tcpMsg) { if (!tcpMsg || tcpMsg->msgLength == 0 || !tcpMsg->msg) { LOGD("TCP_MSG or msgLength or msg is NULL, return."); return -1; } int fd = open_tcp_socket_connect(); if (0 > fd) { return -1; } int len = send(fd, tcpMsg->msg, tcpMsg->msgLength, 0); if (0 >= len) { LOGD("send tcp message failed!"); } close_tcp_socket(fd); return 0; }
- 实现构建TLV消息代码
build_tlv.h
代码如下:
#ifndef __BUILD_TLV_H__ #define __BUILD_TLV_H__ static int TLV_MSG_SEQENCE = 1; // TLV 消息序列号,从1开始递增,取值范围为0x00000000~0x7FFFFFFF #define TLV_MSG_SEQENCE_MIN = 0x01; // TLV 消息序列号最小值 #define TLV_MSG_SEQENCE_MAX = 0x7FFFFFFF; // TLV 消息序列号最大值 /** * TLV 消息命令类型,即此消息是要发送的消息还是接受的消息 **/ enum TLV_COMMAND_ID { SEND_MSG = 0x0002, // 发送消息 RECV_MSG = 0x0003, // 接受消息 }; /** * TLV 消息的类型,每个 TLV_BODY 都有一个类型 **/ typedef enum TLV_BODY_TYPE { TLV_BODY_TYPE_STRING = 0x00, // 字符串型 TLV_BODY_TYPE_INT = 0x01, // 整数型 } TLV_BODY_TYPE; /** * TLV 消息头部,长度共28位 **/ struct TLV_HEAD { int tlvHeadLength; // TLV 消息总长度,即TLV_HEAD + TLV_BODY short tlvHeadVersion; // TLV 消息版本号 short tlvHeadCommandId; // TLV 消息命令类型 int tlvHeadSeqence; // TLV 消息序列号 char tlvHeadCheckNumber[16]; // TLV 消息校验 }; /** * TLV 消息体 * * 实际上tlvBodyValueLength和tlvBodyValue在代码中并未引用 * 而是通过build_tlv_body()将这部分数值组建到buffer里面 * 这样做的目的是: * 1.为了减少代码含量,将重复部分封装到build_tlv_body()里面, * 用户只关心tlvBodyType、tlvBodyTag和需要组建的value值即可。 * 里面的一些大小端转化用户也无需关注。根据需要传入实际值即可。 * 2.在头文件中便于查看TLV_BODY的结构 **/ struct TLV_BODY { int tlvBodyType; // TLV 消息值的类型(TLV_BODY_TYPE) short tlvBodyTag; // TLV 消息tag(消息编号id),根据此tag解析value short tlvBodyValueLength; // TLV 消息值的长度 char tlvBodyValue[0]; // TLV 消息值 }; /** * TLV 消息 **/ struct TLV_MSG { struct TLV_HEAD tlvHead; struct TLV_BODY tlvBody; }; /* 组建TLV消息(消息头 + 消息体) */ void *build_tlv_msg(struct TLV_MSG *tlvMsg, char *buffer, void *value); #endif
build_tlv.c
代码如下:#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include "build_tlv.h" #define DEBUG 1 #define LOGD(fmt, ...) {if (DEBUG == 1 ) printf("[D][%s:%d] "fmt"\n", __FUNCTION__, __LINE__, ##__VA_ARGS__);} /********************************************************************************** * Function: void *buid_tlv_head(struct TLV_HEAD *head, char *buffer) * Description: 按网络字节序组建TLV消息的头部 (网络中发送数据时采用此方法) * Input: struct TLV_MSG *tlvMsg TLV 消息结构体 * char *buffer TLV 消息存储buffer * Return: NULL **********************************************************************************/ static void *build_tlv_head(struct TLV_MSG *tlvMsg, char *buffer) { if (!tlvMsg || !buffer) { LOGD("Head or buffer is null, return.") return NULL; } /* TLV 消息头部处理 (进行网络字节序转换) */ tlvMsg->tlvHead.tlvHeadLength = htonl(0); // (int) TLV 消息的总长度,包括该字段本身。由于第一次构建消息时长度未知,因此先置0,等到消息体构建完毕时再填充 tlvMsg->tlvHead.tlvHeadVersion = htons(2); // (short) TLV 消息的版本号,目前填2 tlvMsg->tlvHead.tlvHeadCommandId = htons(SEND_MSG); // (short) TLV 消息命令类型 tlvMsg->tlvHead.tlvHeadSeqence = htonl(TLV_MSG_SEQENCE); // (int) TLV 消息序列号 memset(tlvMsg->tlvHead.tlvHeadCheckNumber, 0x00, sizeof(tlvMsg->tlvHead.tlvHeadCheckNumber)); // (char) 校验值,保留不使用 /* 组装TLV 消息头 */ memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &tlvMsg->tlvHead.tlvHeadLength, sizeof(tlvMsg->tlvHead.tlvHeadLength)); // 0~3位存储 tlvHeadLength (注意tlvHeadLength初始值为0) tlvMsg->tlvHead.tlvHeadLength += sizeof(tlvMsg->tlvHead.tlvHeadLength); // tlvHeadLength(4) = 0 + 4 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &tlvMsg->tlvHead.tlvHeadVersion, sizeof(tlvMsg->tlvHead.tlvHeadVersion)); // 4~5位存储 tlvHeadVersion tlvMsg->tlvHead.tlvHeadLength += sizeof(tlvMsg->tlvHead.tlvHeadVersion); // tlvHeadLength(6) = 4 + 2 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &tlvMsg->tlvHead.tlvHeadCommandId, sizeof(tlvMsg->tlvHead.tlvHeadCommandId)); // 6~7位存储 tlvHeadCommandId tlvMsg->tlvHead.tlvHeadLength += sizeof(tlvMsg->tlvHead.tlvHeadCommandId); // tlvHeadLength(8) = 6 + 2 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &tlvMsg->tlvHead.tlvHeadSeqence, sizeof(tlvMsg->tlvHead.tlvHeadSeqence)); // 8~11位存储 tlvHeadSeqence tlvMsg->tlvHead.tlvHeadLength += sizeof(tlvMsg->tlvHead.tlvHeadSeqence); // tlvHeadLength(12) = 8 + 4 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, tlvMsg->tlvHead.tlvHeadCheckNumber, sizeof(tlvMsg->tlvHead.tlvHeadCheckNumber)); // 12~27位存储 tlvHeadCheckNumber tlvMsg->tlvHead.tlvHeadLength += sizeof(tlvMsg->tlvHead.tlvHeadCheckNumber); // tlvHeadLength(28) = 12 + 16 /* TLV 消息序列号处理,若tlvHeadSeqence大于0x7FFFFFFF则初始化置1 */ if (0x7FFFFFFF > TLV_MSG_SEQENCE) { TLV_MSG_SEQENCE ++; } else { TLV_MSG_SEQENCE = 1; } return NULL; } /********************************************************************************** * Function: void *build_tlv_msg(struct TLV_MSG *tlvMsg, * char *buffer, void *value) * Description: 按网络字节序组建TLV消息体 * Input: struct TLV_MSG *tlvMsg TLV 消息结构体 * char *buffer TLV 消息存储buffer * void *value 需要组建的值 * Return: NULL **********************************************************************************/ void *build_tlv_msg(struct TLV_MSG *tlvMsg, char *buffer, void *value) { if (!tlvMsg || !buffer || !value) { LOGD("Head or buffer or value is null, return.") return NULL; } /* 若TLV_HEAD还没有构建,则先构建TLV_HEAD */ if (tlvMsg->tlvHead.tlvHeadLength == 0) { LOGD("tlvHead is NULL, build!"); build_tlv_head(tlvMsg, buffer); } /* 根据tlvBodyType对数据进行处理 */ switch (tlvMsg->tlvBody.tlvBodyType) { /* String 型数据处理 */ case TLV_BODY_TYPE_STRING: { /* tag */ short tag = tlvMsg->tlvBody.tlvBodyTag; short tagNbo = htons(tag); /* value */ char *val = (char *)value; // 字符串类型的数据在网络中传输不用转换大小端 /* value length */ short valLength = strlen(val); short valLengthNbo = htons(valLength); LOGD("(## String ##) Tag: %d, Length: %d, Value: %s", tag, valLength, val); memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &tagNbo, sizeof(tagNbo)); // 28~29位存储 tlvBodyTag (这个时候TLV_HEAD已经构建完成) tlvMsg->tlvHead.tlvHeadLength += sizeof(tagNbo); // tlvHeadLength(30) = 28 + 2 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &valLengthNbo, sizeof(valLengthNbo)); // 30~31位存储 tlvBodyValueLength tlvMsg->tlvHead.tlvHeadLength += sizeof(valLengthNbo); // tlvHeadLength(32) = 30 + 2 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, val, strlen(val)); // 32~n位存储 tlvBodyValue tlvMsg->tlvHead.tlvHeadLength += strlen(val); // tlvHeadLength(n) = 32 + sizeof(valLengthNbo) } break; /* int 型数据处理 */ case TLV_BODY_TYPE_INT: { /* tag */ short tag = tlvMsg->tlvBody.tlvBodyTag; short tagNbo = htons(tag); /* value */ int val = *(int *)value; int valNbo = htonl(val); /* value length */ short valLength = sizeof(val); short valLengthNbo = htons(valLength); LOGD("(## int ##) Tag: %d, Length: %d, Value: %d", tag, valLength, val); memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &tagNbo, sizeof(tagNbo)); // 28~29位存储 tlvBodyTag (这个时候TLV_HEAD已经构建完成) tlvMsg->tlvHead.tlvHeadLength += sizeof(tagNbo); // tlvHeadLength(30) = 28 + 2 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &valLengthNbo, sizeof(valLengthNbo)); // 30~31位存储 tlvBodyValueLength tlvMsg->tlvHead.tlvHeadLength += sizeof(valLengthNbo); // tlvHeadLength(32) = 30 + 2 memcpy(buffer + tlvMsg->tlvHead.tlvHeadLength, &valNbo, sizeof(valNbo)); // 32~35位存储 tlvBodyValue tlvMsg->tlvHead.tlvHeadLength += sizeof(valNbo); // tlvHeadLength(36) = 32 + 4 } break; default: LOGD("Other tlv body type.") break; } /* 给TLV_HEAD.tlvHeadLength 赋值 */ int tlvHeadLength = tlvMsg->tlvHead.tlvHeadLength; int tlvHeadLengthNbo = htonl(tlvHeadLength); memcpy(buffer, &tlvHeadLengthNbo, sizeof(tlvHeadLengthNbo)); // TLV_HEAD.tlvHeadLength占头部前四位,因此buffer的指针不用移动,直接赋值即可 return NULL; }
- 实现
main.c
示例使用代码
代码如下:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include "build_tlv.h" #include "tcp_socket.h" #define DEBUG 1 #define LOGD(fmt, ...) {if (DEBUG == 1 ) printf("[D][%s:%d] "fmt"\n", __FUNCTION__, __LINE__, ##__VA_ARGS__);} static const int count = 100; // tag: 1022 static const char hello[10] = "Hello TLV"; // tag: 1023 static const char *into = "test"; // tag: 1024 static void tlv_test() { /* TLV_MSG 结构体 */ struct TLV_MSG tlvMsg; memset(&tlvMsg, 0x00, sizeof(tlvMsg)); /* 最终组建的tlv消息buffer */ char buffer[4096] = {0}; memset(buffer, 0x00, sizeof buffer); /* 添加TLV的BODY,这儿只传入tlvBodyType、 tlvBodyTag、和BODY具体的内容value即可 */ tlvMsg.tlvBody.tlvBodyType = TLV_BODY_TYPE_INT; tlvMsg.tlvBody.tlvBodyTag = 1022; build_tlv_msg(&tlvMsg, buffer, (void *)(&count)); /* 添加TLV的BODY,这儿只传入tlvBodyType、 tlvBodyTag、和BODY具体的内容value即可 */ tlvMsg.tlvBody.tlvBodyType = TLV_BODY_TYPE_STRING; tlvMsg.tlvBody.tlvBodyTag = 1023; build_tlv_msg(&tlvMsg, buffer, (void *)hello); /* 添加TLV的BODY,这儿只传入tlvBodyType、 tlvBodyTag、和BODY具体的内容value即可 */ tlvMsg.tlvBody.tlvBodyType = TLV_BODY_TYPE_STRING; tlvMsg.tlvBody.tlvBodyTag = 1024; build_tlv_msg(&tlvMsg, buffer, (void *)into); /* 通过TCP短链接发送消息 */ struct TCP_MSG *tcpMsg = NULL; tcpMsg = (struct TCP_MSG *)calloc(1, sizeof(struct TCP_MSG)); tcpMsg->msgLength = tlvMsg.tlvHead.tlvHeadLength; tcpMsg->msg = (char *)calloc(1, sizeof(char) * tcpMsg->msgLength); memcpy(tcpMsg->msg, buffer, tcpMsg->msgLength); send_message(tcpMsg); free(tcpMsg->msg); tcpMsg->msg = NULL; free(tcpMsg); tcpMsg = NULL; } int main() { tlv_test(); return 0; }
- 实现构建TLV消息代码
三、运行结果
-
运行结果
如下图所示,网络调试助手接收到发过来的TCP消息:
-
数据分析
我们对网络调试助手接收到的16进制消息转字符串进行分析:### 消息头部 ### 00 00 00 39 # 消息长度 57字节 00 02 # 消息版本号 02 00 02 # 消息类型(发送) 02 00 00 00 01 # 消息序列号 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 消息校验,保留 ### 消息体 (1) ### 03 FE # tag 1022 00 04 # length 4 00 00 00 64 # value 100 ### 消息体 (2) ### 03 FF # tag 1023 00 09 # length 9 48 65 6C 6C 6F 20 54 4C 56 # value Hello TLV ### 消息体 (3) ### 04 00 # tag 1024 00 04 # length 4 74 65 73 74 # value test
由此可见,接收和发送的消息是一致的,且按照
TLV头部+TLV消息体(1)+TLV消息体(2)+TLV消息体(3)
格式。
四、注意事项
- TLV在网络传输中需要进行网络大小端转换,即对应的数据要进行
htonl()/htons()
转换。上面给出的buid_tlv_msg()
函数内部已经做过处理,用户无需关心。 build_tlv.h
中struc TLV_BODY{}
结构体tlvBodyValueLength
、tlvBodyValue
并未在代码中引用。这样做的目的在上面的代码示例中有详细解释。- 用户使用此份Demo只需要按照
main.c
中tlv_test()
函数的组建TLV方法使用即可。