在C语言中通过TCP(SOCK_STREAM)套接字传输结构体

12

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

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

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

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

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

8个回答

18

要在不同平台上可移植地发送结构体,需要遵循以下步骤:

  • 使用__attribute__((packed))对结构体进行紧凑封装(仅适用于gcc和兼容编译器)。

  • 只使用无符号固定大小整数、满足这些要求的其他紧凑结构体或任何前述元素的数组。有符号整数也可以使用,除非您的计算机不使用二进制补码表示。

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

  • 不要获取紧凑结构体成员的指针,除了那些大小为1或其他嵌套的紧凑结构体的成员。参见此答案

下面是一个简单的编码和解码示例,假设字节顺序转换函数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位整数或用于小端转换。您可以使用我的字节顺序转换函数;如果这样做,您必须通过定义BADVPN_LITTLE_ENDIANBADVPN_BIG_ENDIAN来告诉它您机器的字节顺序。
就有符号整数而言,转换函数可以以与我编写和链接的函数相同的方式安全地实现(直接交换字节);只需将无符号更改为有符号即可。 更新:如果您想要一个高效的二进制协议,但不喜欢调整字节,则可以尝试类似Protocol BuffersC实现)的东西。这允许您在单独的文件中描述您的消息格式,并生成源代码,您可以使用该代码对指定的格式的消息进行编码和解码。我也自己实现了类似的东西,但大大简化了;请参见我的BProto生成器一些示例(查看.bproto文件以及addr.h中的用法示例)。

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

5
在发送任何数据之前,需要制定一个协议规范来进行TCP连接。这个规范不必是一个充满技术术语的多页文档,但必须指定谁在何时传输什么以及必须在字节级别上指定所有消息。它应该指定如何确定消息的结束,是否存在任何超时以及由谁强制执行等等。
如果没有规范,很容易提出根本无法回答的问题。如果出现问题,哪一端有问题?有了规范,没有遵循规范的一方有问题。(如果两端都遵循规范但仍然无法正常工作,则规范有问题。)
一旦有了规范,就更容易回答有关如何设计一端或另一端的问题。
我也强烈建议不要围绕硬件的具体细节来设计网络协议。至少,在没有证明性能问题的情况下不要这样做。

2

这取决于你是否能确定连接两端的系统是同质的。如果你确信它们始终是同质的(但大多数人无法确定),那么你可以采取一些捷径,但必须意识到它们只是捷径。

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个字节。 - user434885

1
你可以使用一个带有你想要发送的结构体和数组的联合体:
union SendSomething {
    char arr[sizeof(struct something)];
    struct something smth;
};

这样你就可以发送和接收数组了。当然,你需要注意大小端问题,而且sizeof(struct something)可能会因机器而异(但你可以轻松地通过#pragma pack来解决这个问题)。


1

既然已经有像Message Pack这样的优秀且快速的序列化库可以为您完成所有繁重的工作,并且作为额外奖励,它们还可以为您提供套接字协议的跨语言兼容性,那么为什么要这样做呢?

使用Message Pack或其他序列化库来完成此操作。


我不被允许使用任何外部库。 :/ - user434885

1

通常,序列化相对于例如通过 fwrite 发送结构体位的方式带来了几个好处。

  1. 它逐个处理每个非聚合原子数据(例如 int)。
  2. 它精确定义了发送到网络上的串行数据格式。
  3. 因此,它处理异构架构:发送和接收机器可能具有不同的字长和字节顺序。
  4. 当类型稍微改变时,它可能会更加灵活。因此,如果一台机器正在运行您代码的旧版本,则它可能能够与具有更新版本的机器进行通信,例如具有 char b[80]; 而不是 char b[64]; 的机器。
  5. 它可以以逻辑方式处理更复杂的数据结构 - 可变大小的向量,甚至哈希表(对于哈希表,传输关联信息等)。

通常情况下,序列化例程是自动生成的。即使在20年前,RPCXDR已经存在于此目的,而XDR序列化基元仍然存在于许多libc中。


0

Pragma pack用于在另一端实现二进制兼容性。 因为您发送结构体的服务器或客户端可能是使用其他语言编写或使用其他C编译器或其他C编译器选项进行构建。

序列化,据我所知,是从结构体中创建字节流的过程。当您将结构体写入套接字时,就会进行序列化。


0

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接