在C语言中,两个结构体之间的“转移/转换”是如何进行的?

3
我正在学习HTTP协议,遵循一篇教程,其中给出了易于理解的代码片段,以下是其中的一部分。
struct sockaddr_in address;
...
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons( PORT );

memset(address.sin_zero, '\0', sizeof address.sin_zero);


if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0)
{
    perror("In bind");
    exit(EXIT_FAILURE);
}

这个示例代码运行良好,尽管我不理解两个结构体之间的某种传输方式。

<netinet/in.h>struct sockaddr_in的定义如下:

struct sockaddr_in {
    __uint8_t   sin_len;
    sa_family_t sin_family;
    in_port_t   sin_port;
    struct  in_addr sin_addr;
    char        sin_zero[8];
};

<sys/socket.h>中,struct sockaddr的定义如下:
struct sockaddr {
    __uint8_t   sa_len;     /* total length */
    sa_family_t sa_family;  /* [XSI] address family */
    char        sa_data[14];    /* [XSI] addr value (actually larger) */
};

它们有不同的结构,"传输/转换"是如何工作的呢?

1
你必须记住,这个接口是在C语言中没有void *类型作为通用指针的时候创建的。 - Gerhardh
@Gerhardh 谢谢。 "this interface" 这个接口是指 struct sockaddr_instruct sockaddr 还是其他什么? - JJJohn
我指的是整个套接字接口,包括所有相应的类型和函数。 - Gerhardh
所呈现的代码不涉及这两种结构类型之间的任何强制转换。进行转换的是一个指针(指向不同的指针类型)。这样的指针转换是明确允许的。通过所得到的指针访问指向的对象是一个不同的问题。 - John Bollinger
3个回答

1

强制类型转换有效。 看这两个结构:

struct sockaddr_in {
    __uint8_t   sin_len;
    sa_family_t sin_family;
    in_port_t   sin_port;
    struct in_addr sin_addr;
    char        sin_zero[8];
};

struct sockaddr {
    __uint8_t   sa_len;     /* total length */
    sa_family_t sa_family;  /* [XSI] address family */
    char        sa_data[14];    /* [XSI] addr value (actually larger) */
};

前两个成员,sin_lensa_lensin_familysa_family 不会有问题,因为它们是相同的数据类型。对于 sa_family_t 的填充在两端都完全相同。查看参考文献,

in_port_t 相当于描述在 <inttypes.h> 中的类型 uint16_t
in_addr_t 相当于描述在 <inttypes.h> 中的类型 uint32_t

对于 Windowsstruct in_addr 如下所示:

struct in_addr {
    union {
        struct {
            u_char s_b1;
            u_char s_b2;
            u_char s_b3;
            u_char s_b4;
        } S_un_b;
        struct {
            u_short s_w1;
            u_short s_w2;
        } S_un_w;
        u_long S_addr;
    } S_un;
};

而对于Linux来说,它是:

struct in_addr {
   uint32_t s_addr;     /* address in network byte order */
};

你可能感到困惑的原因是内容对齐方式。然而,这是一个经过深思熟虑的历史性设计。它旨在容纳设计中依赖于实现的方面。 其次,“依赖于实现”指的是在所有系统中,in_addr_t 的实现不一致,如上所示。
简而言之,整个魔法的实现取决于两个因素:前两个成员的确切大小和填充特性,以及最后一个 sa_data[14] 的数据类型是 char,更准确地说是一个 1 字节数据类型的数组。这种在 struct 中使用 union 的设计技巧已被广泛应用。
《Unix 网络编程卷 1》指出:
sin_addr成员是一个结构体,而不仅仅是in_addr_t,这是由于历史原因。早期的版本(4.2BSD)将in_addr结构定义为各种结构的联合体,以允许访问32位IPv4地址中的4个字节和两个16位值中的每一个。这在使用A、B和C类地址时用于获取地址的适当字节。但随着子网划分的出现以及各种地址类别的消失,联合体的需求也随之消失。今天大多数系统已经放弃了联合体,只将in_addr定义为具有单个in_addr_t成员的结构体。 虽然不是您要求的内容,但还是很有用的: 同一标头声明:
sockaddr_in结构用于存储Internet地址族的地址。应用程序应将此类型的值转换为struct sockaddr以便与套接字函数一起使用。
因此,sockaddr_in是专门针对基于IP的通信的结构体,而sockaddr更多地是用于套接字操作的通用结构体。
只是尝试一下:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(void)
{
    printf("sizeof(struct sockaddr_in) = %zu bytes\n", sizeof(struct sockaddr_in));
    printf("sizeof(struct sockaddr) = %zu bytes\n", sizeof(struct sockaddr));
    return 0;
}

输出:

sizeof(struct sockaddr_in) = 16 bytes
sizeof(struct sockaddr) = 16 bytes

非常感谢。这里的“padding”是否意味着在sin_family/sa_family字段之后的所有字段? - JJJohn
不完全是。当在结构体成员方面使用填充一词时,它应被解释为将该结构体成员填充到自然地址边界。因此,不仅仅是“所有”字段,而是从 sa_family 之后的地址位置直到自然边界,通常为4个字节。 - WedaPashi
如果您对结构填充的概念不太了解,这可能有点复杂。这篇文章或许可以帮助您 - WedaPashi
你如何确定编译器没有将 sin_addr 对齐到32位边界?如果是这样,两个结构的大小就不同了。 - Guillaume Petitjean

1
我不理解两个结构体之间的某种转移。
不同结构体之间没有数据传输,也没有结构对象的任何转换。在bind(server_fd, (struct sockaddr *)&address, sizeof(address))中,一个指向结构体的指针被转换为另一个对象指针类型。这是C明确允许的。
C语言规范没有定义通过转换后的指针访问结构体的任何行为。任何尝试这样做的行为都会违反严格别名规则,但这不是你的问题。你提出的示例演示了bind()函数的完全标准的使用习惯,它就是为此而设计的。因此,你可以依靠bind()实现对其进行正确处理,无论需要什么魔法。
从概念上讲,你可以观察到struct sockaddrstruct sockaddr_in的前两个成员具有相同的数据类型。因此,你可以想象,bind能够通过转换后的指针访问这两个成员,尽管这构成了严格别名的违规。虽然C没有为此定义行为,但POSIX隐含要求至少在这种情况下工作。然后,这些成员中的第二个表示地址族,通过它bind()可以为实际类型的地址调用适当的行为。
这是C风格的多态的一种变体。第三个bind参数,地址对象的大小,有助于bind()在不知道其真正有效数据类型的情况下复制地址对象。
这些结构体类型和bind() API可以稍微不同地定义,以避免暗示的严格别名违规,但在早期的C中,成员名称直接对应于从结构的开头的偏移量。而且那些名称是全局的,这就是为什么你在这些成员名称中看到了sin_sa_前缀,在许多其他由系统提供的结构体类型中也是如此。现在,最好只接受bind()的使用方式,并由系统提供一个bind()实现来适应它。

0

我认为这个转换破坏了严格别名规则,如果bind函数解引用指针,则是未定义行为。

实际上,该代码假设struct sockaddr_in的所有字段都是连续的,因此您可以将字节缓冲区视为struct sockaddr_instruct sockaddr等效访问。但是,结构的字段不能保证是连续的。例如,如果in_port_t长2个字节,则在32字节机器编译器中,在sin_portsin_addr之间可能会有一个空隙,因为它可能希望将sin_addr字段对齐到32字节地址。

当您开发通信接口驱动程序时,这种编码方式很常见:您接收需要解释为数据结构(例如:第一个字节是地址,后面的字节是长度等)的字节缓冲区。从一个结构转换到另一个结构可以避免复制数据。

请注意,通常编译器提供非标准C的方法来确保结构的所有字段是连续的。例如,在gcc中是__attribute__((packed))

现在,回答你的问题:如果结构体是紧凑的且没有未定义的行为,那么强制转换基本上不起作用。 sa_data 将是位于字段 sin_family 之后的字节数组。因此,该数组将由 sin_port 组成,后跟 sin_addr,然后是数组 sin_zero

编辑:我使用 arm-none-eabi-gcc 在 STM32H7(ARM Cortex M7,32 位架构)上编译了以下结构:

struct in_addr {
    uint32_t s_addr;
};
struct sockaddr_in {
    uint8_t sin_len;
    uint16_t sin_family;
    uint16_t sin_port;
    struct in_addr sin_addr;
    char     sin_zero[8];
};
struct sockaddr {
    uint8_t sa_len;
    uint16_t sa_family;
    char     sin_zero[14];
};

sockaddr_in 的大小为20。

sockaddr 的大小为18。

请注意,如果 sa_family_t 的类型是 char 而不是 short,由于对齐的原因,两个结构的大小相同。


3
在结构体内部有填充位并不重要。前两个字段拥有相同的类型,因此填充位也会相同。而且,只需使用前两个字段就足以确定所需的结构体类型来访问整个数据。 - Gerhardh
在这种情况下,sin_portsin_addrsin_zero之间可能存在填充,这样代码就无法正常工作。 - Guillaume Petitjean
正如我所提到的,只要这两个结构体的前两个成员具有相同的类型,填充就会相同。代码将正常工作。正如您所看到的,OP的代码中这些结构体没有被打包。在调用函数之前,只需要读取这前两个字段,即可使用正确的类型访问其他字段。 - Gerhardh
正如我之前提到的,只要这两个结构体的前两个成员类型相同,那么填充就是一样的。但是接下来的三个字段呢? - Guillaume Petitjean
我可能漏掉了一些东西,但我仍然相信这两个结构体不等价,因为在 sin_portsin_addr 之间可能有一些填充,而 sa_data 假定没有。当然,具体取决于 bind 函数对指针的处理方式,它可能没有影响。 - Guillaume Petitjean
显示剩余4条评论

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