为什么从sockaddr_in到sockaddr的类型转换可行?

4
在C语言中,绑定Socket的典型方法如下所示:bind
int server_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
int port_number = 55555;

addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port_number);

int result = bind(server_socket_fd,(struct sockaddr *)&addr , sizeof(addr));
if(bind_result > 0)
{
    // Stuff
}

我想知道为什么从sockaddr_in转换为sockaddr的类型转换有效,因为我找不到任何文档说明为什么它有效。似乎每个人都这样做。
为什么这里的类型转换有效?
我不是在问我们为什么要进行强制转换,这已经在这里回答了。我在问为什么它有效。

4个回答

5
允许将结构指针转换为不同的结构指针,然后再转回来。这在C标准的第6.3.2.3p7节中有详细说明:
“对象类型的指针可以转换为不同对象类型的指针。如果所得到的指针未正确对齐于引用类型,则其行为是未定义的。否则,当再次转换时,结果应与原始指针相等。当将对象指针转换为字符类型指针时,结果指向对象的最低寻址字节。结果的连续增量,直至对象大小,会产生指向对象剩余字节的指针。”
上述段落中有一个关于对齐的限制,该限制在第6.2.5p28节中进一步详细说明:
一个指向void的指针应该具有与指向字符类型的指针相同的表示和对齐要求。同样,指向兼容类型的限定或非限定版本的指针应具有相同的表示和对齐要求。所有结构类型的指针应具有彼此相同的表示和对齐要求。所有联合类型的指针应具有彼此相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。

可以假设bind函数知道它拥有哪种套接字描述符,并将struct sockaddr *转换回struct sockaddr_in *


“很可能,bind函数知道它拥有什么类型的套接字描述符” - 是的,因为地址族是套接字内部信息的一部分。 bind()(和connect())验证传入的sockaddr与套接字的地址族匹配。 “并将struct sockaddr *转换回struct sockaddr_in *” - 是的(对于AF_INET),在检查传递的sockaddr是否是正确的族和字节大小之后,才进行转换(这就是为什么bind()connect()accept()有一个参数来接收sockaddr的大小)。如果发生不匹配,bind()(以及connect()accept())将失败。 - Remy Lebeau

3

sockaddr结构体基本上只有一个字段,即地址族。接收此结构体的代码可以使用该字段来确定结构体的实际类型。所有真正使用的结构体也都将该字段作为第一个字段,因此该值是确定的。

实现还使结构体具有相同大小的填充,因此内存使用量完全确定。这使其可以正常工作。

例如,Microsoft在Visual Studio 2017中定义了sockaddr结构体如下:

struct sockaddr {  
    unsigned short sa_family;  
    char sa_data[14];  
};

sa_data
所有不同套接字地址结构的最大大小。

因此,任何可能被发送的“子”结构必须具有14个字节的数据,不能多也不能少。

sockaddr_in 则是

struct sockaddr_in{  
    short sin_family;  
    unsigned short sin_port;  
struct in_addr sin_addr;  
    char sin_zero[8];  
};

这里的端口和 in_addr 需要总共六个字节,因此使用了8个填充字节来保持大小与 sockaddr 相同。

当然,也可以创建例如 sockaddr_un,将其地址族设置为声称它是 sockaddr_in ,任何接收该结构的代码都会进行错误的转换并获得完全错误的值。


3
“所以任何可能被发送的“子”结构体都必须有14个字节的数据,不能多也不能少” - 这不是正确的。 sockaddr 的定义方式是为了保持与 sockaddr_in 的大小兼容性,后者的大小为 16 字节,并且在 AF_INET 是唯一的地址族时 sockaddr 也是这个大小。但是,较新的地址族不限于 14 个字节的数据。例如,sockaddr_in6 有 26 个字节(仅 sin6_addr 字段就有 16 个字节)。唯一的要求是所有 sockaddr_... 类型必须以一个2字节的 family 成员作为起始,其值决定了其余数据的大小。 - Remy Lebeau

2

它之所以有效,是因为bind函数仅使用sockrt_addr结构族中所有元素显然相同的一些前面字段。


1
所有 sockaddr_... 结构体中唯一的共同字段是第一个字段 - 地址 family。仅基于该值,bind() 可以将 sockaddr* 指针转换为更适当的指针类型 (sockaddr_in*, sockaddr_in6*, sockaddr_un* 等)。再加上 addrlen 参数,bind() 使用它来检查 sockaddr* 指向的缓冲区是否足够大,以便在执行转换后安全访问所有 sockaddr_... 字段。 - Remy Lebeau

2
sockaddr类型旨在用于适合低级编程的优质编译器。 在这样的编译器上,将结构的地址强制转换为共享公共初始序列的不同结构类型的指针将产生一个指针,该指针可用于至少检查该公共初始序列的成员,直到发生以下情况之一:
  1. 代码通过派生指针以外的方式写入这些字段。
  2. 代码通过派生指针以外的方式形成指针,该指针将用于写入这些字段。[即使写入“应该”在检查CIS成员的代码之后,编译器也可能将后续写入通过后者指针重新排序到创建它的点,但编译器不能重新排序这样的写入除非它能够证明它将被执行]。
  3. 代码进入其中一个以上的循环。
  4. 代码调用其中一个以上的函数。
从标准的角度来看,支持上述构造实际上是实现质量问题。《Rationale》明确承认,“符合”实现可能如此劣质以至于无用。对于甚至无法处理像上述简单情况的编译器(例如gcc和clang)的任何允许都应该从这个角度来看待。

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