为小端和大端解析网络数据

Parsing network data for little and big endian

如何为大端系统和小端系统编写通用的数据报解析器?我不明白的是如何一次从字节缓冲区传递 16 位或 32 位的字节...

假设你有这个数据报负载

uint8_t datagram[8] = {0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8};

有些协议说

  1. 参数 a - 8 位
  2. 参数 b - 16 位(小端)
  3. 参数 c - 8 位
  4. 参数 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?

直接转换为结构是否更好?

假设您指的是位而不是字节。

ntohsntohl 仅在您的数据包是大端时才有用。所以我们要把它拼出来。

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

几乎所有计算机都是小端法,因此(在设计任何东西时——例如网络协议、文件格式等)您想使用小端法来提高几乎所有计算机的性能。由于历史原因,“网络顺序”是大端的,这使得 ntohlhtonlntohshtons 在你想确保数据是小端时无用。