Socket 编程中 sockaddr 及 sockaddr_in 结构体分析

1. 引言

在 Socket 编程中，我们时常使用的结构体 sockaddr_in 来构建 socket 信息。

Copy
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 结构体的源码：

Copy
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 结构体被定义为：

Copy
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr; // 32位的IPV4地址，网络字节序
};

在 Windows 平台上，in_addr 结构体被定义为：

Copy
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 字段后：

Copy
serv_addr.sin_addr.s_addr = inet_addr(ip);

我们可以使用上述的 3 种 Union 类型 来对 IPV4 地址进行解释。

4. sockaddr 结构体

Copy
struct sockaddr{
sa_family_t sin_family; //地址族（Address Family），也就是地址类型
char sa_data[14]; //IP地址和端口号
};

Copy
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填充
};

Copy
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 使用起来有诸多不便，这才有了后两者。

但在使用的时候，比如:

Copy
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++ 的一种优化特性，指绝对不允许对一个和另一个类型不同的对象进行访问，它能够有效避免产生优化错误，保证操作的正确性。

这里可以合理使用的原因：这两个结构体的长度相同，强制转换类型时不会丢失字节，也没有多余的字节。