banner
Rick Sanchez

Rick Sanchez

OS && DB 爱好者,深度学习炼丹师,蒟蒻退役Acmer,二刺螈。

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

FwfiZF0acAAyLU8

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
};

观察到,sockaddrsockaddr_insockaddr_in6 实际上长度是相同的,只是sockaddr 将 ip 地址和端口号合并在一起,后两者是前者的派生类型。

那为什么我们不直接给其传入 IP:Port 的方式呢?
因为 API 并没有提供相关函数去解析 IPPort,并且原始的 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++ 的一种优化特性,指绝对不允许对一个和另一个类型不同的对象进行访问,它能够有效避免产生优化错误,保证操作的正确性。

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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。