C语言实现TLV消息组成

3 篇文章 2 订阅
1 篇文章 0 订阅

一、前言

在网络数据传输过程中,TLV格式的消息比较常见。那么什么是TLV格式?
一个完整的TLV消息是由 消息头部+消息体(1)+消息体(2)+消息体(n) 组成。
如下图所示:
一个完整的TLV消息

  1. TLV消息头
    根据上图所示,TLV的消息头是由28个字节组成,且长度固定不变。
    length:消息的总长度,即TLV消息头长度 + TLV消息体长度。(4字节)
    version:消息的版本号,目前填2,且在使用中一般是固定不变的。(2字节)
    commandId:消息的命令类型,接受消息或者发送消息,根据需要填写。(2字节)
    seqence:消息序列号,范围在0x01 ~ 0x7FFFFFFF之间。(4字节)
    checkNumber:消息校验值,一般保留不使用。(16字节)
  2. 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
需要发送的数据如下:
需要发送的数据

  1. 实现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;
    }
    
    
    1. 实现构建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;
    }
    
    1. 实现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;
    }
    
    

三、运行结果

  1. 运行结果
    如下图所示,网络调试助手接收到发过来的TCP消息:
    网络调试助手接收的数据

  2. 数据分析
    我们对网络调试助手接收到的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)格式。

四、注意事项

  1. TLV在网络传输中需要进行网络大小端转换,即对应的数据要进行htonl()/htons()转换。上面给出的buid_tlv_msg()函数内部已经做过处理,用户无需关心。
  2. build_tlv.hstruc TLV_BODY{}结构体tlvBodyValueLengthtlvBodyValue并未在代码中引用。这样做的目的在上面的代码示例中有详细解释。
  3. 用户使用此份Demo只需要按照main.ctlv_test()函数的组建TLV方法使用即可。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值