为小端和大端解析网络数据
Parsing network data for little and big endian
如何为大端系统和小端系统编写通用的数据报解析器?我不明白的是如何一次从字节缓冲区传递 16 位或 32 位的字节...
假设你有这个数据报负载
uint8_t datagram[8] = {0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8};
有些协议说
- 参数 a - 8 位
- 参数 b - 16 位(小端)
- 参数 c - 8 位
- 参数 d - 32 位(小端)
所以你想要一个可以在大端和小端机器上工作的通用解析器
uint8_t param_a = datagram[0];
uint16_t param_b = datagram[1]; // how to use ntohs here?
uint8_t param_c = datagram[3];
uint32_t param_d = datagram[4]; // how to use ntohl here?
直接转换为结构是否更好?
假设您指的是位而不是字节。
ntohs
和 ntohl
仅在您的数据包是大端时才有用。所以我们要把它拼出来。
uint8_t param_a = datagram[0];
uint16_t param_b = (uint16_t)datagram[1] | ((uint16_t)datagram[2] << 8);
uint8_t param_c = datagram[3];
uint32_t param_d = (uint32_t)datagram[4] | ((uint32_t)datagram[5] << 8) | ((uint32_t)datagram[6] << 16) | ((uint32_t)datagram[7] << 24);
奖金:我们不需要谈论对齐或 volatile
或其他废话。
Is it better to just cast to structs instead?
没有。除了字节序之外还有其他问题。像结构填充(其中数量是“特定于编译器实现的”)和对齐。比如这个结构:
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 {
在海湾合作委员会中。然而;某些 CPU 无法处理未对齐的读取,因此这可能会以不同的方式破坏事物(例如导致性能问题并导致原子操作失败),因此您不想在之后处理数据时使用“打包”结构。
还值得一提的是,(通常)您的代码之外的任何内容(例如用户输入、文件中的数据、网络中的数据)都不应“假定有效”。它可能已被恶意构造以利用您代码中的“意外情况”;它可能是其他代码中的错误的结果;这可能是硬件故障的结果。在任何情况下,您都需要在使用前对数据进行完整性检查(并希望报告在数据中发现的任何问题;以便更轻松地制作漂亮的用户界面,或更快地 find/fix 其他人的代码中的错误和避免代码中出现“无法解释的症状”)。为了确保这种情况正确发生,最好使用该语言的类型系统——特别是;有一种类型用于“原始和未经检查”的数据(例如 uint8_t
的数组)和一种不同类型的“完整性检查数据”(例如 struct myStructure
),因此任何 accident/mistake (例如,假设数据已经过检查,但实际上没有)将导致编译时出现“类型不匹配”错误。当然,这意味着您将编写代码以从一种类型转换为另一种类型(同时进行健全性检查),这也解决了涉及数据布局的问题(例如编译器特定的填充、字节顺序)。
例如:
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
几乎所有计算机都是小端法,因此(在设计任何东西时——例如网络协议、文件格式等)您想使用小端法来提高几乎所有计算机的性能。由于历史原因,“网络顺序”是大端的,这使得 ntohl
、htonl
、ntohs
、htons
在你想确保数据是小端时无用。
如何为大端系统和小端系统编写通用的数据报解析器?我不明白的是如何一次从字节缓冲区传递 16 位或 32 位的字节...
假设你有这个数据报负载
uint8_t datagram[8] = {0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8};
有些协议说
- 参数 a - 8 位
- 参数 b - 16 位(小端)
- 参数 c - 8 位
- 参数 d - 32 位(小端)
所以你想要一个可以在大端和小端机器上工作的通用解析器
uint8_t param_a = datagram[0];
uint16_t param_b = datagram[1]; // how to use ntohs here?
uint8_t param_c = datagram[3];
uint32_t param_d = datagram[4]; // how to use ntohl here?
直接转换为结构是否更好?
假设您指的是位而不是字节。
ntohs
和 ntohl
仅在您的数据包是大端时才有用。所以我们要把它拼出来。
uint8_t param_a = datagram[0];
uint16_t param_b = (uint16_t)datagram[1] | ((uint16_t)datagram[2] << 8);
uint8_t param_c = datagram[3];
uint32_t param_d = (uint32_t)datagram[4] | ((uint32_t)datagram[5] << 8) | ((uint32_t)datagram[6] << 16) | ((uint32_t)datagram[7] << 24);
奖金:我们不需要谈论对齐或 volatile
或其他废话。
Is it better to just cast to structs instead?
没有。除了字节序之外还有其他问题。像结构填充(其中数量是“特定于编译器实现的”)和对齐。比如这个结构:
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 {
在海湾合作委员会中。然而;某些 CPU 无法处理未对齐的读取,因此这可能会以不同的方式破坏事物(例如导致性能问题并导致原子操作失败),因此您不想在之后处理数据时使用“打包”结构。
还值得一提的是,(通常)您的代码之外的任何内容(例如用户输入、文件中的数据、网络中的数据)都不应“假定有效”。它可能已被恶意构造以利用您代码中的“意外情况”;它可能是其他代码中的错误的结果;这可能是硬件故障的结果。在任何情况下,您都需要在使用前对数据进行完整性检查(并希望报告在数据中发现的任何问题;以便更轻松地制作漂亮的用户界面,或更快地 find/fix 其他人的代码中的错误和避免代码中出现“无法解释的症状”)。为了确保这种情况正确发生,最好使用该语言的类型系统——特别是;有一种类型用于“原始和未经检查”的数据(例如 uint8_t
的数组)和一种不同类型的“完整性检查数据”(例如 struct myStructure
),因此任何 accident/mistake (例如,假设数据已经过检查,但实际上没有)将导致编译时出现“类型不匹配”错误。当然,这意味着您将编写代码以从一种类型转换为另一种类型(同时进行健全性检查),这也解决了涉及数据布局的问题(例如编译器特定的填充、字节顺序)。
例如:
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
几乎所有计算机都是小端法,因此(在设计任何东西时——例如网络协议、文件格式等)您想使用小端法来提高几乎所有计算机的性能。由于历史原因,“网络顺序”是大端的,这使得 ntohl
、htonl
、ntohs
、htons
在你想确保数据是小端时无用。