直接转换为结构会更好吗?
没有。除了字节顺序之外,还有其他问题。像结构填充(其中数量是“特定于编译器实现”)和对齐。比如这个结构:
struct myStructure {
uint8_t Param_a;
uint16_t Param_b;
uin8_t Param_c;
uint16_t Param_d;
}
..可能会变得更像:
struct myStructure {
uint8_t Param_a;
uint8_t padding1; // Inserted by compiler
uint16_t Param_b;
uin8_t Param_c;
uint8_t padding2; // Inserted by compiler
uint16_t Param_d;
}
..但也可以变成这个(或其他任何东西):
struct myStructure {
uint8_t Param_a;
uint8_t padding1[3]; // Inserted by compiler
uint16_t Param_b;
uint8_t padding2[2]; // Inserted by compiler
uin8_t Param_c;
uint8_t padding3[3]; // Inserted by compiler
uint16_t Param_d;
}
对于网络协议(数据布局必须完全匹配);即使网络上的所有计算机都是小端的,这也会破坏一切。为了防止出现问题,编译器提供了强制“打包”结构(没有填充)的方法 - 例如struct __attribute__((__packed__)) myStructure { 在 GCC 中。然而;一些 CPU 无法处理未对齐的读取,因此这可能会以不同的方式破坏事物(例如,导致性能问题并导致原子操作失败),因此您不想在之后处理数据时使用“打包”结构。
还值得一提的是,(通常)代码外部的任何内容(例如用户输入、文件数据、网络数据)都应该“假定有效”。它可能被恶意构建以利用您代码中的“意外情况”;这可能是其他一些代码中的错误的结果;这可能是硬件故障的结果。在任何情况下,您都需要在使用前对数据进行完整性检查(并希望报告在数据中发现的任何问题;以便更轻松地制作漂亮的用户界面,或者更快地查找/修复其他人代码中的错误并避免“无法解释的症状”在您的代码中)。为了确保这种情况正确发生,使用语言的类型系统是一个好主意——特别是;有一种类型用于“原始和未经检查”的数据(例如 uint8_t 的数组)和另一种类型的“完整性检查数据”(例如 struct myStructure),以便任何事故/错误(例如,假设数据已被检查如果没有)将在编译时导致“类型不匹配”错误。当然,这意味着您将编写代码以从一种类型转换为另一种类型(同时进行完整性检查),这也解决了涉及数据布局的问题(例如编译器特定的填充、字节序)。
例如:
struct myStructure {
uint8_t Param_a; // Must be a value from 0 to 100
uint16_t Param_b; // Must be a value >= "year 2000"
uin8_t Param_c; // Flags. Must be 1, 2, 4 or 6.
uint16_t Param_d; // Sender's "Request ID" (can be anything - always returned as is in reply packet so sender can figure out which reply is for which request)
}
int parseRawData(struct myStructure *outData, uint8_t **inputBuffer, size_t *inputBufferSize) {
uint8_t a;
uint16_t b;
uin8_t c;
uint16_t d;
// Check size of data received
if(*inputBufferSize == 0) {
return 1; // No data
}
if(*inputBufferSize <= 6) {
return 2; // Not enough data (yet) - can happen for "split packets" in TCP streams
}
// Parse raw data and do sanity checks
a = (*inputBuffer)[0];
if(a > 100) {
return 10; // Value out of range for param_a
}
b = (*inputBuffer)[1] | (*inputBuffer)[2];
if(b < 2000) {
return 20; // Value out of range param_b
}
c = (*inputBuffer)[3];
switch(c) {
case 1:
case 2:
case 4:
case 6:
break;
default:
return 30; // Bad value or unsupported value for param_c
}
d = (*inputBuffer)[4] | (*inputBuffer)[5];
// Data was valid, so store it and update the buffer tracking
outData->Param_a = a;
outData->Param_b = b;
outData->Param_c = c;
outData->Param_d = d;
*inputBuffer += 20;
*inputBufferSize -= 20;
return 0; // No problem!
}
当然,您可能还想使用 enum 来表示错误代码,并且可能需要某种“将缓冲区中的 2 个字节转换为 uint16_t”的宏。
关于ntohl、htonl、ntohs、htons
几乎所有计算机都是 little-endian,因此(在设计任何东西时 - 例如网络协议、文件格式等)您希望使用 little-endian 来提高几乎所有计算机的性能。由于历史原因,“网络顺序”是大端的,这使得ntohl、htonl、ntohs、htons 在您要确保数据为小端时无用。