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解析知识体系 应用层DNS解析 gethostbyname() getaddrinfo() gethostbyname() 特性 • 仅IPv4 • 线程不安全(静态缓冲区) • 过时API,不推荐新项目使用 getaddrinfo() 特性 • IPv4 + IPv6 双栈支持 • 线程安全,动态分配内存 • 当前推荐标准API DNS缓存(系统级 + 应用级)

嗯,这里要注意:DNS缓存虽然能提升性能,但也会带来问题。比如你改了域名的A记录,客户端可能还在用旧的缓存IP。所以缓存时间(TTL)的设置是个权衡——太短了查询频繁,太长了更新不及时。

39.6 避坑总结

最后,把我这些年踩过的坑整理一下:

  1. 不要在生产环境用gethostbyname()——线程不安全,高并发下必出问题
  2. getaddrinfo()的hints结构体一定要清零——我见过有人忘记memset,结果ai_flags里残留垃圾值,解析结果完全不对
  3. 记得调用freeaddrinfo()——每次解析都会分配内存,不释放就是泄漏
  4. DNS解析可能阻塞——默认是同步阻塞的,如果在事件循环里调用,会卡住整个线程。可以考虑用异步DNS库或者单独线程处理
  5. 处理超时和重试——DNS服务器可能挂掉,或者网络抖动。我一般设置3秒超时,重试2次

一句话总结:

新项目一律用getaddrinfo(),老项目如果还在用gethostbyname(),尽快迁移。应用层自己做DNS缓存,别全指望系统。记住这三点,DNS解析这块基本不会出大问题。


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