浅谈自定义通讯协议——TLV(封装)

最近在写一个温度实时监控的项目,要用到TLV通讯协议,看了很多博客,慢慢的从里面明白了TLV的实现方式及用c语言实现TLV的编码,下面我将浅谈一下TLV,在讲TLV之前我们先讲一下什么是通讯协议。

通讯协议

1、通讯协议

协议可以使双方不需要了解对方的实现细节的情况下进行通信,因此双方可以是异构的,server可以是c++,client可以是java,基于相同的协议,我们可以用自己熟识的语言工具来实现。

协议一般由一个或多个消息组成,简单的来说,消息就像是一个Table,由表头(消息的字段定义,包括名称与数据类型)与行(字段值)组成。

2、自定义通讯协议

约定好双方交换数据的编解码方式,包括一致的基本数据类型,业务类型,字节序、消息内容等。

什么是自定义通讯协议TLV

TLV协议是BER编码的一种,全称是Tag、length、value。该协议简单高效,能适用于各种通信场景,且具有良好的可扩展性。TLV协议的基本格式如下:
在这里插入图片描述
其中,Tag,是报文的唯一标识;Length,表示Value字段的长度;Value字段的数据是需要传输的数据,长度由Length字段表示。

只用TLV可能会出现问题

我们该如何理解TLV那,通信的双方一定要有一种规约,收到的数据该如何解析,比如我们在应用层A用户给B用户发数据:
A------- 数据----------->B
数据在收发时,一定会存在三个非常重要的信息:
1、这是什么数据? Tag
2、数据有多少个字节? Length
3、数据的内容是什么? Value
B收到A发来的信息,就开始按照这三个开始解析,是不是A发来的消息。但是只有TLV封装的话,A给B发送过程中,会出现许多差错,比如:

  • 问题1:数据可能重合!如果你设置0x01为温度,那么后面有个0x01可能是表示数据的,而不是温度!
    这时候我们需要加一个报头,固定为0xFD,用来标志一个报文的开始

  • 问题2:数据可能会跳变!你不能保证数据传输过程中数据的跳变,0x01表示温度,跳变为0x02就表示另外的东西了!

这时候我们需要在一个字节流末尾加一个CRC校验,传输前算一下字节流的大小,传输到另外一个端口后再算一下,对比前后两个CRC的值,如果相同,表示没有发生字节跳变,如果不同,那就舍弃!

  • 问题3:报头可能在TLV中!

    同样也要加CRC校验

加工后的TLV:
在这里插入图片描述

如果还有不清楚为什么要加报文头和CRC校验的同学,可以参考这篇博客https://blog.csdn.net/qq_43296898/article/details/88863854

为接下来我要完成网络socket实现温度上报,要用到自定义通讯协议TLV,我讲用TLV的格式将“ID/时间/温度“上报,如“RPI0001/2019-01-05 11:40:30/30.0C”; 根据上面介绍的我将把这个数据封装成TLV格式上报。这里面有三种数据要发送,所以就要写三个TLV报文。

后来因为加入了重传机制,所以要加入了ACK的报文,所以在c文件中我写了四个TLV报文

TLV的封包代码示例

下面是我用c代码实现的TLV报文格式的发送信息:

#include <stdio.h>
#include <string.h>
#include "header.h"
#include "tlv.crc-itu-t.h"


#define HEAD  0xFD   //定义一个报文头
#define BUF_SIZE  128   //定义一个buffer的大小
#define TLV_FIXED_SIZE  5  //固定TLV字节流大小,不包含value的值
#define TLV_MINI_SIZE    (TLV_FIXED_SIZE+1)  //TLV字节流的最小值,value的最小值为1个字节
 
enum   //使用枚举  Tag的值会自加
{
	TAG_ACK = 1,
	TAG_SN,    //树莓派上的id
  	TAG_TEMP,   //温度
	TAG_TIME,   //时间

 }int pack_ack (char *buf , int size);  //声明ack封装函数
int pack_sn  (char *buf , int size);//声明id封装函数
int pack_temp (char *buf , int size);//声明温度封装函数
int pack_time  (char *buf , int size);//声明时间温度封装函数

int  main (int argc, char **argv)
{
	char buf[BUF_SIZE];
	int   bytes;  //一个封装函数的字节流的个数

	bytes =  pack_ack (buf , sizeof(buf));
	dump_buf("ack", buf, bytes);//设置dump_buf函数,把所有的字节流都放到dump_buf里面
	
	bytes =  pack_sn(buf , sizeof(buf));
	dump_buf("sn", buf, bytes);

	bytes =  pack_temp(buf , sizeof(buf));
	dump_buf("temp", buf, bytes);

	bytes =  pack_time (buf , sizeof(buf));
	dump_buf("time", buf, bytes);
	
	return 0;
}
/*ACK报文
 * H              T             L               Value    CRC1    CRC2
 * 0xFD          0x01         0x01              0x00      0x12    0x34
 * */

int pack_ack(char *buf, int size)
{
    unsigned short crc16=0; //CRC校验值
    int pack_len = 0;
    int data_len = 0;
    int ofset = 0;//buf的索引位置
    char *val="0";
    
    if(!buf || size < TLV_MINI_SIZE)
    {
        printf("Invalid input argument\n");
        return 0;
    }

    buf[ofset]=HEAD;  //将buf[0]设报文头0xFD
    ofset +=1;//索引加1

    buf[ofset]=TAG_ACK; //将buf[1]设为TAG_ACK=0x01
    ofset +=1;

    data_len = strlen(val); //ACK报文value的长度
    pack_len = data_len+TLV_FIXED_SIZE;// 封装TLV的总长度(length)
    buf[ofset]=pack_len;//将buf[2]设为TLV的总长度
    ofset  += 1;

    memcpy(&buf[ofset], val, data_len);//把ACK的内容拷贝到buf里,按字节拷贝
    ofset +=data_len; //索引加到data_len个字节

    crc16 = crc_itu_t(MAGIC_CRC, buf, ofset);//调用crc函数,此函数在crc头文件中
    ushort_to_bytes(&buf[ofset], crc16);
    ofset +=2;

    return ofset;//返回索引值
}
/*树莓派的编号ID=RPI0001报文
 * H      T             L               Value    CRC1    CRC2
 * 0xFD  0x01         0x01              0x07      0x12    0x34
 * */

int pack_sn(char *buf, int size)//声明id封装函数
{
    unsigned short crc16 = 0;
    char           *id = "RPI0001";
    int     ofset = 0;
    int         pack_len = 0;
    int     data_len = 0;

    if(!buf || size < TLV_MINI_SIZE)
    {
        printf("Invalid input argument \n");
        return 0;
    }

    buf[ofset]=HEAD;
    ofset+=1;

    buf[ofset]=TAG_SN;
    ofset +=1;

    data_len=strlen(id);
    pack_len = data_len + TLV_FIXED_SIZE;
    buf[ofset]=pack_len;
    ofset +=1;

    memcpy(&buf[ofset], id, data_len);
    ofset += data_len;

    crc16=crc_itu_t(MAGIC_CRC, buf, ofset);
    ushort_to_bytes(&buf[ofset], crc16);
    ofset +=2;

    return ofset; //返回索引值
}

/* 树莓派上获取温度30.5C报文
 * H                  T            L              Value           CRC1    CRC2
 * 0xFD              0x03         0x00         0x14 0x3A           0x12    0x34
*/

int  pack_temp(char *buf, int size)
{
    unsigned short crc16=0;
    int     pack_len = 0;
    int     data_len = 0;
    int     ofset = 0;
    float   temp;
    char    datatime[32];
    int     i,j;

    if(!buf || size < TLV_MINI_SIZE)
     {

        printf("Invalid input adgument\n");
        return 0;

    }
    buf[ofset]=HEAD;
    ofset +=1;

    buf[ofset]=TAG_TEMP;
    ofset +=1;

    ds18b20_get_temperature(&temp);
    i=(int)temp; //整数部分
    j=(int) ((temp-i) *100);//小数部分

    data_len = sizeof(temp);
    pack_len= data_len + TLV_FIXED_SIZE;
    buf[ofset]=pack_len;
    ofset +=1;

    buf[ofset]=i;
    ofset+=1;
    buf[ofset]=j;
    ofset+=1;

    crc_itu_t(MAGIC_CRC, buf, ofset);
    ushort_to_bytes(&buf[ofset], crc16);
 ushort_to_bytes(&buf[ofset], crc16);
    ofset +=2;
    return ofset;//返回索引值
}
/* 获取系统时间的报文 2020-4-12 23:22:00
 * H                  T             L                   Value                                  
CRC1    CRC2
 * 0xFD  0x04         0x06          0x14    0x04 0x0c 0x17 0x16 0x00              0x12    0x34
 * */

int pack_time (char *buf, int size)
{
    unsigned short crc16=0;
    int  ofset = 0;
    int  pack_len = 0;
    int  data_len = 0;
    char  datatime[32];
    char *datime;
    if( !buf || size < TLV_MINI_SIZE)
    {
        printf("Invald input argument\n");
        return 0;
    }

    buf[ofset]=HEAD;
    ofset+=1;

    buf[ofset]=TAG_TIME;
    ofset +=1;
    memset(datatime, 0, sizeof(datatime));
    int time=get_sys_time(datatime);
    data_len = time;
    pack_len =data_len + TLV_MINI_SIZE;
    buf[ofset]=pack_len;
    ofset+=1;

    for(int i=0; i< data_len; i++, ofset++)
        buf[data_len]=datatime[i];

    crc16=crc_itu_t(MAGIC_CRC,buf, ofset);
    ushort_to_bytes(&buf[ofset], crc16);
    ofset +=2;
    return ofset;//返回索引值

}

void dump_buf(char *type, char *data, int len)//data指针,指向buf的首地址,len是buf的长度
{
    if(type)
    {
        printf("%s:\n", type);
    }

    int i;
    for(i=0; i<len; i++)
    {
        printf("0x%02x\n",data[i]);
             printf("0x%02x\n",data[i]);
    }
    printf("\n");

}

后续我将会更新TLV的解包过程!代码可能存在bug,如果有什么问题,还请大家指出!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章