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);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM