c语言数据拼包

c语言数据拼包

单片机数据拼包

对于数据包拼包方式常规方式有:

数组

指针

结构体

下文将此三种方式分别列举此数据包的实现。

然后对比优缺点。

本文举例数据包协议:

包头

长度Length

消息类型

消息序列号Seq

负载数据

校验

2字节

1字节

1字节

1字节

N字节

2字节

名称

描述

其他

包头

固定 0X0A,0X0A

对于以太网数据包可以不设立此段。串口一般需要使用,对解包有利,这里不赘述。

长度 Length

数据包长度,(除去包头和自身)

消息类型

-

低7bit是消息类型,最高bit标记是否是回复消息

消息序列号Seq

消息编号,用于回复消息与请求消息的匹配

负载数据

消息类型对应的负载数据

负载数据长度 = Length - 4

校验

前面所有字节的校验值

代码中使用类型如下定义:

// https://github.com/NewLifeX/microCLib.git Core 目录 Type.h 内定义。

typedef char sbyte;

typedef unsigned char byte;

typedef unsigned short ushort;

typedef unsigned int uint;

typedef long long int int64;

typedef unsigned long long int uint64;

基本定义:

///

消息类型

typedef enum

{

///

Ping = 0x01,

///

注册

Reg = 0x02,

///

登录

Login = 0x03,

}MsgType_e;

// 数据包头

static byte PktHead[] = {0x0A,0x0A};

// 函数原型

///

创建消息

/// 消息序列号Seq

/// 负载数据内容指针

/// 负载数据长度

/// 消息输出缓冲区

/// 缓冲区长度

/// 返回消息真实长度

int Buil(byte seq, byte* payload, int payloadlen, byte* data, int len);

// 下列代码,会根据实现方式在函数名加尾缀 ByXXX

数组

int BuilByteArray(byte seq, byte* payload, int payloadlen, byte* data, int len)

{

if (data == NULL)return -1;

// 判断缓冲区长度是否足够

if (len < payloadlen + 4 + 3)return -1;

// 用于记录长度/写入位置

int idx = 0;

// 写数据包头

// memcpy(&data[idx], PktHead, sizeof(PktHead)); // idx=0 可以直接写data

memcpy(data, PktHead, sizeof(PktHead));

idx += sizeof(PktHead);

// 长度

data[idx++] = payloadlen + 4;

// 类型

data[idx++] = (byte)Reg;

// 序列号

data[idx++] = seq;

// 负载

memcpy(&data[idx], payload, payloadlen);

idx += payloadlen;

// 计算crc

ushort crc = CaclcCRC16(data, idx);

// 写入crc

memcpy(&data[idx], (byte*)&crc, sizeof(crc));

idx += sizeof(crc);

return idx;

}

常规操作,在各种c项目中最为常见。

容易出错的点在 idx 维护。

基本无难度。

阅读难度很高,如果不写好备注。基本头秃。

指针

int BuilByPoint(MsgType_e type, byte seq, byte* payload, int payloadlen, byte* data, int len)

{

if (data == NULL)return -1;

// 判断缓冲区长度是否足够

if (len < payloadlen + 4 + 3)return -1;

byte* p = data;

// 写数据包头

// memcpy(&data[idx], PktHead, sizeof(PktHead)); // idx=0 可以直接写data

memcpy(p, PktHead, sizeof(PktHead));

p += sizeof(PktHead);

// 长度

*p++ = payloadlen + 4;

// 类型

*p++ = (byte)type;

// 序列号

*p++ = seq;

// 负载

memcpy(p, payload, payloadlen);

p += payloadlen;

// 计算crc

ushort crc = CaclcCRC16(data, p - data);

// 写入crc

memcpy(p, (byte*)&crc, sizeof(crc));

p += sizeof(crc);

return p - data;

}

基本就是数组方式的翻版。

在执行效率上优于数组方式。

指针对于 c 来说一直都是难点。

容易写出错。

阅读难度非常高,如果不写好备注。基本头秃。

结构体

// 压栈编译器配置

#pragma pack(push)

// 告诉编译按照1字节对齐排布内存。

#pragma pack(1)

///

固定位置的数据部分

typedef struct

{

///

包头

ushort PktHead;

///

长度

byte Length;

///

消息类型,enum长度不确定,所以写个基础类型

byte Type;

///

消息序列号

byte Seq;

}MsgBase_t;

// 恢复编译器配置(弹栈)

#pragma pack(pop)

int BuilByStruct(MsgType_e type, byte seq, byte* payload, int payloadlen, byte* data, int len)

{

if (data == NULL)return -1;

// 判断缓冲区长度是否足够

if (len < payloadlen + 4 + 3)return -1;

// 直接写入能描述的部分。

MsgBase_t* mb = (MsgBase_t*)data;

memcpy((byte*)&(mb->PktHead), PktHead, sizeof(PktHead));

mb->Length = payloadlen + 4;

mb->Type = (byte)type;

mb->Seq = seq;

int idx = sizeof(MsgBase_t);

// 负载

memcpy(&data[idx], payload, payloadlen);

idx += payloadlen;

// 计算crc

ushort crc = CaclcCRC16(data, idx);

// 写入crc

memcpy(&data[idx], (byte*)&crc, sizeof(crc));

idx += sizeof(crc);

return idx;

}

很少出现在各种开源软件中。

需要掌握一个高级知识点,涉及编译器和 cpu 特征。

cpu位宽、非对齐访问以及对应的编译器知识。

对于固定长度的指令来说,非常方便。

cpu执行效率非常高,跟数组方式的速度一致。

写好结构体,数值填充顺序就跟协议内容无关了。

很好理解,阅读无压力。

对于读非固定格式数据来说,0灵活度。只能抽取相同部分做部分处理。非常头秃。

(本文主体是写数据,详细讨论)

数据流

// https://github.com/NewLifeX/microCLib.git

#include "Stream.h"

int BuildByStream(MsgType_e type, byte seq, byte* payload, int payloadlen, byte* data, int len)

{

if (data == NULL)return -1;

// 判断缓冲区长度是否足够

if (len < payloadlen + 4 + 3)return -1;

// 初始化流

Stream_t st;

StreamInit(&st, data, len);

// 包头

StreamWriteBytes(&st, PktHead, sizeof(PktHead));

// 长度

StreamWriteByte(&st, payloadlen + 4);

// 类型

StreamWriteByte(&st, (byte)type);

// 序列号

StreamWriteByte(&st, seq);

// 负载

StreamWriteBytes(&st, payload, payloadlen);

// 计算crc

ushort crc = CaclcCRC16(st.MemStart, st.Position);

// 写入crc

StreamWriteBytes(&st, (byte*)&crc, sizeof(crc));

return st.Position;

}

上位机处理常规方式。算是面对对象编程的范畴了。

阅读难度很小。

Stream 内部已做边界判断,基本不会出现bug。

缺点,效率低。每个操作都是函数调用,此处产生大量消耗。

Stream 还定义了一些带扩容的方法。可以在外部不传入缓冲的情况下完成数据包构建。

由于内部使用了堆,所以需要手动释放内存。

自带扩容的方式,属于另一种使用方式了,这里不做对比。

对比总结

以下评判为个人经验判断,欢迎讨论。

执行速度:指针>结构体>数组>流

技术难度:指针>结构体>数组>流

写错可能性:指针>数组>结构体>流

易读性:结构体>流>数组>指针

使用样例

// 为了减少折腾,我采用Stream方法写的。

// https://github.com/NewLifeX/microCLib.git

#include "Version.h"

#include "HardwareVersion.h"

#include "Cpu.h"

///

同服务器打招呼,携带软硬件版本和产品唯一ID

/// 消息序列号

/// 消息输出缓冲区

/// 缓冲区长度

/// 返回消息真实长度

int BuildPinMsg(byte seq, byte* data, int len)

{

// 固件版本。软件编译时间/发布工具生成的时间(特定格式)。

uint fwVer = GetVersion();

// 代码内写的时间转换而来。

// 一般用电路板画板时间作为基准。规避电路板命名的差异。

uint hdVer = GetHardwareVersion();

// CPU唯一ID

byte cpuid[12];

GetCpuid(cpuid, sizeof(cpuid));

byte payload[20];

Stream_t st;

StreamInit(&st, payload, sizeof(payload));

StreamWriteBytes(&st, (byte*)&fwVer, sizeof(fwVer));

StreamWriteBytes(&st, (byte*)&hdVer, sizeof(hdVer));

StreamWriteBytes(&st, (byte*)&cpuid, sizeof(cpuid));

return BuildByStream(Ping, seq, payload, st.Position, data, len);

}