需求背景
CPU 与智能网卡(DPU)交互时,需要把表项数据按协议序列化成字节流。由于 DPU 按 bit 粒度解析数据,协议对布局通常有明确约束。
- 传输的数据必须是字节流
- DPU 逐 bit 读取,字段需要紧密排列
- 协议规定比特位按从高到低(MSB-first)排列
说明:CPU 主机序为小端序。但这里的协议是按 bit 流(MSB-first)定义的,不等价于“把结构体内存布局直接拷贝出去”。
带位域结构体的序列化
手动序列化:先推导出标准答案
先用一个最小例子把协议格式算清楚,后面所有实现都以这份“标准答案”做对照。
结构体定义如下。
struct Vrf {
BYTE hitFlag : 1;
BYTE pad1 : 7;
WORD vrfId : 12;
WORD pad2 : 4;
DWORD rsv;
};
以 hitFlag = 1、vrfId = 100 为例。
hitFlag只占 1 bit,写到第一个字节最高位,因此第一个字节开头是1000 0000(0x80)vrfId占 12 bit,100 的二进制是0000 0110 0100,紧接着写入 bit 流- 其余位域填 0,
rsv也填 0
#include <bits/stdc++.h>
using namespace std;
using BYTE = unsigned char;
using WORD = unsigned short int;
using DWORD = unsigned int;
using U64 = unsigned long long;
struct Vrf {
BYTE hitFlag : 1;
BYTE pad1 : 7;
WORD vrfId : 12;
WORD pad2 : 4;
DWORD rsv;
Vrf() = default;
explicit Vrf(const std::vector<BYTE>& config) {
hitFlag = config[0] >> 7;
pad1 = 0;
vrfId = (config[1] << 4) | (config[2] >> 4);
pad2 = 0;
rsv = 0;
}
explicit Vrf(WORD vrfId)
: hitFlag(1), pad1(0), vrfId(vrfId), pad2(0), rsv(0) {};
std::vector<BYTE> serialize() const {
std::vector<BYTE> buf(sizeof(Vrf));
buf[0] = (hitFlag << 7) | pad1;
buf[1] = static_cast<BYTE>(vrfId >> 4);
buf[2] = static_cast<BYTE>(((vrfId & 0x0F) << 4) | pad2);
buf[3] = static_cast<BYTE>((rsv >> 24) & 0xFF); // rsv 最高字节
buf[4] = static_cast<BYTE>((rsv >> 16) & 0xFF);
buf[5] = static_cast<BYTE>((rsv >> 8) & 0xFF);
buf[6] = static_cast<BYTE>(rsv & 0xFF);
return buf;
}
std::vector<BYTE> serializeByMemcpy() const {
std::vector<BYTE> buf(sizeof(Vrf));
memcpy(buf.data(), this, sizeof(Vrf));
return buf;
}
std::string toString() const {
return "hitFlag:" + std::to_string(hitFlag) +
", vrfId:" + std::to_string(vrfId);
}
} __attribute__((packed));
void dumpVector(const std::vector<BYTE>& data, size_t dumpCount) {
cout << hex << uppercase;
for (size_t i = 0; i < data.size(); ++i) {
cout << setw(2) << setfill('0') << static_cast<int>(data[i]) << " ";
}
cout << dec << endl;
}
int main() {
Vrf vrf(100);
cout << vrf.toString() << endl;
auto config = vrf.serialize();
cout << "Serialized: " << endl;
dumpVector(config);
auto config2 = vrf.serializeByMemcpy();
cout << "SerializedByMemcpy: " << endl;
dumpVector(config2);
return 0;
}
serialize() 输出如下:80 06 40 00 00 00 00。
内存布局拷贝测试
这里用 memcpy 直接展示结构体在内存中的布局,作为对比。
std::vector<BYTE> serializeByMemcpy() const {
std::vector<BYTE> buf(sizeof(Vrf));
memcpy(buf.data(), this, sizeof(Vrf));
return buf;
}
-
测试结果如下:
-
预期目标:
80 06 40 00 00 00 00 -
serializeByMemcpy()返回:01 64 00 00 00 00 00 -
原因分析:
BYTE hitFlag : 1:位域在内存中的具体落位由编译器/ABI 决定,常见实现会从低 bit 开始放,导致内存里看到 0000 0001(0x01)。
WORD vrfId : 12:GCC 常见实现会先分配一个 16-bit 容器;由于主机序为小端序,两个字节在内存里表现为 64 00。这与协议要求的 MSB-first bit 流顺序不是一回事。
优化一:定义工具类代替手动序列化
每次都手写“位拼装”容易出错,也不便维护。一个更可复用的做法是抽象出 BitWriter:只关心“写多少 bit、按 MSB-first 写到哪里”。
-
接口定义:writeBits(value, n),其中value表示要序列化的值,n表示这个值要占几位
-
功能定义:先序列化的位,放在前面的字节,一个字节内,也是从高到低位的顺序来存储
-
算法描述:自动根据已序列化的bit数量,计算待填充的字节,每个字节内部又从高到低存储比特位
class BitWriter {
public:
BitWriter() : bitPos_(0) {}
// 写入 value 的低 bitCount 位(bitCount 最大 64)。
void writeBits(U64 value, BYTE bitCount) {
if (bitCount == 0) {
return;
}
if (bitCount > 64) {
throw invalid_argument("bitCount must be <= 64");
}
value &= maskLowerBits(bitCount); //去掉高位脏数据
//从高到低写入比特位
for (int i = bitCount - 1; i >= 0; --i) {
appendOneBit(static_cast<BYTE>((value >> i) & 1ULL));
}
}
void writeZeroBits(DWORD bitCount) {
for (DWORD i = 0; i < bitCount; ++i) {
appendOneBit(0);
}
}
const vector<BYTE>& bytes() const {
return data_;
}
size_t bitSize() const {
return bitPos_;
}
private:
static U64 maskLowerBits(BYTE bitCount) {
if (bitCount == 64) {
return ~0ULL;
}
return (1ULL << bitCount) - 1ULL;
}
void appendOneBit(BYTE bit) {
//计算比特位所属的字节
size_t byteIndex = bitPos_ / 8;
BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);
if (byteIndex >= data_.size()) {
data_.push_back(0);
}
BYTE shift = static_cast<BYTE>(7 - bitInByte);
data_[byteIndex] |= static_cast<BYTE>(bit << shift);
++bitPos_;
}
private:
vector<BYTE> data_;
size_t bitPos_;
};
那么在序列化的时候就可以简单调用writeBits接口即可
std::vector<BYTE> serializeByBitWriter() const {
BitWriter w;
w.writeBits(static_cast<U64>(hitFlag), 1);
w.writeBits(static_cast<U64>(pad1), 7);
w.writeBits(static_cast<U64>(vrfId), 12);
w.writeBits(static_cast<U64>(pad2), 4);
w.writeBits(static_cast<U64>(rsv), 32);
//或者w.writeZeroBits(32);
return w.bytes();
}
结果与手动序列化结果一致
优化二:只维护一处结构体位域信息
- 痛点:结构体里已经写过“字段 + 位宽”,序列化又写一遍;字段变更时容易漏改,维护成本高
- 难点:C++ 缺少反射,运行期拿不到字段列表与位宽
- 方案:使用 X Macro,把“字段列表”集中维护一处,然后派生出位域声明、序列化与反序列化代码
#define VRF_FIELD_LIST(X) \
X(BYTE, hitFlag, 1) \
X(BYTE, pad1, 7) \
X(WORD, vrfId, 12) \
X(WORD, pad2, 4) \
X(DWORD, rsv, 32)
struct Vrf {
#define VRF_DECLARE_BITFIELD(type, name, bits) type name : bits;
VRF_FIELD_LIST(VRF_DECLARE_BITFIELD)
#undef VRF_DECLARE_BITFIELD
Vrf() = default;
explicit Vrf(const std::vector<BYTE>& config) {
hitFlag = config[0] >> 7;
pad1 = 0;
vrfId = (config[1] << 4) | (config[2] >> 4);
pad2 = 0;
rsv = 0;
}
explicit Vrf(WORD vrfId)
: hitFlag(1), pad1(0), vrfId(vrfId), pad2(0), rsv(0) {};
std::vector<BYTE> serialize() const {
BitWriter w;
#define VRF_WRITE_FIELD(type, name, bits) w.writeBits(static_cast<U64>(name), bits);
VRF_FIELD_LIST(VRF_WRITE_FIELD)
#undef VRF_WRITE_FIELD
return w.bytes();
}
std::string toString() const {
return "hitFlag:" + std::to_string(hitFlag) +
", vrfId:" + std::to_string(vrfId);
}
} __attribute__((packed));
带位域结构体的反序列化
BitReader
延续序列化思路,实现一个 BitReader 按 MSB-first 从字节流读出 bit,并按位宽拼回字段值。
class BitReader {
public:
explicit BitReader(const vector<BYTE>& data) : data_(data), bitPos_(0) {}
U64 readBits(BYTE bitCount) {
if (bitCount == 0) {
return 0;
}
if (bitCount > 64) {
throw invalid_argument("bitCount must be <= 64");
}
U64 value = 0;
for (BYTE i = 0; i < bitCount; ++i) {
value = (value << 1) | readOneBit();
}
return value;
}
private:
BYTE readOneBit() {
size_t byteIndex = bitPos_ / 8;
BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);
++bitPos_;
if (byteIndex >= data_.size()) {
return 0;
}
BYTE shift = static_cast<BYTE>(7 - bitInByte);
return static_cast<BYTE>((data_[byteIndex] >> shift) & 1U);
}
private:
const vector<BYTE>& data_;
size_t bitPos_;
};
宏驱动修改反序列化
explicit Vrf(const std::vector<BYTE>& config) {
BitReader r(config);
#define VRF_READ_FIELD(type, name, bits) name = static_cast<type>(r.readBits(bits));
VRF_FIELD_LIST(VRF_READ_FIELD)
#undef VRF_READ_FIELD
}
最终优化版本及测试代码
下面给出整合版代码与最小测试,目标是:序列化字节序列与“标准答案”一致,且反序列化能恢复关键字段。
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
using namespace std;
// 自定义类型定义
using BYTE = unsigned char;
using WORD = unsigned short int;
using DWORD = unsigned int;
using U64 = unsigned long long;
// MSB-first 位流:先写入的位占据缓冲区中更早的字节、更高位(与常见网络报文一致)。
class BitWriter {
public:
BitWriter() : bitPos_(0) {}
// 写入 value 的低 bitCount 位(bitCount 最大 64)。
void writeBits(U64 value, BYTE bitCount) {
if (bitCount == 0) {
return;
}
if (bitCount > 64) {
throw invalid_argument("bitCount must be <= 64");
}
value &= maskLowerBits(bitCount); //去掉高位脏数据
//从高到低写入比特位
for (int i = bitCount - 1; i >= 0; --i) {
appendOneBit(static_cast<BYTE>((value >> i) & 1ULL));
}
}
void writeZeroBits(DWORD bitCount) {
for (DWORD i = 0; i < bitCount; ++i) {
appendOneBit(0);
}
}
const vector<BYTE>& bytes() const {
return data_;
}
size_t bitSize() const {
return bitPos_;
}
private:
static U64 maskLowerBits(BYTE bitCount) {
if (bitCount == 64) {
return ~0ULL;
}
return (1ULL << bitCount) - 1ULL;
}
void appendOneBit(BYTE bit) {
//计算比特位所属的字节
size_t byteIndex = bitPos_ / 8;
BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);
if (byteIndex >= data_.size()) {
data_.push_back(0);
}
BYTE shift = static_cast<BYTE>(7 - bitInByte);
data_[byteIndex] |= static_cast<BYTE>(bit << shift);
++bitPos_;
}
private:
vector<BYTE> data_;
size_t bitPos_;
};
class BitReader {
public:
explicit BitReader(const vector<BYTE>& data) : data_(data), bitPos_(0) {}
U64 readBits(BYTE bitCount) {
if (bitCount == 0) {
return 0;
}
if (bitCount > 64) {
throw invalid_argument("bitCount must be <= 64");
}
U64 value = 0;
for (BYTE i = 0; i < bitCount; ++i) {
value = (value << 1) | readOneBit();
}
return value;
}
private:
BYTE readOneBit() {
size_t byteIndex = bitPos_ / 8;
BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);
++bitPos_;
if (byteIndex >= data_.size()) {
return 0;
}
BYTE shift = static_cast<BYTE>(7 - bitInByte);
return static_cast<BYTE>((data_[byteIndex] >> shift) & 1U);
}
private:
const vector<BYTE>& data_;
size_t bitPos_;
};
#define VRF_FIELD_LIST(X) \
X(BYTE, hitFlag, 1) \
X(BYTE, pad1, 7) \
X(WORD, vrfId, 12) \
X(WORD, pad2, 4) \
X(DWORD, rsv, 32)
struct Vrf {
#define VRF_DECLARE_BITFIELD(type, name, bits) type name : bits;
VRF_FIELD_LIST(VRF_DECLARE_BITFIELD)
#undef VRF_DECLARE_BITFIELD
Vrf() = default;
explicit Vrf(const std::vector<BYTE>& config) {
BitReader r(config);
#define VRF_READ_FIELD(type, name, bits) name = static_cast<type>(r.readBits(bits));
VRF_FIELD_LIST(VRF_READ_FIELD)
#undef VRF_READ_FIELD
}
explicit Vrf(WORD vrfId)
: hitFlag(1), pad1(0), vrfId(vrfId), pad2(0), rsv(0) {};
std::vector<BYTE> serialize() const {
BitWriter w;
#define VRF_WRITE_FIELD(type, name, bits) w.writeBits(static_cast<U64>(name), bits);
VRF_FIELD_LIST(VRF_WRITE_FIELD)
#undef VRF_WRITE_FIELD
return w.bytes();
}
std::string toString() const {
return "hitFlag:" + std::to_string(hitFlag) +
", vrfId:" + std::to_string(vrfId);
}
} __attribute__((packed));
void dumpVector(const std::vector<BYTE>& data) {
cout << hex << uppercase;
for (size_t i = 0; i < data.size(); ++i) {
cout << setw(2) << setfill('0') << static_cast<int>(data[i]) << " ";
}
cout << dec << endl;
}
int main() {
Vrf vrf(100);
cout << "Origin: " << vrf.toString() << endl;
auto config = vrf.serialize();
//cout << "Serialized: " << endl;
//dumpVector(config);
const vector<BYTE> expected = {0x80, 0x06, 0x40, 0x00, 0x00, 0x00, 0x00};
const bool serializeOk = (config == expected);
cout << "Serialize bytes test: " << (serializeOk ? "PASS" : "FAIL") << endl;
cout << " actual : ";
dumpVector(config);
cout << " expected: ";
dumpVector(expected);
Vrf decoded(config);
cout << "Decoded: " << decoded.toString() << endl;
const bool deserializeOk = (decoded.hitFlag == vrf.hitFlag) &&
(decoded.vrfId == vrf.vrfId);
cout << "Deserialize test: " << (deserializeOk ? "PASS" : "FAIL") << endl;
return 0;
}
可复用的工程经验
- 协议是协议,内存是内存:位域布局不受标准保证,
memcpy只能拿到内存表示,不能当作协议序列化 - 先手算一个标准答案:有了可对照的期望字节序列,后续任何实现(手写、工具类、宏驱动)都能用单测快速验证
- 把“bit 流写入/读取”抽成工具:
BitWriter/BitReader只做一件事:维护 bit 游标,按 MSB-first 写/读 - 把字段列表当作单一事实源:用 X Macro 集中维护
type/name/bits,再派生声明、序列化、反序列化,减少重复与漏改
8590

被折叠的 条评论
为什么被折叠?



