39、DNS解析:gethostbyname()、getaddrinfo()、DNS缓存
做网络编程,DNS解析是绕不开的一关。
说白了,就是把域名变成IP地址。你写代码时用的是"www.example.com",但操作系统和网卡只认"93.184.216.34"这种数字。这个转换过程,就是DNS解析。
我刚开始写网络程序时,觉得这步很简单——不就是查个表吗?后来踩了坑才明白,这里面门道不少。今天咱们就把它彻底讲透。
39.1 老牌函数:gethostbyname()
这个函数是BSD Socket时代的产物,历史悠久。我最早学网络编程时,教材上清一色用的都是它。
#include <netdb.h>
#include <sys/socket.h>
struct hostent *gethostbyname(const char *name);
// 返回的hostent结构体
struct hostent {
char *h_name; // 官方主机名
char **h_aliases; // 别名列表
int h_addrtype; // 地址类型(AF_INET)
int h_length; // 地址长度(4字节)
char **h_addr_list; // IP地址列表(网络字节序)
};
用法很简单,传个域名进去,返回一个结构体指针。里面装着IP地址列表。
核心要点:
- 只支持IPv4(AF_INET)
- 返回的地址是网络字节序(大端)
- h_addr_list是个指针数组,每个元素指向一个IP地址
- 函数内部使用静态缓冲区,不可重入
⚠ 避坑指南:
我曾经在一个高并发服务器里用了gethostbyname(),结果线上频繁崩溃。查了半天才发现——这个函数不是线程安全的!多个线程同时调用,内部静态缓冲区会被覆盖。后来我全部换成了getaddrinfo()才解决问题。
39.2 现代方案:getaddrinfo()
getaddrinfo()是POSIX标准推荐的现代API。它解决了gethostbyname()的诸多痛点。
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
int getaddrinfo(const char *node, // 域名或IP字符串
const char *service, // 端口号或服务名
const struct addrinfo *hints,
struct addrinfo **res);
// 返回的addrinfo链表
struct addrinfo {
int ai_flags; // 标志位
int ai_family; // AF_INET / AF_INET6 / AF_UNSPEC
int ai_socktype; // SOCK_STREAM / SOCK_DGRAM
int ai_protocol; // IPPROTO_TCP / IPPROTO_UDP
socklen_t ai_addrlen; // socket地址长度
struct sockaddr *ai_addr; // socket地址结构体
char *ai_canonname; // 规范主机名
struct addrinfo *ai_next; // 下一个节点(链表)
};
我个人习惯用getaddrinfo(),原因有三:
- 协议无关:同时支持IPv4和IPv6,设置ai_family为AF_UNSPEC即可
- 线程安全:每次调用都分配新的内存,不存在静态缓冲区问题
- 返回链表:一个域名可能对应多个IP,它用链表串起来,方便遍历
💡 使用技巧:
调用完getaddrinfo()后,记得用freeaddrinfo()释放内存。我见过不少新手忘了这步,导致内存泄漏。
39.3 实战对比:两个函数的代码示例
咱们直接上代码,看看实际用起来有什么区别。
gethostbyname() 示例:
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
void resolve_gethostbyname(const char *domain) {
struct hostent *host = gethostbyname(domain);
if (!host) {
printf("解析失败: %s\n", hstrerror(h_errno));
return;
}
printf("官方名称: %s\n", host->h_name);
// 遍历所有IP地址
for (int i = 0; host->h_addr_list[i] != NULL; i++) {
struct in_addr addr;
addr.s_addr = *(uint32_t *)host->h_addr_list[i];
printf("IP[%d]: %s\n", i, inet_ntoa(addr));
}
}
getaddrinfo() 示例:
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
void resolve_getaddrinfo(const char *domain) {
struct addrinfo hints, *res, *p;
char ipstr[INET6_ADDRSTRLEN];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // 同时支持IPv4和IPv6
hints.ai_socktype = SOCK_STREAM;
int status = getaddrinfo(domain, NULL, &hints, &res);
if (status != 0) {
printf("解析失败: %s\n", gai_strerror(status));
return;
}
printf("解析结果:\n");
for (p = res; p != NULL; p = p->ai_next) {
void *addr;
if (p->ai_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
} else {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
}
inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));
printf(" %s\n", ipstr);
}
freeaddrinfo(res); // 别忘了释放!
}
关键差异总结:
| 对比项 | gethostbyname() | getaddrinfo() |
|---|---|---|
| 协议支持 | 仅IPv4 | IPv4 + IPv6 |
| 线程安全 | 否 | 是 |
| 内存管理 | 静态缓冲区 | 动态分配,需手动释放 |
| 返回结构 | 固定数组 | 链表,可遍历 |
| 错误处理 | h_errno + hstrerror() | 返回值 + gai_strerror() |
| 标准 | 过时(POSIX标记为遗留) | 当前推荐标准 |
39.4 DNS缓存:你绕不开的性能问题
每次调用DNS解析函数,底层都会发起网络请求去查询DNS服务器。如果频繁查询同一个域名,效率极低。
操作系统本身有DNS缓存。在Linux上,nscd(Name Service Cache Daemon)会缓存解析结果。Windows也有类似的DNS Client服务。
但作为开发者,我们不能完全依赖系统缓存。为什么呢?
- 缓存时间不可控:系统缓存TTL(生存时间)由DNS记录决定,可能很短
- 进程级缓存缺失:如果你的程序频繁解析同一个域名,每次都要经过系统调用
- 高并发场景:大量并发请求同时解析,可能打爆DNS服务器
💡 我的实践建议:
在应用层自己做DNS缓存。我一般用哈希表+LRU淘汰策略。缓存时间设为60秒,既保证时效性,又减少DNS查询次数。遇到解析失败时,可以适当延长缓存时间,避免雪崩效应。
39.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你想想看,DNS解析其实就这三件事:
嗯,这里要注意:DNS缓存虽然能提升性能,但也会带来问题。比如你改了域名的A记录,客户端可能还在用旧的缓存IP。所以缓存时间(TTL)的设置是个权衡——太短了查询频繁,太长了更新不及时。
39.6 避坑总结
最后,把我这些年踩过的坑整理一下:
- 不要在生产环境用gethostbyname()——线程不安全,高并发下必出问题
- getaddrinfo()的hints结构体一定要清零——我见过有人忘记memset,结果ai_flags里残留垃圾值,解析结果完全不对
- 记得调用freeaddrinfo()——每次解析都会分配内存,不释放就是泄漏
- DNS解析可能阻塞——默认是同步阻塞的,如果在事件循环里调用,会卡住整个线程。可以考虑用异步DNS库或者单独线程处理
- 处理超时和重试——DNS服务器可能挂掉,或者网络抖动。我一般设置3秒超时,重试2次
一句话总结:
新项目一律用getaddrinfo(),老项目如果还在用gethostbyname(),尽快迁移。应用层自己做DNS缓存,别全指望系统。记住这三点,DNS解析这块基本不会出大问题。
公众号:蓝海资料掘金营,微信deep3321