11.4.7 主机和服务的转换

11.4.7 主机和服务的转换

Linux 提供了一些强大的函数(称为 getaddrinfogetnameinfo)实现二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。当和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的 IP 协议的网络程序。

1. getaddrinfo 函数

getaddrinfo 函数将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构。它是已弃用的 gethostbynamegetservbyname 函数的新的替代品。和以前的那些函数不同,这个函数是可重入的(见 12.7.2 节),适用于任何协议。

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *host, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **result);

返回: 如果成功则为 0,如果错误则为非零的错误代码。

void freeaddrinfo(struct addrinfo *result);

返回: 无。

const char *gai_strerror(int errcode);

返回: 错误消息。

给定 hostservice(套接字地址的两个组成部分),getaddrinfo 返回 resultresult 一个指向 addrinfo 结构的链表,其中每个结构指向一个对应于 hostservice 的套接字地址结构(图 11-15)。

getaddrinfo 返回的数据结构

图 11-15 getaddrinfo 返回的数据结构

在客户端调用了 getaddrinfo 之后,会遍历这个列表,依次尝试每个套接字地址,直到调用 socketconnect 成功,建立起连接。类似地,服务器会尝试遍历列表中的每个套接字地址,直到调用 socketbind 成功,描述符会被绑定到一个合法的套接字地址。为了避免内存泄漏,应用程序必须在最后调用 freeaddrinfo,释放该链表。如果 getaddrinfo 返回非零的错误代码,应用程序可以调用 gai_strerror,将该代码转换成消息字符串。

getaddrinfohost 参数可以是域名,也可以是数字地址(如点分十进制 IP 地址)。service 参数可以是服务名(如 http),也可以是十进制端口号。如果不想把主机名转换成地址,可以把 host 设置为 NULL。对 service 来说也是一样。但是必须指定两者中至少一个。

可选的参数 hints 是一个 addrinfo 结构(见图 11-16),它提供对 getaddrinfo 返回的套接字地址列表的更好的控制。如果要传递 hints 参数,只能设置下列字段:ai_familyai_socktypeai_protocolai_flags 字段。其他字段必须设置为 0(或 NULL)。实际中,我们用 memset 将整个结构清零,然后有选择地设置一些字段:

  • getaddrinfo 默认可以返回 IPv4 和 IPv6 套接字地址。ai_family 设置为 AF_INET 会将列表限制为 IPv4 地址;设置为 AF_INET6 则限制为 IPv6 地址。
  • 对于 host 关联的每个地址,getaddrinfo 函数默认最多返回三个 addrinfo 结构,每个的 ai_socktype 字段不同:一个是连接,一个是数据报(本书未讲述),一个是原始套接字(本书未讲述)。ai_socktype 设置为 SOCK_STREAM 将列表限制为对每个地址最多一个 addrinfo 结构,该结构的套接字地址可以作为连接的一个端点。这是所有示例程序所期望的行为。
  • ai_flags 字段是一个位掩码,可以进一步修改默认行为。可以把各种值用 OR 组合起来得到该掩码。下面是一些我们认为有用的值:

AI_ADDRCONFIG 如果在使用连接,就推荐使用这个标志 [34]。它要求只有当本地主机被配置为 IPv4 时,getaddrinfo 返回 IPv4 地址。对于 IPv6 也是类似。

AI_CANONNAME ai_canonname 字段默认为 NULL。如果设置了该标志,就是告诉 getaddrinfo 将列表中第一个 addrinfo 结构的 ai_canonname 字段指向 host 的权威(官方)名字(见图 11-15)。

AI_NUMERICSERV 参数 service 默认可以是服务名或端口号。这个标志强制参数 service 为端口号。

AI_PASSIVE getaddrinfo 默认返回套接字地址,客户端可以在调用 connect 时用作主动套接字。这个标志告诉该函数,返回的套接字地址可能被服务器用作监听套接字。在这种情况中,参数 host 应该为 NULL。得到的套接字地址结构中的地址字段会是通配符地址(wildcard address),告诉内核这个服务器会接受发送到该主机所有 IP 地址的请求。这是所有示例服务器所期望的行为。

code/netp/netpfragments.c

struct addrinfo {
    int              ai_flags;     /* Hints argument flags */
    int              ai_family;    /* First arg to socket function */
    int              ai_socktype;  /* Second arg to socket function */
    int              ai_protocol;  /* Third arg to socket function */
    char            *ai_canonname; /* Canonical hostname */
    size_t           ai_addrlen;   /* Size of ai_addr struct */
    struct sockaddr *ai_addr;      /* Ptr to socket address structure */
    struct addrinfo *ai_next;      /* Ptr to next item in linked list */
};

图 11-16 getaddrinfo 使用的 addrinfo 结构

getaddrinfo 创建输出列表中的 addrinfo 结构时,会填写每个字段,除了 ai_flagsai_addr 字段指向一个套接字地址结构,ai_addrlen 字段给出这个套接字地址结构的大小,而 ai_next 字段指向列表中下一个 addrinfo 结构。其他字段描述这个套接字地址的各种属性。

getaddrinfo 一个很好的方面是 addrinfo 结构中的字段是不透明的,即它们可以直接传递给套接字接口中的函数,应用程序代码无需再做任何处理。例如,ai_familyai_socktypeai_protocol 可以直接传递给 socket。类似地,ai_addrai_addrlen 可以直接传递给 connectbind。这个强大的属性使得我们编写的客户端和服务器能够独立于某个特殊版本的 IP 协议。

2. getnameinfo 函数

getnameinfo 函数和 getaddrinfo 是相反的,将一个套接字地址结构转换成相应的主机和服务名字符串。它是已弃用的 gethostbyaddrgetservbyport 函数的新的替代品,和以前的那些函数不同,它是可重入和与协议无关的。

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                char *host, size_t hostlen,
                char *service, size_t servlen, int flags);

返回: 如果成功则为 0,如果错误则为非零的错误代码。

参数 sa 指向大小为 salen 字节的套接字地址结构,host 指向大小为 hostlen 字节的缓冲区,service 指向大小为 servlen 字节的缓冲区。getnameinfo 函数将套接字地址结构 sa 转换成对应的主机和服务名字符串,并将它们复制到 hostservice 缓冲区。如果 getnameinfo 返回非零的错误代码,应用程序可以调用 gai_strerror 把它转化成字符串。

如果不想要主机名,可以把 host 设置为 NULLhostlen 设置为 0。对服务字段来说也是一样。不过,两者必须设置其中之一。

参数 flags 是一个位掩码,能够修改默认的行为。可以把各种值用 OR 组合起来得到该掩码。下面是两个有用的值:

  • NI_NUMERICHOST getnameinfo 默认试图返回 host 中的域名。设置该标志会使该函数返回一个数字地址字符串。
  • NI_NUMERICSERV getnameinfo 默认会检查 /etc/services,如果可能,会返回服务名而不是端口号。设置该标志会使该函数跳过查找,简单地返回端口号。

图 11-17 给出了一个简单的程序,称为 HOSTINFO,它使用 getaddrinfogetnameinfo 展示出域名到和它相关联的 IP 地址之间的映射。该程序类似于 11.3.2 节中的 NSLOOKUP 程序。

code/netp/hostinfo.c

#include "csapp.h"

int main(int argc, char **argv)
{
    struct addrinfo *p, *listp, hints;
    char buf[MAXLINE];
    int rc, flags;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
        exit(0);
    }

    /* Get a list of addrinfo records */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;          /* IPv4 only */
    hints.ai_socktype = SOCK_STREAM;    /* Connections only */
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
        exit(1);
    }

    /* Walk the list and display each IP address */
    flags = NI_NUMERICHOST; /* Display address string instead of domain name */
    for (p = listp; p; p = p->ai_next) {
        Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
        printf("%s\n", buf);
    }

    /* Clean up */
    Freeaddrinfo(listp);

    exit(0);
}

图 11-17 HOSTINFO 展示出域名到和它相关联的 IP 地址之间的映射

首先,初始化 hints 结构,使 getaddrinfo 返回我们想要的地址。在这里,我们想查找 32 位的 IP 地址(第 16 行),用作连接的端点(第 17 行)。因为只想 getaddrinfo 转换域名,所以用 service 参数为 NULL 来调用它。

调用 getaddrinfo 之后,会遍历 addrinfo 结构,用 getnameinfo 将每个套接字地址转换成点分十进制地址字符串。遍历完列表之后,我们调用 freeaddrinfo 小心地释放这个列表(虽然对于这个简单的程序来说,并不是严格需要这样做的)。

运行 HOSTINFO 时,我们看到 twitter.com 映射到了四个 IP 地址,和 11.3.2 节用 NSLOOKUP 的结果一样。

linux> ./hostinfo twitter.com
199.16.156.102
199.16.156.230
199.16.156.6
199.16.156.70

练习题 11.4 函数 getaddrinfogetnameinfo 分别包含了 inet_ptoninet_ntop 的功能,提供了更高级别的、独立于任何特殊地址格式的抽象。想看看这到底有多方便,编写 HOSTINFO(图 11-17)的一个版本,用 inet_pton 而不是 getnameinfo 将每个套接字地址转换成点分十进制地址字符串。