【问题标题】:Parsing network data for little and big endian为小端和大端解析网络数据
【发布时间】:2020-12-17 20:51:02
【问题描述】:

如何为大端和小端系统编写通用数据报解析器?我不明白的是如何一次从字节缓冲区传递 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?

直接转换为结构会更好吗?

【问题讨论】:

    标签: c endianness


    【解决方案1】:

    假设您的意思是位而不是字节。

    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 或其他废话。

    【讨论】:

    • 那么这在大小端机器上会有相同的结果吗?
    • @pbuzz007:是的!
    • 好的。所以如果数据是大端的,我们只是交换 ORing 的顺序吗?那还会支持这两种机器类型吗?
    • @pbuzz007:是的!
    • 这个解决方案总是交换。如果您的协议是 little-endian 并且您的主机系统也是 little-endian,那么它仍然会交换,这是不正确的。您需要一个仅在协议和主机字节序不同时才进行交换的解决方案。如果主机字节序 == 网络字节序,则 ntoh 和 hton 通常(总是)无操作。
    【解决方案2】:

    直接转换为结构会更好吗?

    没有。除了字节顺序之外,还有其他问题。像结构填充(其中数量是“特定于编译器实现”)和对齐。比如这个结构:

    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 来提高几乎所有计算机的性能。由于历史原因,“网络顺序”是大端的,这使得ntohlhtonlntohshtons 在您要确保数据为小端时无用。

    【讨论】:

    • 这是不正确的outData=&gt;Param_a。我相信你的意思是outData-&gt;Param_a。结构outData的其他成员访问语句相同。
    • @H.S.:谢谢 - 已修复 :-)
    猜你喜欢
    • 2010-11-03
    • 2017-12-22
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-14
    相关资源
    最近更新 更多