3.3. 数据结构

很好,终於讲到这里了,该是谈谈编程的时间了。在本节,我会介绍 socket 接口的各种数据型别,因为它们有些会不太好理解。

首先是最简单的:socket descriptor,型别如下:

int

就是一般的 int。

从这里开始会有点不好理解,所以不用问太多,直接读过就好。

我的第一个 StructTM -struct addrinfo,这个数据结构是最近的发明,用来准备之後要用的 socket 地址数据结构,也用在主机名(host name)及服务名(service name)的查询。当我们之後开始实际应用时,才会开始觉得比较靠谱,现在只需要知道你在建立连接调用时会用到这个数据结构。

struct addrinfo {
    int ai_flags; // AI_PASSIVE, AI_CANONNAME 等。
    int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
    int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
    int ai_protocol; // 用 0 当作 "any"
    size_t ai_addrlen; // ai_addr 的大小,单位是 byte
    struct sockaddr *ai_addr; // struct sockaddr_in 或 _in6
    char *ai_canonname; // 典型的 hostname
    struct addrinfo *ai_next; // 链表丶下个节点
};

你可以载入这个数据结构,然後调用 getaddrinfo()。它会返回一个指针,这个指针指向一个新的链表,这个链表有一些数据结构,而数据结构的内容记载了你所需的东西。

你可以在 ai_family 栏位中设定强制使用 IPv4 或 IPv6,或者将它设定为 AF_UNSPEC,AF_UNSPEC 很酷,因为这样你的程序就可以不用管 IP 的版本。

要注意的是,这是个链表:ai_next 是指向下一个成员(element),可能会有多个结果让你选择。我会直接用它提供的第一个结果,不过你可能会有不同的个人考量;先生!我不是万事通。

你会在 struct addrinfo 中看到 ai_addr 栏位是一个指向 struct sockaddr 的指针。这是我们开始要了解 IP 地址结构中有哪些细节的地方。有时候,你需要的是调用 getaddrinfo() 帮你填好 struct addrinfo。然而,你必须查看这些数据结构,并将值取出,所以我在这边会进行说明。

[还有,在发明 struct addrinfo 以前的代码都要手动填写这些数据的每个栏位,所以你会看到很多 IPv4 的代码真的用很原始的方式去做这件事。你知道的,本教程在旧版也是这样做]。

有些 structs 是 IPv4,而有些是 IPv6,有些两者都是。我会特别注明清楚它们属於哪一种。

总之,struct sockaddr 记录了很多 sockets 类型的 socket 的地址资料。

struct sockaddr {
    unsigned short sa_family; // address family, AF_xxx
    char sa_data[14]; // 14 bytes of protocol address
};

sa_family 可以是任何东西,不过在这份教程中我们会用到的是 AF_INET[IPv4]或 AF_INET6[IPv6]。sa_data 包含一个 socket 的目地地址与 port number。这样很不方便,因为你不会想要手动的将地址封装到 sa_data 里。

为了处理 struct sockaddr,程序设计师建立了对等平行的数据结构:struct sockaddr_in["in"是代表"internet"]用在 IPv4。

而这有个重点:指向 struct sockaddr_in 的指针可以转型(cast)为指向 struct sockaddr 的指针,反之亦然。所以即使 connect() 需要一个 struct sockaddr *,你也可以用 struct sockaddr_in,并在最後的时候对它做型别转换!

// (IPv4 only--see struct sockaddr_in6 for IPv6)
struct sockaddr_in {
    short int sin_family; // Address family, AF_INET
    unsigned short int sin_port; // Port number
    struct in_addr sin_addr; // Internet address
    unsigned char sin_zero[8]; // 与 struct sockaddr 相同的大小
};

这个数据结构让它很容易可以参考(reference)socket 地址的成员。要注意的是 sin_zero[这是用来将数据结构补足符合 struct sockaddr 的长度],应该要使用 memset()函数将 sin_zero 整个清为零。还有,sin_family 是对应到 struct sockaddr 中的 sa_family,并应该设定为"AF_INET"。最後,sin_port 必须是 Network Byte Order[利用 htons()]。

让我们再更深入点!你可以在 sruct in_addr 里看到 sin_addr 栏位。

那是什麽?

好,别太激动,不过它是其中一个最恐怖的 union:

// (仅限 IPv4 — Ipv6 请参考 struct in6_addr)
// Internet address (a structure for historical reasons)
struct in_addr {
    uint32_t s_addr; // that's a 32-bit int (4 bytes)
};

哇!好耶,它以前是 union,不过这个包袱现在似乎已经不见了。因此,若你已将 ina 宣告为 struct sockaddr_in 的型别时,那麽 ina.sin_addr.s_addr 会参考到 4-byte 的 IP address(以 Network Byte Order)。要注意的是,如果你的系统仍然在 struct in_addr 使用超恐怖的 union,你依然可以像我上面说的,精确地参考到 4-byte 的 IP address[这是因为 #define]。

那麽 IPv6 会怎样呢?

IPv6 也有提供类似的 struct,比如:
// (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4)
struct sockaddr_in6 {
    u_int16_t sin6_family; // address family, AF_INET6
    u_int16_t sin6_port; // port number, Network Byte Order
    u_int32_t sin6_flowinfo; // IPv6 flow information
    struct in6_addr sin6_addr; // IPv6 address
    u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
    unsigned char s6_addr[16]; // IPv6 address
};
要注意到 IPv6 协议有一个 IPv6 address 与一个 port number,就像 IPv4 协议有一个 IPv4 address 与 port number 一样。

我现在还不会介绍 IPv6 的流量资料,或是 Scope ID 栏位 … 这只是一份入门教程嘛 :-)

最後要强调的一点,这个简单的 struct sockaddr_storage 是设计用来足够储存 IPv4 与 IPv6 structures 的 structure。[你看看,对於某些 calls,你有时无法事先知道它是否会使用 IPv4 或 IPv6 address 来填好你的 struct sockaddr。所以你用这个平行的 structure 来传递,它除了比较大以外,也很类似 struct sockaddr ,因而可以将它转型为你所需的型别]。

struct sockaddr_storage {
    sa_family_t ss_family; // address family
    // all this is padding, implementation specific, ignore it:
    char __ss_pad1[_SS_PAD1SIZE];
    int64_t __ss_align;
    char __ss_pad2[_SS_PAD2SIZE];
};

重点是你可以在 ss_family 栏位看到地址家族(address family),检查它是 AF_INET 或 AF_INET6(是 IPv4 或 IPv6)。之後如果你愿意的话,你就可以将它转型为 sockaddr_in 或 struct sockaddr_in6。
Comments