在 C 中通过 TCP (SOCK_STREAM) 套接字传递结构

发布于 2024-12-14 02:51:13 字数 329 浏览 1 评论 0原文

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

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

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

我的问题是,仅使用 pragma pack 或仅使用序列化就足够了吗?或者我需要同时使用两者?

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

I have a small client server application in which i wish to send an entire structure over a TCP socket in C not C++. Assume the struct to be the following:

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

I have found many posts saying that i need to use pragma pack or to serialize the data before sending and recieveing.

My question is, is it enough to use JUST pragma pack or just serialzation ? Or do i need to use both?

Also since serialzation is processor intensive process this makes your performance fall drastically, so what is the best way to serialize a struct WITHOUT using an external library(i would love a sample code/algo)?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

痕至 2024-12-21 02:51:14

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

  • 打包结构。对于 gcc 和兼容编译器,请使用 __attribute__((packed)) 执行此操作。

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

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

  • 此外,不要获取压缩结构成员的指针,大​​小为 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 告诉它您的计算机的字节顺序。

就带符号整数而言,转换函数可以按照我编写和链接的相同方式安全地实现(直接交换字节);只需将未签名更改为已签名即可。

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

You need the following to portably send struct's over the network:

  • Pack the structure. For gcc and compatible compilers, do this with __attribute__((packed)).

  • Do not use any members other than unsigned integers of fixed size, other packed structures satisfying these requirements, or arrays of any of the former. Signed integers are OK too, unless your machine doesn't use a two's complement representation.

  • Decide whether your protocol will use little- or big-endian encoding of integers. Make conversions when reading and writing those integers.

  • Also, do not take pointers of members of a packed structure, except to those with size 1 or other nested packed structures. See this answer.

A simple example of encoding and decoding follows. It assumes that the byte order conversion functions hton8(), ntoh8(), hton32(), and ntoh32() are available (the former two are a no-op, but there for consistency).

#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;
}

As far as byte order conversion functions are concerned, your system's htons()/ntohs() and htonl()/ntohl() can be used, for 16- and 32-bit integers, respectively, to convert to/from big-endian. However, I'm not aware of any standard function for 64-bit integers, or to convert to/from little endian. You can use my byte order conversion functions; if you do so, you have to tell it your machine's byte order by defining BADVPN_LITTLE_ENDIAN or BADVPN_BIG_ENDIAN.

As far as signed integers are concerned, the conversion functions can be implemented safely in the same way as the ones I wrote and linked (swapping bytes directly); just change unsigned to signed.

UPDATE: if you want an efficient binary protocol, but don't like fiddling with the bytes, you can try something like Protocol Buffers (C implementation). This allows you to describe the format of your messages in separate files, and generates source code that you use to encode and decode messages of the format you specify. I also implemented something similar myself, but greatly simplified; see my BProto generator and some examples (look in .bproto files, and addr.h for usage example).

屋顶上的小猫咪 2024-12-21 02:51:14

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

如果没有规范,很容易提出根本无法回答的问题。如果出了问题,到底是哪一端出了问题呢?有了规范,不符合规范的那一端就有问题。 (如果两端都遵循规范,但仍然无法工作,则说明规范有问题。)

一旦有了规范,就可以更容易地回答有关如何设计一端或另一端的问题。

我还强烈建议不要围绕硬件的具体情况设计网络协议。至少,并非没有经过证实的性能问题。

Before you send any data over a TCP connection, work out a protocol specification. It doesn't have to be a multiple-page document filled with technical jargon. But it does have to specify who transmits what when and it must specify all messages at the byte level. It should specify how the ends of messages are established, whether there are any timeouts and who imposes them, and so on.

Without a specification, it's easy to ask questions that are simply impossible to answer. If something goes wrong, which end is at fault? With a specification, the end that didn't follow the specification is at fault. (And if both ends follow the specification and it still doesn't work, the specification is at fault.)

Once you have a specification, it's much easier to answer questions about how one end or the other should be designed.

I also strongly recommend not designing a network protocol around the specifics of your hardware. At least, not without a proven performance issue.

古镇旧梦 2024-12-21 02:51:14

这取决于您是否可以确定连接两端的系统是同构的。如果您始终确定(我们大多数人都做不到),那么您可以采取一些捷径 - 但您必须意识到它们是捷径。

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

以及类似的read()

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

It depends on whether you can be sure that your systems on either end of the connection are homogeneous or not. If you are sure, for all time (which most of us cannot be), then you can take some shortcuts - but you must be aware that they are shortcuts.

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

and the analogous read().

However, if there's any chance that the systems might be different, then you need to establish how the data will be transferred formally. You might well linearize (serialize) the data - possibly fancily with something like ASN.1 or probably more simply with a format that can be reread easily. For that, text is often beneficial - it is easier to debug when you can see what's going wrong. Failing that, you need to define the byte order in which an int is transferred and make sure that the transfer follows that order, and the string probably gets a byte count followed by the appropriate amount of data (consider whether to transfer a terminal null or not), and then some representation of the float. This is more fiddly. It is not all that hard to write serialization and deserialization functions to handle the formatting. The tricky part is designing (deciding on) the protocol.

穿透光 2024-12-21 02:51:14

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

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

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

You could use an union with the structure you want to send and an array:

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

This way you can send and receive just arr. Of course, you have to take care about endianess issues and sizeof(struct something) might vary across machines (but you can easily overcome this with a #pragma pack).

假装不在乎 2024-12-21 02:51:14

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

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

Why would you do this when there are good and fast serialization libraries out there like Message Pack which do all the hard work for you, and as a bonus they provide you with cross-language compatibility of your socket protocol?

Use Message Pack or some other serialization library to do this.

我的鱼塘能养鲲 2024-12-21 02:51:14

通常,与通过线路发送结构位(例如使用fwrite)相比,序列化会带来一些好处。

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

它可以用逻辑方式处理更复杂的 ,生成序列化例程。即使在 20 年前,RPCXDR 就已经存在用于此目的,并且 XDR 序列化原语仍然存在于许多 libc 中。

Usually, serialization brings several benefits over e.g. sending the bits of the structure over the wire (with e.g. fwrite).

  1. It happens individually for each non-aggregate atomic data (e.g. int).
  2. It defines precisely the serial data format sent over the wire
  3. So it deals with heterogenous architecture: sending and recieving machines could have different word length and endianness.
  4. It may be less brittle when the type change a little bit. So if one machine has an old version of your code running, it might be able to talk with a machine with a more recent version, e.g. one having a char b[80]; instead of char b[64];
  5. It may deal with more complex data structures -variable-sized vectors, or even hash-tables- with a logical way (for the hash-table, transmit the association, ..)

Very often, the serialization routines are generated. Even 20 years ago, RPCXDR already existed for that purpose, and XDR serialization primitives are still in many libc.

憧憬巴黎街头的黎明 2024-12-21 02:51:14

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

据我了解,序列化是从结构中生成字节流。当你在套接字中写入结构时,你就进行了序列化。

Pragma pack is used for the binary compatibility of you struct on another end.
Because the server or the client to which you send the struct may be written on another language or builded with other c compiler or with other c compiler options.

Serialization, as I understand, is making stream of bytes from you struct. When you write you struct in the socket you make serialiazation.

小伙你站住 2024-12-21 02:51:14

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-ch 及其相应的payload.pb-cc 在您的目录中。

创建您的 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< 编译您的程序/strong> 旗帜

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

Google Protocol Buffer offers a nifty solution to this problem. Refer here Google Protobol Buffer - C Implementaion

Create a .proto file based on the structure of your payload and save it as payload.proto

syntax="proto3"

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

Compile the .proto file using

protoc --c_out=. payload.proto

This will create the header file payload.pb-c.h and its corresponding payload.pb-c.c in your directory.

Create your server.c file and include the protobuf-c header files

#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. 
}

On your receiving side 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);
}

Make sure you compile your programs with -lprotobuf-c flag

gcc server.c payload.pb-c.c -lprotobuf-c -o server.out
gcc client.c payload.pb-c.c -lprotobuf-c -o client.out
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文