在C语言中通过套接字传递结构体

60

我试图从客户端到服务器或者反过来传递整个数据结构。假设我的数据结构如下:

struct temp {
  int a;
  char b;
}

我正在使用 sendto 函数发送结构变量的地址,并且在另一端使用 recvfrom 函数接收它。但是我无法在接收端获得原始数据。在 sendto 函数中,我将接收到的数据保存到类型为 struct temp 的变量中。

n = sendto(sock, &pkt, sizeof(struct temp), 0, &server, length);
n = recvfrom(sock, &pkt, sizeof(struct temp), 0, (struct sockaddr *)&from,&fromlen);

pkt是struct temp类型的变量。

尽管我接收到了8字节的数据,但如果我尝试打印它,它只会显示垃圾值。有什么方法可以修复吗?

注意:不能使用第三方库。

编辑1:我对这个序列化概念真的很陌生..但是不做序列化难道我就不能通过套接字发送一个结构体吗?

编辑2:当我尝试使用sendtorecvfrom函数发送字符串或整数变量时,接收端能够正确接收到数据。为什么在结构体的情况下不行呢? 如果我不使用序列化函数,那我应该逐个发送结构体成员吗?这确实不是一个合适的解决方案,因为如果有'n'个成员,就必须添加'n'行代码来发送或接收数据。


1
你能发布你的发送/接收代码吗? - Brian Agnew
1
为什么你要撤销我对奇怪的双问号进行更正的编辑? - unwind
1
不要将结构体用作网络协议,而应该使用网络协议作为网络协议。设计你的协议,以八位字节为单位,并编写一个库来发送和接收它。或者使用现有的库,如DML、XDR等。使用结构体会引入至少六个你可能没有意识到的依赖项,并引起进一步的问题,比如这个问题。 - user207421
@EJP - 我同意你的观点。 - codingfreak
7个回答

94

这是一个非常糟糕的想法。二进制数据应该以以下方式发送:

永远不要以二进制方式写入整个结构体,无论是写入文件还是套接字。

始终单独写入每个字段,并以相同的方式读取它们。

您需要拥有以下函数:

unsigned char * serialize_int(unsigned char *buffer, int value)
{
  /* Write big-endian int value into buffer; assumes 32-bit int and 8-bit char. */
  buffer[0] = value >> 24;
  buffer[1] = value >> 16;
  buffer[2] = value >> 8;
  buffer[3] = value;
  return buffer + 4;
}

unsigned char * serialize_char(unsigned char *buffer, char value)
{
  buffer[0] = value;
  return buffer + 1;
}

unsigned char * serialize_temp(unsigned char *buffer, struct temp *value)
{
  buffer = serialize_int(buffer, value->a);
  buffer = serialize_char(buffer, value->b);
  return buffer;
}

unsigned char * deserialize_int(unsigned char *buffer, int *value);

或者等价的方式,当然有多种方式可以设置缓冲区管理等内容。然后需要进行高级函数,将整个结构体进行序列化/反序列化。

这假设序列化是针对缓冲区执行的,这意味着序列化无需知道最终目标是文件还是套接字。这也意味着你需要付出一些内存开销,但通常出于性能原因这是一个好的设计(你不想对套接字的每个值都进行write()操作)。

一旦你拥有了上述功能,下面就是如何对结构体实例进行序列化和传输的方法:

int send_temp(int socket, const struct sockaddr *dest, socklen_t dlen,
              const struct temp *temp)
{
  unsigned char buffer[32], *ptr;

  ptr = serialize_temp(buffer, temp);
  return sendto(socket, buffer, ptr - buffer, 0, dest, dlen) == ptr - buffer;
}

关于上述内容需要注意以下几点:
  • 首先将要发送的结构体按字段序列化成buffer
  • 序列化程序返回指向缓冲区中下一个可用字节的指针,我们可以用它来计算它序列化的字节数。
  • 显然,我的示例序列化程序没有防止缓冲区溢出。
  • 如果sendto()调用成功,则返回值为1,否则为0。

5
在这种情况下,int 在不同的机器上可能有不同的大小。 - Douglas Leeder
@unwind - 为什么我要对每个变量进行序列化,比如整型、字符?使用UDP套接字,我可以发送整数、字符,并在接收端正确地接收它们……我只是使用sendto和recvfrom调用……在这种情况下,我不进行任何序列化。你通过右移来做什么……将主机字节顺序转换为网络字节顺序或反之亦然吗?……当我的结构具有枚举和其他类型时,你的方法可能会变得复杂。 - codingfreak
这个答案不太好。如果你喜欢吃苦头,可以按照这种方式自己做。但更简单的方法是打包一个结构体(以确保成员对齐正确),必要时使用位域,并让编译器为你完成工作,它会比你的尝试做得更好。 - xryl669
@xryl669 那么你就依赖于确切的编译器和平台,这对于网络来说是一个非常糟糕的想法。它通常需要在这些方面具有互操作性和独立性。 - unwind
@unwind:大多数情况下,这是用于网络编程的(只需阅读您正在使用的sockaddr等结构的标题),并考虑开发人员在没有位域的情况下进行位掩码和移位以设置sockaddr_in结构中的端口会有多困难。您应该使用正确的pragma打包您的结构,并处理2种情况:大端和小端。编译器并不愚蠢,当需要打包时,如果您的声明书写得好,那么只有一个解决方案。最终,对于代码用户来说,这更容易,并且可以消除错误,而不是添加错误。 - xryl669
显示剩余14条评论

11

使用“pragma”包选项解决了我的问题,但我不确定它是否有任何依赖关系?

#pragma pack(1)   // this helps to pack the struct to 5-bytes
struct packet {
int i;
char j;
};
#pragma pack(0)   // turn packing off

接下来的这段代码没有任何问题,可以正常运行

n = sendto(sock,&pkt,sizeof(struct packet),0,&server,length);

n = recvfrom(sock, &pkt, sizeof(struct packet), 0, (struct sockaddr *)&from, &fromlen);

@devin - http://www.cplusplus.com/forum/general/14659/ 或者 http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html - jschmier
3
你可能在同一台机器上进行了测试。你是否尝试从大端机器到小端机器,反之亦然? - resultsway
@purpletech - 嗯,那可能是个问题。 - codingfreak

9

不需要编写自己的序列化程序来处理shortlong整数类型 - 使用htons()/htonl() POSIX函数即可。


1
@qrdl:正如我的文档所述,我的函数会始终序列化为大端字节序。当然,您也可以使用htonX()/ntohX()函数,但这试图说明一种更通用的方法。 - unwind
@codingfreak 我并没有说要在结构体上使用它。这只是序列化短整型或长整型的正确方式,仅此而已。 - qrdl
@qrdl - 但我的问题是如何通过套接字发送结构体...即使不使用htons()/htonl()函数,我仍然能够使用sendto()和recvfrom()函数正确地发送和接收数据..... - codingfreak
@qrdl:我的函数对主机的字节序不作任何假设。 - unwind
1
@unwind 抱歉,我的错误。我很抱歉,我一直忘记移位是按字节顺序的。我会修改我的帖子。 - qrdl

6
如果您不想自己编写序列化代码,可以找到一个适当的序列化框架并使用它。也许Google的协议缓冲区是可行的?

1

序列化是个好主意。你也可以使用Wireshark来监控流量并了解实际传递在数据包中的内容。


0

不必序列化和依赖第三方库,使用标签、长度和值来设计原始协议更加简单。

Tag: 32 bit value identifying the field
Length: 32 bit value specifying the length in bytes of the field
Value: the field

按需连接。使用枚举标记。并使用网络字节顺序...

易于编码,易于解码。

如果使用TCP,请记住它是数据的“流”,因此如果您发送3个数据包,则不一定会收到3个数据包。它们可能会根据nodelay / nagel算法等被“合并”成一个流,并且您可能会在一个recv中获得它们所有...您需要使用RFC1006等方法来分隔数据。

UDP更容易,您将为每个发送的数据包接收到一个独立的数据包,但它的安全性要低得多。


目前我正在使用recvfrom和sendto,它们通常用于UDP通信的情况下... - codingfreak
是的,它适用于环回或可靠连接。 - user172783
有很多选择,例如Google Protocols、Apache Thrift、ASN.1、CSN.1、JSON、XML等。根据您的应用程序,您可以继续使用当前方法,它可能能正常工作...但它并不是非常健壮! - user172783
我已经在问题中提到了...没有第三方的东西,只用libc库。 - codingfreak
ASN.1, CSN.1, JSON, XML只有在您选择不自己编写时才依赖第三方工具。标记是Google协议、Apache Thrift、ASN.1和CSN.1的最基本构建块,这很简单。您还可以查看此处:liw.iki.fi/liw/texts/cpp-trick.html,尽管它忽略了字节顺序问题。 - user172783

0
如果您要传输的数据格式非常简单,那么将其转换为 ANSI 字符串并进行转换是简单且可移植的。

@mandrill - 假设它是一个复杂的问题——如果可能的话,通用解决方案将是最佳答案...? - codingfreak
如果格式更复杂,那么我会参考本主题中其他人提供的更优秀的解决方案!或者将其序列化为更通用的格式,例如XML或类似SOAP的封装。 - the_mandrill
最终的目标是以可移植的方式序列化数据,因此将其转换为字符串是可移植、简单和易读的。这可能不是最安全或最有效的方法,但它不需要任何第三方库。 - the_mandrill
如何将字符串重新转换为可放入结构体中的数据......我觉得这可能会更加复杂吧? - codingfreak
要将字符串重新转换为数据,您需要解析字符串,使用如atof()、atoi()、sscanf()等的函数。(也许不包括sscanf(),它很危险)。您是正确的,对于非简单数据,解析字符串可能会变得复杂。我建议使用第三方序列化库。 - Jeremy Friesner

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