【问题标题】:passing a struct over TCP (SOCK_STREAM) socket in C在C中通过TCP(SOCK_STREAM)套接字传递结构
【发布时间】:2023-04-08 23:41:01
【问题描述】:

我有一个小型客户端服务器应用程序,我希望通过 C 而不是 C++ 中的 TCP 套接字发送整个结构。假设结构如下:

    struct something{
int a;
char b[64];
float c;
}

我发现很多帖子说我需要在发送和接收之前使用 pragma pack 或序列化数据。

我的问题是,使用 JUST pragma pack 还是仅使用序列化是否足够?还是我需要两者都用?

此外,由于序列化是处理器密集型过程,这会使您的性能急剧下降,那么在不使用外部库的情况下序列化结构的最佳方法是什么(我想要示例代码/算法)?

【问题讨论】:

    标签: c sockets tcp ipc


    【解决方案1】:

    您需要以下内容才能通过网络可移植地发送结构:

    • 打包结构。对于 gcc 和兼容的编译器,请使用 __attribute__((packed))

    • 不要使用除固定大小的无符号整数、满足这些要求的其他压缩结构或任何前者的数组之外的任何成员。有符号整数也可以,除非您的机器不使用二进制补码表示。

    • 决定你的协议是使用小端还是大端的整数编码。在读取和写入这些整数时进行转换。

    • 另外,不要使用压缩结构成员的指针,除了大小为 1 或其他嵌套压缩结构的指针。见this answer

    下面是一个简单的编码和解码示例。它假设字节顺序转换函数hton8()ntoh8()hton32()ntoh32()可用(前两个是空操作,但为了一致性)。

    #include <stdint.h>
    #include <inttypes.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    // get byte order conversion functions
    #include "byteorder.h"
    
    struct packet {
        uint8_t x;
        uint32_t y;
    } __attribute__((packed));
    
    static void decode_packet (uint8_t *recv_data, size_t recv_len)
    {
        // check size
        if (recv_len < sizeof(struct packet)) {
            fprintf(stderr, "received too little!");
            return;
        }
    
        // make pointer
        struct packet *recv_packet = (struct packet *)recv_data;
    
        // fix byte order
        uint8_t x = ntoh8(recv_packet->x);
        uint32_t y = ntoh32(recv_packet->y);
    
        printf("Decoded: x=%"PRIu8" y=%"PRIu32"\n", x, y);
    }
    
    int main (int argc, char *argv[])
    {
        // build packet
        struct packet p;
        p.x = hton8(17);
        p.y = hton32(2924);
    
        // send packet over link....
        // on the other end, get some data (recv_data, recv_len) to decode:
        uint8_t *recv_data = (uint8_t *)&p;
        size_t recv_len = sizeof(p);
    
        // now decode
        decode_packet(recv_data, recv_len);
    
        return 0;
    }
    

    就字节顺序转换函数而言,您系统的htons()/ntohs()htonl()/ntohl() 可以分别用于 16 位和 32 位整数,以转换为/从大端。但是,我不知道 64 位整数的任何标准函数,或转换为/从小端。您可以使用my byte order conversion functions;如果这样做,您必须通过定义BADVPN_LITTLE_ENDIANBADVPN_BIG_ENDIAN 来告诉它您的机器的字节顺序。

    就有符号整数而言,转换函数可以像我写和链接的一样安全地实现(直接交换字节);只需将未签名更改为已签名即可。

    更新:如果你想要一个高效的二进制协议,但不喜欢摆弄字节,你可以试试Protocol Buffers (C implementation)。这允许您在单独的文件中描述消息的格式,并生成用于对指定格式的消息进行编码和解码的源代码。我自己也实现了类似的东西,但大大简化了;请参阅 my BProto generatorsome examples(查看 .bproto 文件和 addr.h 中的用法示例)。

    【讨论】:

    • 我将尝试这种方法,我只是想问一下,如果我只使用 sprintf 并使用分隔符将所有数据写入字符串以分隔结构的元素并通过套接字发送然后使用 strtok 提取另一侧的每个元素?这也是一个可行的解决方案吗?
    • 是的,sprintf 可以工作,但适用于整数;如果您想发送一个字符串(即原始字节数组),使用此方法,您必须将它们视为字节数组并将每个字节转换为整数,并在其间插入空格。例如,“abc”将作为“97 98 99”发送。这可能更可取,因为它在调试时更容易分析,但编码/解码很笨拙,特别是如果您希望在解码时进行完整的错误检查。
    • 第二个要点背后的动机是什么 - 仅使用无符号整数。为什么不能在 struct(或 char 数组)中使用 chars 来发送字母、字节或字符串?
    • 为什么recv_data被初始化为uint8_t*类型,为什么不是char*
    【解决方案2】:

    在通过 TCP 连接发送任何数据之前,请制定协议规范。它不必是充满技术术语的多页文档。但它确实必须指定谁在何时传输什么,并且必须在字节级别指定所有消息。它应该指定消息的结束是如何建立的,是否有任何超时以及谁强加了它们等等。

    没有规范,很容易提出根本无法回答的问题。如果出现问题,哪一端有问题?有了规范,不遵循规范的那端就有错。 (如果两端都遵循规范,但仍然不起作用,则规范有问题。)

    一旦制定了规范,就可以更轻松地回答有关一端或另一端应该如何设计的问题。

    我也强烈建议不要围绕您的硬件细节设计网络协议。至少,并非没有经过验证的性能问题。

    【讨论】:

      【解决方案3】:

      这取决于您能否确定连接两端的系统是同构的。如果你确定,永远(我们大多数人都做不到),那么你可以走一些捷径——但你必须知道它们是捷径。

      struct something some;
      ...
      if ((nbytes = write(sockfd, &some, sizeof(some)) != sizeof(some))
          ...short write or erroneous write...
      

      还有类似的read()

      但是,如果系统可能会有所不同,那么您需要确定数据的正式传输方式。您可能会很好地线性化(序列化)数据 - 可能使用 ASN.1 之类的东西,或者更简单地使用可以轻松重读的格式。为此,文本通常是有益的——当您可以看到问题所在时,调试起来会更容易。如果做不到这一点,您需要定义传输int 的字节顺序,并确保传输遵循该顺序,并且字符串可能会获取字节数,然后是适当数量的数据(考虑是否传输终端null 与否),然后是浮点数的一些表示。这更繁琐。编写序列化和反序列化函数来处理格式化并不是那么难。棘手的部分是设计(决定)协议。

      【讨论】:

      • 这在某些情况下会起作用,但我的服务器和客户端很可能是 32 位和 64 位机器,因此 sizeof(struct) 函数将返回不同大小的值作为int 将从 4 个字节增加到 8 个字节。
      【解决方案4】:

      您可以将union 与您要发送的结构和数组一起使用:

      union SendSomething {
          char arr[sizeof(struct something)];
          struct something smth;
      };
      

      这样您就可以只发送和接收 arr。当然,您必须注意字节顺序问题,sizeof(struct something) 可能因机器而异(但您可以使用#pragma pack 轻松克服这个问题)。

      【讨论】:

        【解决方案5】:

        当有像 Message Pack 这样的优秀且快速的序列化库为您完成所有艰苦的工作时,您为什么要这样做?作为奖励,它们为您的套接字协议提供跨语言兼容性?

        使用 Message Pack 或其他一些序列化库来执行此操作。

        【讨论】:

        • 我不允许使用任何外部库。 ://
        【解决方案6】:

        通常,序列化比例如序列化带来一些好处。通过网络发送结构的位(例如fwrite)。

        1. 它针对每个非聚合原子数据(例如 int)单独发生。
        2. 它精确定义了通过网络发送的串行数据格式
        3. 因此它处理异构架构:发送和接收机器可能有不同的字长和字节序。
        4. 当类型改变一点时,它可能不那么脆弱。因此,如果一台机器运行旧版本的代码,它可能能够与具有更新版本的机器通信,例如一个有char b[80]; 而不是char b[64];
        5. 它可以处理更复杂的数据结构——可变大小的向量,甚至是哈希表——以一种逻辑方式(对于哈希表,传输关联,..)

        通常会生成序列化例程。甚至在 20 年前,RPCXDR 已经为此目的而存在,并且 XDR 序列化原语仍然存在于许多 libc 中。

        【讨论】:

          【解决方案7】:

          Pragma pack 用于另一端结构的二进制兼容性。 因为您将结构发送到的服务器或客户端可能是用另一种语言编写的,或者是使用其他 c 编译器或其他 c 编译器选项构建的。

          据我了解,序列化是从您的结构中生成字节流。当您在套接字中编写结构时,您会进行序列化。

          【讨论】:

            【解决方案8】:

            Google Protocol Buffer 为这个问题提供了一个很好的解决方案。参考这里Google Protobol Buffer - C Implementaion

            根据负载的结构创建一个 .proto 文件并将其保存为 payload.proto

            syntax="proto3"
            
            message Payload {
                 int32 age = 1;
                 string name = 2;
            } . 
            

            使用

            编译 .proto 文件
            protoc --c_out=. payload.proto
            

            这将在您的目录中创建头文件 payload.pb-c.h 及其对应的 payload.pb-c.c

            创建您的 server.c 文件并包含 protobuf-c 头文件

            #include<stdio.h>
            #include"payload.pb.c.h"
            
            int main()
            {
               Payload pload = PLOAD__INIT;
               pload.name = "Adam";
               pload.age = 1300000;
            
               int len = payload__get_packed_size(&pload);
            
               uint8_t buffer[len];
            
               payload__pack(&pload, buffer);
            
               // Now send this buffer to the client via socket. 
            }
            

            在您的接收方client.c

            ....
            int main()
            {
               uint8_t buffer[MAX_SIZE]; // load this buffer with the socket data. 
               size_t buffer_len; // Length of the buffer obtain via read()
               Payload *pload = payload_unpack(NULL, buffer_len, buffer);
            
               printf("Age : %d Name : %s", pload->age, pload->name);
            }
            

            确保使用 -lprotobuf-c 标志编译程序

            gcc server.c payload.pb-c.c -lprotobuf-c -o server.out
            gcc client.c payload.pb-c.c -lprotobuf-c -o client.out
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2010-12-07
              • 2018-06-26
              • 2016-01-11
              • 2017-04-24
              • 2014-09-02
              • 1970-01-01
              • 2018-02-18
              • 1970-01-01
              相关资源
              最近更新 更多