十年前我刚接触Linux网络编程时,教科书和网络教程都在教gethostbyname这个函数。它简单直接,几行代码就能把域名转换成IP地址,就像下面这样:
c复制struct hostent *h = gethostbyname("www.example.com");
printf("IP: %s\n", inet_ntoa(*(struct in_addr*)h->h_addr));
但随着项目经验增多,我逐渐发现这个"经典"函数在现代网络环境中越来越力不从心。最典型的一次是在2016年,当时我们需要让服务同时支持IPv4和IPv6,结果发现gethostbyname根本处理不了IPv6地址。更糟的是,当多个线程同时调用这个函数时,程序会随机崩溃——因为它内部使用静态缓冲区存储结果,完全不是线程安全的。
这些问题背后,其实是网络协议栈的演进带来的必然变革。gethostbyname设计于上世纪80年代,那时:
而现代网络环境要求我们:
这就是getaddrinfo诞生的背景。作为gethostbyname的替代者,它从设计之初就考虑了这些现代需求。下面我们通过一个真实案例对比:假设要连接www.google.com,旧方法需要先调用gethostbyname获取IP,再手动创建socket。而新方法只需要一个getaddrinfo调用,就能同时解决地址查询和socket准备两个问题。
在维护一个老旧网络库时,我遇到过这样的报错:"Name or service not known"。调试发现当用户传递IPv6地址时,gethostbyname直接返回失败。查看源码发现它的hostent结构体设计决定了这个缺陷:
c复制struct hostent {
char *h_name; // 官方主机名
char **h_aliases; // 别名列表
int h_addrtype; // 地址类型(只能是AF_INET)
int h_length; // 地址长度(永远是4字节)
char **h_addr_list; // IPv4地址数组
};
关键问题在于h_addrtype被硬编码为AF_INET。即使你的系统支持IPv6,这个函数也无法返回IPv6地址。我曾尝试用以下workaround:
c复制// 先尝试IPv6解析
struct hostent *h = gethostbyname2(name, AF_INET6);
if (!h) h = gethostbyname(name); // 回退到IPv4
但这样不仅代码臃肿,还存在竞态条件。更讽刺的是,gethostbyname2也不是所有平台都支持。
去年帮朋友调试一个高并发DNS查询服务时,我们观察到约0.1%的请求会返回错误地址。最终定位到是gethostbyname的静态缓冲区问题。看这个例子:
c复制// 线程1:
struct hostent *h1 = gethostbyname("www.baidu.com");
// 线程2(几乎同时执行):
struct hostent *h2 = gethostbyname("www.google.com");
// 此时h1和h2可能指向相同的内存!
这是因为gethostbyname内部使用静态存储返回结果,连续调用会覆盖之前的值。我曾用互斥锁封装这个函数:
c复制pthread_mutex_t gethostbyname_lock = PTHREAD_MUTEX_INITIALIZER;
struct hostent *threadsafe_gethostbyname(const char *name) {
pthread_mutex_lock(&gethostbyname_lock);
struct hostent *ret = gethostbyname(name);
pthread_mutex_unlock(&gethostbyname_lock);
return ret;
}
虽然解决了线程安全问题,但性能直线下降——所有DNS查询变成串行执行。在需要每秒处理上千查询的场景下,这种方案根本不可行。
现代DNS系统早已不只是简单的域名到IP映射。比如需要:
但gethostbyname只能返回IP地址,其他信息全部丢失。我曾参与一个需要根据SRV记录发现微服务端口的项目,最终不得不引入第三方库,因为标准接口根本不支持这些功能。
第一次用getaddrinfo实现双栈支持时,我被它的简洁性震惊了。看这个例子:
c复制struct addrinfo hints = {
.ai_family = AF_UNSPEC, // 同时接受IPv4/IPv6
.ai_socktype = SOCK_STREAM
};
struct addrinfo *result;
getaddrinfo("www.example.com", "http", &hints, &result);
// 遍历所有可用地址
for (struct addrinfo *p = result; p; p = p->ai_next) {
char ipstr[INET6_ADDRSTRLEN];
inet_ntop(p->ai_family,
p->ai_addr->sa_family == AF_INET6
? (void*)&((struct sockaddr_in6*)p->ai_addr)->sin6_addr
: (void*)&((struct sockaddr_in*)p->ai_addr)->sin_addr,
ipstr, sizeof(ipstr));
printf("%s\n", ipstr);
}
关键优势在于:
与gethostbyname不同,getaddrinfo每次调用都分配新内存,通过返回值传递结果。这意味着:
实际项目中,我习惯用RAII模式封装:
c复制void cleanup_addrinfo(struct addrinfo **ai) {
if (*ai) freeaddrinfo(*ai);
}
#define SCOPED_ADDRINFO __attribute__((cleanup(cleanup_addrinfo))) struct addrinfo*
使用时:
c复制void query_dns() {
SCOPED_ADDRINFO res = NULL;
getaddrinfo(..., &res);
// 退出作用域时自动释放
}
这种模式彻底解决了内存泄漏问题,特别适合异常处理复杂的场景。
getaddrinfo的hints参数提供了丰富的控制选项:
c复制struct addrinfo hints = {
.ai_flags = AI_CANONNAME | AI_V4MAPPED, // 获取规范名 + 将IPv6映射为IPv4
.ai_family = AF_INET6, // 优先IPv6
.ai_socktype = SOCK_STREAM, // TCP连接
.ai_protocol = IPPROTO_TCP // 明确指定TCP
};
我曾用这些特性优化过海外节点的连接策略:
假设我们要获取www.example.com的IP并建立连接,旧代码可能是:
c复制// 旧方案(gethostbyname)
struct hostent *he = gethostbyname("www.example.com");
if (!he) error();
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(80),
.sin_addr = *(struct in_addr*)he->h_addr
};
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
对应的新方案:
c复制// 新方案(getaddrinfo)
struct addrinfo hints = {
.ai_family = AF_UNSPEC,
.ai_socktype = SOCK_STREAM
};
struct addrinfo *res;
getaddrinfo("www.example.com", "80", &hints, &res);
// 自动尝试所有可用地址
for (struct addrinfo *p = res; p; p = p->ai_next) {
int sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (connect(sock, p->ai_addr, p->ai_addrlen) == 0) {
// 连接成功
break;
}
close(sock); // 尝试下一个地址
}
freeaddrinfo(res);
新代码的优势显而易见:
在改造遗留系统时,我总结出这些经验:
条件编译方案:
c复制#if defined(USE_GETADDRINFO)
// 现代代码路径
#else
// 兼容旧系统的回退路径
#endif
渐进式迁移策略:
性能优化点:
通过strace工具观察getaddrinfo的调用链,可以发现它比gethostbyname复杂得多:
名称解析阶段:
服务解析阶段:
结果过滤与排序:
我曾遇到一个案例:在Kubernetes环境中,getaddrinfo优先返回IPv6地址导致连接超时。解决方案是通过/etc/gai.conf调整地址排序策略。
与gethostbyname不同,getaddrinfo使用返回值而非全局变量报告错误。正确处理方式:
c复制int ret = getaddrinfo(..., &res);
if (ret != 0) {
fprintf(stderr, "Error: %s\n", gai_strerror(ret));
// EAI_NONAME: 名称不存在
// EAI_AGAIN: 临时故障
// EAI_FAIL: 不可恢复失败
}
特别要注意EAI_AGAIN这种情况,合理的重试策略应该是:
服务发现实现:
c复制struct addrinfo hints = {
.ai_flags = AI_PASSIVE, // 用于bind
.ai_socktype = SOCK_STREAM
};
// 自动发现可用端口
getaddrinfo(NULL, "0", &hints, &res);
协议优先配置:
c复制// 优先IPv6,但兼容IPv4
hints.ai_family = AF_INET6;
hints.ai_flags = AI_V4MAPPED;
在多宿主主机(Multi-homed)环境中,还可以通过绑定特定源地址:
c复制hints.ai_flags |= AI_PASSIVE;
hints.ai_addr = &my_source_addr;
在我的测试环境(Ubuntu 22.04,Intel i7)中,对localhost进行10000次查询:
| 函数 | 耗时(ms) | 线程安全 | 内存使用 |
|---|---|---|---|
| gethostbyname | 125 | 否 | 低 |
| getaddrinfo | 210 | 是 | 中 |
| getaddrinfo+缓存 | 45 | 是 | 高 |
虽然原生getaddrinfo较慢,但引入缓存后反而更快。推荐使用libevent的evdns或c-ares这类异步DNS库。
常见的错误用法:
c复制struct addrinfo *res;
getaddrinfo(..., &res);
// 使用res...
free(res); // 错误!应该用freeaddrinfo
正确的内存管理模式:
使用dig命令对比结果:
bash复制dig www.example.com A # IPv4查询
dig www.example.com AAAA # IPv6查询
在代码中打印完整addrinfo链:
c复制void print_addrinfo(struct addrinfo *ai) {
for (; ai; ai = ai->ai_next) {
char host[NI_MAXHOST], port[NI_MAXSERV];
getnameinfo(ai->ai_addr, ai->ai_addrlen,
host, sizeof(host),
port, sizeof(port),
NI_NUMERICHOST | NI_NUMERICSERV);
printf("%s:%s (family=%d, socktype=%d)\n",
host, port, ai->ai_family, ai->ai_socktype);
}
}