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", ...)时,系统会做以下几件事:
- 查本地缓存——先看看/etc/hosts文件或者本地DNS缓存里有没有记录。有就直接返回,省得去网络问。
- 问本地DNS服务器——缓存没有,就发请求到配置的DNS服务器(比如8.8.8.8)。
- 递归查询——本地DNS服务器如果也不知道,它会替你去问根服务器、顶级域服务器、权威服务器,一层层往下查。
- 返回结果——查到IP后,原路返回,并且本地DNS服务器会缓存一段时间(TTL)。
为什么会设计成递归查询?因为客户端不想管那么多。你想想看,如果每个客户端都要自己去遍历DNS树,那网络得乱成什么样。
下面这张图,把整个流程串起来了:
17.6 避坑指南
我曾经在一个高并发服务里,每来一个请求就调一次getaddrinfo()。结果压测的时候,性能直接崩了。
为什么?因为每次解析都要走网络,慢啊。后来我加了一层DNS缓存,把解析结果存起来,TTL时间内直接复用,性能一下就上来了。
注意:不要频繁调用DNS解析函数。能缓存就缓存。特别是getaddrinfo(),它内部可能阻塞等待网络响应。
还有一点,gethostbyname()返回的错误信息用herror()打印,而getaddrinfo()用gai_strerror()。别搞混了,不然查错的时候一脸懵。
17.7 小结
域名解析这块,说白了就两件事:
- 老代码——看到
gethostbyname(),知道它怎么用,但别在新项目里写。 - 新代码——一律用
getaddrinfo(),IPv4/IPv6通吃,线程安全,一步到位。
理解DNS的递归查询流程,能帮你快速定位网络问题。下次遇到“能ping通IP但ping不通域名”的情况,你就知道该查DNS了。
个人建议:写网络程序时,把域名解析结果缓存起来,设置合理的过期时间。既保证实时性,又避免频繁网络请求。
公众号:蓝海资料掘金营,微信deep3321