1. 引言#
在 Socket 编程中,我们时常使用的结构体 sockaddr_in
来构建 socket 信息。
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(ip);
serv_addr.sin_port = htons(port);
我们来看看 sockaddr_in
结构体的源码:
struct sockaddr_in {
short sin_family; // 地址族(Address Family),AF_INET
u_short sin_port; // 16位TCP/UDP端口号,网络字节序(Network Byte Order)
struct in_addr sin_addr; // 32位IP地址,网络字节序(Network Byte Order)
char sin_zero[8]; // 暂时没有使用,可以用来填充
};
我们注意到,第 4 行中,我们并没有直接使用 s_addr
字段来表示一个 ip 地址,而是其中嵌套了一个结构体 sin_addr
。
那么这样有什么好处呢?
2. 分析#
在 Unix 平台上,in_addr
结构体被定义为:
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; // 32位的IPV4地址,网络字节序
};
在 Windows 平台上,in_addr
结构体被定义为:
struct in_addr {
union {
struct {
u_char s_b1, s_b2, s_b3, s_b4;
} S_un_b;
struct {
u_short s_w1, s_w2;
} S_un_w;
u_long S_addr;
} S_un;
};
可以看到,在不同的平台上,对于 s_addr
字段的处理方式是不一样的,所以,这样设计保证了平台兼容性。
这里就解释了为什么我们在 sockaddr_in
结构体中看到的 s_addr
字段使用 in_addr
结构体包裹而不是直接使用这个字段了。
3. in_addr 中 Union 的分析#
在 Windows 平台上,in_addr
结构体中使用了 一个 Union 类型来表示 s_addr
字段,分别表示以 4 个字节、2 个 16 位整数或 1 个 32 位整数来解释 IPV4 地址的不同部分。
那么当我们初始化 in_addr
字段后:
serv_addr.sin_addr.s_addr = inet_addr(ip);
我们可以使用上述的 3 种 Union 类型 来对 IPV4
地址进行解释。
4. sockaddr 结构体#
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6
in_port_t sin6_port; //(2)16位端口号
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具体的IPv6地址
uint32_t sin6_scope_id; //(4)接口范围ID
};
观察到,sockaddr
、 sockaddr_in
和 sockaddr_in6
实际上长度是相同的,只是sockaddr
将 ip 地址和端口号合并在一起,后两者是前者的派生类型。
那为什么我们不直接给其传入 IP:Port
的方式呢?
因为 API 并没有提供相关函数去解析 IP
和 Port
,并且原始的 sockaddr
使用起来有诸多不便,这才有了后两者。
但在使用的时候,比如:
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(server_addr)))
我们通过 type punning 的方式(即强制类型转换),来调用上述函数,这样不管是 sockaddr_in
还是 sockaddr_in6
都可以兼容使用了。
type punning: 指在 C/C++ 中使用不同类型访问同一段存储空间的技巧,从而可以变相的改变存储空间的类型,即通过改变变量的类型获取一定的位模式。
type punning 的方式有很多,比如通过 Union 和 强制类型转换,以及 officially sanctioned 方式,如 memcpy 等。
但是在使用 type punning 的时候可能会导致 strict aliasing 的问题,所以需要慎重使用。
strict aliasing: 是指 C/C++ 的一种优化特性,指绝对不允许对一个和另一个类型不同的对象进行访问,它能够有效避免产生优化错误,保证操作的正确性。
这里可以合理使用的原因:这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。