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

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

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

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

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

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

您需要以下内容通过网络可移植地发送struct:

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

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

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

  • 此外, 不要使用压缩结构的成员指针 ,除了那些大小为1或其他嵌套打包结构的指针 。 看到这个答案 。

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

 #include  #include  #include  #include  // 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; px = hton8(17); py = 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位整数转换为big-endian或从big-endian转换。 但是,我不知道64位整数的任何标准函数,或者转换为/从小端转换。 您可以使用我的字节顺序转换function ; 如果你这样做,你必须通过定义BADVPN_LITTLE_ENDIANBADVPN_BIG_ENDIAN来告诉它你机器的字节顺序。

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

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

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

没有规范,很容易提出根本无法回答的问题。 如果出现问题,哪一端有问题? 对于规范,不符合规范的结束是错误的。 (如果两端都遵循规范并且仍然不起作用,那么规范是错误的。)

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

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

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

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

和类似的read()

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

您可以使用具有要发送的结构和数组的联合:

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

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

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

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

通常,序列化带来了几个好处,例如通过线路发送结构的比特(例如fwrite )。

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

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

Pragma包用于另一端的结构的二进制兼容性。 因为发送结构的服务器或客户端可能使用其他语言编写,也可能与其他c编译器或其他c编译器选项一起构建。

据我所知,序列化正在从你的struct生成字节流。 当您在套接字中编写struct时,您将进行序列化。