通过网络发送结构
Sending structs over a network
在我们公司,通过网络发送 C/C++ 结构是很常见的。任何结构都有一个或多个 uint32_t 字段:
typedef struct {
uint32_t data1;
uint32_t data2;
} Data;
和字段(data1,data2,...)封装了我们需要发送的真实数据。我们的数据大部分是一位或几位长,因此为了保存 space/bandwidth 它在特定位位置的那些字段内对齐。
为了访问我们的真实数据,我们编写了(在结构之外!)'getters' 和 'setters' 具有位移和位掩码的宏:
#define READY_MASK 0x01 // indicates that READY is a single bit value
#define READY_OFFSET 3 // indicates position of READY inside 32-bit field
#define IS_READY(x) { ... } // returns READY value from x field
#define SET_READY(x,r) { ... } // sets READY value to x field
现在我想通过直接在结构中添加 getter 和 setter 来修改、简化并使这个过程更安全,例如:
typedef struct {
uint32_t data1;
#define READY_MASK 0x01
#define READY_OFFSET 3
inline void set_ready(uint32_t r) { /*...*/ }
inline uint32_t is_ready() { /*...*/ }
// lots of other getters and setters
} Data1;
就我的实验而言,我注意到这种修改不会影响结构的大小 sizeof(Data)==sizeof(Data1)
,我可以通过网络发送这个结构并在另一端。
我的问题是:这个修改有什么不对的地方吗?有什么风险或我应该注意的事情吗?
原则上不加虚函数就没有vptr,struct size应该和没有成员函数一样。但它不再是 POD 结构,因此您可能很容易陷入 UB 的模糊地带,即未定义行为。由于 C++(还)没有标准化的 ABI,因此通常最好使用 C 风格的 POD 结构,例如通过网络序列化。为 C++ modules/subsystems 制作精简、可移植的 API 也是如此,坚持使用 C ABI。
此外,如果您考虑这些(正如某些评论者所建议的那样),我建议您远离位域,手动位掩码通常更易于分析和移植。位域可以例如给你字节序问题,这至少部分是因为 struct 'always' 的第一个成员结束于 struct start 的较低内存地址,而不管字节顺序如何。位域的工作方式也可能取决于传输介质,例如BSD 网络流套接字是字节流,但许多 HW 接口复制整个 32 位字大小的块而不是字节。
您所做的修改不会造成任何伤害,但它也不会带来任何好处,因为无论如何结构成员都是 public。 Data和Data1大小相同的原因是因为你添加的是内联函数,不占用对象中的space而是用实际函数代码替换任何函数调用。
现在,如果您要通过网络发送结构和二进制数据,则必须考虑以下两个规则:
按照惯例,整数值是使用网络字节顺序发送的,它是 Big Endian 而不是 x86 的 Little Endian。如果您确定所有发送和接收数据的机器都是 x86,这不是问题。但是,如果任何机器具有不同的字节序,如大字节序或无字节序(或中字节序),你就会遇到问题。例如,您可以有一个 ARM 处理器、一个 SPARC 等。在 C 中,您有以下宏:
ntohs、htons、ntohl、htonl
您还必须考虑不同的内存对齐方式。结构的内存对齐取决于体系结构、编译器和编译模式。编译器可能会添加填充(他们不能在 C 中重新排序成员,但请阅读:Can a C++ compiler re-order elements in a struct)。此外,long 等类型在 32 位和 64 位体系结构中具有不同的大小。即使可以肯定只有两个 unsigned int 成员不会有问题,您也不应该将内存从结构复制到将在消息中发送的数据缓冲区,也不应该从消息中的原始数据复制内存到一个结构。基本上你不应该做这样的事情:
字符缓冲区[BUF_SIZE];
Data1 myData;
memcpy(buffer, myData, sizeof(Data));
https://en.wikipedia.org/wiki/Data_structure_alignment
你应该把成员一个一个地移到缓冲区。从消息中收到的原始数据填充结构时也是如此。
我还建议不要使用位域,阅读 Why bit endianness is an issue in bitfields?。但非常清楚,在您的代码片段中您没有使用位域。使用位域与使用整数中的位来表示某些特定状态不同。在这种情况下,你做得很好,应该只考虑具有不同字节顺序的体系结构中的整数表示(如果这是可能的情况)。
在我们公司,通过网络发送 C/C++ 结构是很常见的。任何结构都有一个或多个 uint32_t 字段:
typedef struct {
uint32_t data1;
uint32_t data2;
} Data;
和字段(data1,data2,...)封装了我们需要发送的真实数据。我们的数据大部分是一位或几位长,因此为了保存 space/bandwidth 它在特定位位置的那些字段内对齐。
为了访问我们的真实数据,我们编写了(在结构之外!)'getters' 和 'setters' 具有位移和位掩码的宏:
#define READY_MASK 0x01 // indicates that READY is a single bit value
#define READY_OFFSET 3 // indicates position of READY inside 32-bit field
#define IS_READY(x) { ... } // returns READY value from x field
#define SET_READY(x,r) { ... } // sets READY value to x field
现在我想通过直接在结构中添加 getter 和 setter 来修改、简化并使这个过程更安全,例如:
typedef struct {
uint32_t data1;
#define READY_MASK 0x01
#define READY_OFFSET 3
inline void set_ready(uint32_t r) { /*...*/ }
inline uint32_t is_ready() { /*...*/ }
// lots of other getters and setters
} Data1;
就我的实验而言,我注意到这种修改不会影响结构的大小 sizeof(Data)==sizeof(Data1)
,我可以通过网络发送这个结构并在另一端。
我的问题是:这个修改有什么不对的地方吗?有什么风险或我应该注意的事情吗?
原则上不加虚函数就没有vptr,struct size应该和没有成员函数一样。但它不再是 POD 结构,因此您可能很容易陷入 UB 的模糊地带,即未定义行为。由于 C++(还)没有标准化的 ABI,因此通常最好使用 C 风格的 POD 结构,例如通过网络序列化。为 C++ modules/subsystems 制作精简、可移植的 API 也是如此,坚持使用 C ABI。
此外,如果您考虑这些(正如某些评论者所建议的那样),我建议您远离位域,手动位掩码通常更易于分析和移植。位域可以例如给你字节序问题,这至少部分是因为 struct 'always' 的第一个成员结束于 struct start 的较低内存地址,而不管字节顺序如何。位域的工作方式也可能取决于传输介质,例如BSD 网络流套接字是字节流,但许多 HW 接口复制整个 32 位字大小的块而不是字节。
您所做的修改不会造成任何伤害,但它也不会带来任何好处,因为无论如何结构成员都是 public。 Data和Data1大小相同的原因是因为你添加的是内联函数,不占用对象中的space而是用实际函数代码替换任何函数调用。
现在,如果您要通过网络发送结构和二进制数据,则必须考虑以下两个规则:
按照惯例,整数值是使用网络字节顺序发送的,它是 Big Endian 而不是 x86 的 Little Endian。如果您确定所有发送和接收数据的机器都是 x86,这不是问题。但是,如果任何机器具有不同的字节序,如大字节序或无字节序(或中字节序),你就会遇到问题。例如,您可以有一个 ARM 处理器、一个 SPARC 等。在 C 中,您有以下宏:
ntohs、htons、ntohl、htonl
您还必须考虑不同的内存对齐方式。结构的内存对齐取决于体系结构、编译器和编译模式。编译器可能会添加填充(他们不能在 C 中重新排序成员,但请阅读:Can a C++ compiler re-order elements in a struct)。此外,long 等类型在 32 位和 64 位体系结构中具有不同的大小。即使可以肯定只有两个 unsigned int 成员不会有问题,您也不应该将内存从结构复制到将在消息中发送的数据缓冲区,也不应该从消息中的原始数据复制内存到一个结构。基本上你不应该做这样的事情:
字符缓冲区[BUF_SIZE];
Data1 myData;
memcpy(buffer, myData, sizeof(Data));
https://en.wikipedia.org/wiki/Data_structure_alignment
你应该把成员一个一个地移到缓冲区。从消息中收到的原始数据填充结构时也是如此。
我还建议不要使用位域,阅读 Why bit endianness is an issue in bitfields?。但非常清楚,在您的代码片段中您没有使用位域。使用位域与使用整数中的位来表示某些特定状态不同。在这种情况下,你做得很好,应该只考虑具有不同字节顺序的体系结构中的整数表示(如果这是可能的情况)。