c語言數據拼包

單片機數據拼包

對於數據包拼包方式常規方式有

  • 數組
  • 指針
  • 結構體

下文將此三種方式分別列舉此數據包的實現。
然後對比優缺點。

本文舉例數據包協議

包頭 長度Length 消息類型 消息序列號Seq 負載數據 校驗
2位元組 1位元組 1位元組 1位元組 N位元組 2位元組
名稱 描述 其他
包頭 固定 0X0A,0X0A 對於乙太網數據包可以不設立此段。串口一般需要使用,對解包有利,這裡不贅述。
長度 Length 數據包長度,(除去包頭和自身)
消息類型 低7bit是消息類型,最高bit標記是否是回復消息
消息序列號Seq 消息編號,用於回復消息與請求消息的匹配
負載數據 消息類型對應的負載數據 負載數據長度 = Length – 4
校驗 前面所有位元組的校驗值

程式碼中使用類型如下定義

// //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靈活度。只能抽取相同部分做部分處理。非常頭禿。
    (本文主體是寫數據,詳細討論)

數據流

// //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 還定義了一些帶擴容的方法。可以在外部不傳入緩衝的情況下完成數據包構建。
由於內部使用了堆,所以需要手動釋放記憶體。
自帶擴容的方式,屬於另一種使用方式了,這裡不做對比。

對比總結

以下評判為個人經驗判斷,歡迎討論。

執行速度:指針>結構體>數組>流
技術難度:指針>結構體>數組>流
寫錯可能性:指針>數組>結構體>流
易讀性:結構體>流>數組>指針