17、域名解析:gethostbyname()、getaddrinfo()、DNS解析原理

说到网络编程,有个绕不开的话题——域名解析。

你写代码的时候,总不能每次都记IP地址吧?192.168.1.100这种还好说,要是碰上240e:390:3c0:1000::2这种IPv6地址,写代码的人怕是要疯掉。所以,我们得靠域名。

但计算机只认IP,不认域名。那怎么办?中间有个翻译官,就是DNS。

17.1 DNS解析到底干了什么

说白了,DNS就是把www.example.com这种人类友好的名字,翻译成机器能理解的IP地址。

这个过程,我习惯把它想象成查电话本。你喊一声“老王”,电话本翻出来,找到对应的号码,拨过去。DNS也一样,你给个域名,它返回一个或多个IP。

核心要点:域名解析是网络通信的第一步。如果这一步挂了,后面什么都干不了。

我记得有一次线上排查问题,客户端死活连不上服务器。抓包一看,DNS解析超时了。折腾了半天,发现是本地DNS缓存污染。从那以后,我对域名解析这块就格外上心。

17.2 老牌函数:gethostbyname()

这个函数,资历老,但问题也多。我刚开始学网络编程的时候,用的就是它。

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

struct hostent *gethostbyname(const char *name);

返回的是一个struct hostent结构体:

struct hostent {
    char  *h_name;            // 官方主机名
    char **h_aliases;         // 别名列表
    int    h_addrtype;        // 地址类型(AF_INET)
    int    h_length;          // 地址长度
    char **h_addr_list;       // IP地址列表(网络字节序)
};

用法也很直接:

struct hostent *he;
struct in_addr addr;

he = gethostbyname("www.example.com");
if (he == NULL) {
    herror("gethostbyname");
    return -1;
}

// 取出第一个IP地址
memcpy(&addr, he->h_addr_list[0], sizeof(addr));
printf("IP: %s\n", inet_ntoa(addr));

注意:gethostbyname()不支持IPv6。它只能解析IPv4地址。这在今天看来,是个硬伤。

另外,这个函数不是线程安全的。它内部用了静态缓冲区,多线程环境下会出问题。我早期一个项目就踩过这个坑——两个线程同时解析不同域名,结果互相覆盖,查出来的IP全是错的。

17.3 现代方案:getaddrinfo()

到了IPv6时代,gethostbyname()明显不够用了。于是getaddrinfo()登场。

我个人强烈推荐大家用这个。它既能解析IPv4,也能解析IPv6,而且是线程安全的。

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

int getaddrinfo(const char *node,        // 域名或IP
                const char *service,     // 端口号或服务名
                const struct addrinfo *hints,
                struct addrinfo **res);

返回的是一个链表:

struct addrinfo {
    int              ai_flags;
    int              ai_family;     // AF_INET 或 AF_INET6
    int              ai_socktype;   // SOCK_STREAM 或 SOCK_DGRAM
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next;       // 指向下一个结果
};

来看一个完整的例子:

struct addrinfo hints, *res, *p;
int status;
char ipstr[INET6_ADDRSTRLEN];

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;     // 不限制IPv4或IPv6
hints.ai_socktype = SOCK_STREAM; // TCP

status = getaddrinfo("www.example.com", "80", &hints, &res);
if (status != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
    return -1;
}

// 遍历所有结果
for (p = res; p != NULL; p = p->ai_next) {
    void *addr;
    char *ipver;

    if (p->ai_family == AF_INET) {
        struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
        addr = &(ipv4->sin_addr);
        ipver = "IPv4";
    } else {
        struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
        addr = &(ipv6->sin6_addr);
        ipver = "IPv6";
    }

    inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
    printf("  %s: %s\n", ipver, ipstr);
}

freeaddrinfo(res); // 别忘了释放

小技巧:AF_UNSPEC让系统自动选择IPv4或IPv6。如果只想用IPv4,就填AF_INET;只想用IPv6,填AF_INET6

你想想看,getaddrinfo()把域名解析和地址结构体填充一步到位了。以前用gethostbyname()拿到IP,还得自己填sockaddr_in,现在直接拿过来就能用,省事多了。

17.4 两个函数的对比

特性 gethostbyname() getaddrinfo()
IPv6支持 不支持 支持
线程安全
返回结构 静态缓冲区 动态分配链表
端口解析 不支持 支持(service参数)
推荐程度 不推荐(遗留代码) 强烈推荐

17.5 DNS解析的底层原理

光会用函数还不够,咱们得理解背后发生了什么。

当你调用getaddrinfo("www.example.com", ...)时,系统会做以下几件事:

  1. 查本地缓存——先看看/etc/hosts文件或者本地DNS缓存里有没有记录。有就直接返回,省得去网络问。
  2. 问本地DNS服务器——缓存没有,就发请求到配置的DNS服务器(比如8.8.8.8)。
  3. 递归查询——本地DNS服务器如果也不知道,它会替你去问根服务器、顶级域服务器、权威服务器,一层层往下查。
  4. 返回结果——查到IP后,原路返回,并且本地DNS服务器会缓存一段时间(TTL)。

为什么会设计成递归查询?因为客户端不想管那么多。你想想看,如果每个客户端都要自己去遍历DNS树,那网络得乱成什么样。

下面这张图,把整个流程串起来了:

DNS解析流程 客户端 getaddrinfo() 本地缓存 /etc/hosts + 缓存 本地DNS服务器 递归查询 根DNS服务器 . 顶级域DNS服务器 .com 权威DNS服务器 example.com ① 查缓存 缓存命中 ② 未命中,发请求 ③ 问根 返回.com服务器 ④ 问顶级域 返回example.com服务器 ⑤ 问权威 返回IP地址 ⑥ 返回最终结果 拿到IP地址 开始连接 ⑦ 返回给应用

17.6 避坑指南

我曾经在一个高并发服务里,每来一个请求就调一次getaddrinfo()。结果压测的时候,性能直接崩了。

为什么?因为每次解析都要走网络,慢啊。后来我加了一层DNS缓存,把解析结果存起来,TTL时间内直接复用,性能一下就上来了。

注意:不要频繁调用DNS解析函数。能缓存就缓存。特别是getaddrinfo(),它内部可能阻塞等待网络响应。

还有一点,gethostbyname()返回的错误信息用herror()打印,而getaddrinfo()gai_strerror()。别搞混了,不然查错的时候一脸懵。

17.7 小结

域名解析这块,说白了就两件事:

  • 老代码——看到gethostbyname(),知道它怎么用,但别在新项目里写。
  • 新代码——一律用getaddrinfo(),IPv4/IPv6通吃,线程安全,一步到位。

理解DNS的递归查询流程,能帮你快速定位网络问题。下次遇到“能ping通IP但ping不通域名”的情况,你就知道该查DNS了。

个人建议:写网络程序时,把域名解析结果缓存起来,设置合理的过期时间。既保证实时性,又避免频繁网络请求。


公众号:蓝海资料掘金营,微信deep3321