zoukankan      html  css  js  c++  java
  • 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;
    

    基本定义

    /// <summary>消息类型</summary>
    typedef enum
    {
    	/// <summary></summary>
    	Ping = 0x01,
    	/// <summary>注册</summary>
    	Reg = 0x02,
    	/// <summary>登录</summary>
    	Login = 0x03,
    }MsgType_e;
    
    // 数据包头
    static byte PktHead[] = {0x0A,0x0A};
    
    // 函数原型
    /// <summary>创建消息</summary>
    /// <param name="seq">消息序列号Seq</param>
    /// <param name="payload">负载数据内容指针</param>
    /// <param name="payloadlen">负载数据长度</param>
    /// <param name="data">消息输出缓冲区</param>
    /// <param name="len">缓冲区长度</param>
    /// <returns>返回消息真实长度</returns>
    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)
    
    /// <summary>固定位置的数据部分</summary>
    typedef struct
    {
    	/// <summary>包头</summary>
    	ushort PktHead;
    	/// <summary>长度</summary>
    	byte Length;
    	/// <summary>消息类型,enum长度不确定,所以写个基础类型</summary>
    	byte Type;
    	/// <summary>消息序列号</summary>
    	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"
    
    /// <summary>同服务器打招呼,携带软硬件版本和产品唯一ID</summary>
    /// <param name="seq">消息序列号</param>
    /// <param name="data">消息输出缓冲区</param>
    /// <param name="len">缓冲区长度</param>
    /// <returns>返回消息真实长度</returns>
    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);
    }
    
    
  • 相关阅读:
    浮动广告
    jQuery给table添加行和删除行
    oracle优化方式和sql标准
    使用JavaScript中的ActiveXObject填充并设置Excel格
    打印相关的js
    利用js导出Excel
    Oracle左外连接和右外连接的写法
    天气预报抓取的方法和源代码(包括从IP获取)
    algorithm
    ungetc
  • 原文地址:https://www.cnblogs.com/JiuHuan/p/15059199.html
Copyright © 2011-2022 走看看