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