第13章:域名解析与地址转换——gethostbyname、getaddrinfo、inet_pton/inet_ntop、DNS解析实战

说实话,域名解析这块,我当年刚入行时觉得挺简单的——不就是把域名转成IP嘛。直到有一次线上服务出了故障,我才发现这里面水挺深。今天咱们就把这块彻底讲透。

13.1 为什么需要域名解析?

你想想看,网络通信本质上是在IP地址之间进行的。但人类记不住一串数字,比如 142.250.80.46,我们更习惯记 www.google.com。所以就需要一个「翻译官」,把域名翻译成IP地址。

这个翻译官,就是DNS(Domain Name System)。

在C语言里,我们主要用两组API来完成这个工作:

  • 老牌函数gethostbyname() —— 经典但有过时
  • 现代函数getaddrinfo() —— 推荐使用
  • 地址转换inet_pton() / inet_ntop() —— 二进制与字符串互转

核心要点:我个人建议,新项目一律用 getaddrinfo(),它更安全、更灵活,而且支持IPv6。

13.2 gethostbyname —— 老牌但别用了

这个函数我早期项目里用过不少。它的原型很简单:

#include <netdb.h>

struct hostent *gethostbyname(const char *name);

返回一个 struct hostent 结构体:

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

用起来大概这样:

#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>

int main() {
    struct hostent *host = gethostbyname("www.baidu.com");
    if (host == NULL) {
        herror("gethostbyname");
        return 1;
    }

    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地址: %s\n", inet_ntoa(addr));
    }
    return 0;
}

⚠️ 注意gethostbyname() 不支持IPv6,而且不是线程安全的。我在一个老项目中吃过这个亏——多线程环境下,返回值被覆盖了,查了半天才发现是它的问题。

13.3 getaddrinfo —— 现代推荐方案

这个函数是POSIX标准推荐的。它把域名解析和地址转换合二为一,而且支持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 是一个链表:

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;    // 地址长度
    struct sockaddr *ai_addr;       // 地址结构
    char            *ai_canonname;  // 规范名称
    struct addrinfo *ai_next;       // 下一个节点
};

来看一个完整的实战例子:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>

int main() {
    struct addrinfo hints, *res, *p;
    char ipstr[INET6_ADDRSTRLEN];

    // 清空hints结构
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;    // 支持IPv4和IPv6
    hints.ai_socktype = SOCK_STREAM; // TCP

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

    printf("域名解析结果:\n");
    for (p = res; p != NULL; p = p->ai_next) {
        void *addr;
        char *ipver;

        // 获取IP地址
        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);  // 释放内存
    return 0;
}

💡 个人经验getaddrinfo()hints 参数一定要初始化。我曾经忘了 memset,结果里面残留了垃圾数据,导致解析失败。嗯,这种低级错误犯过一次就记住了。

13.4 inet_pton 与 inet_ntop —— 地址转换利器

这两个函数负责IP地址的「二进制 ↔ 字符串」互转。说白了,就是让你能在人类可读的字符串和网络传输用的二进制之间自由切换。

函数 作用 原型
inet_pton 字符串 → 二进制(网络字节序) int inet_pton(int af, const char *src, void *dst);
inet_ntop 二进制 → 字符串 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

举个例子:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in_addr addr;
    char str[INET_ADDRSTRLEN];

    // 字符串 → 二进制
    if (inet_pton(AF_INET, "192.168.1.1", &addr) != 1) {
        perror("inet_pton");
        return 1;
    }
    printf("二进制: 0x%x\n", addr.s_addr);

    // 二进制 → 字符串
    if (inet_ntop(AF_INET, &addr, str, sizeof(str)) == NULL) {
        perror("inet_ntop");
        return 1;
    }
    printf("字符串: %s\n", str);

    return 0;
}

⚠️ 注意inet_pton 返回1表示成功,0表示输入字符串格式不对,-1表示地址族不支持。别把它当成布尔值判断!

13.5 DNS解析实战:一个完整的域名查询工具

下面我把上面学的东西整合起来,写一个完整的DNS查询工具。它支持IPv4和IPv6,还能显示别名信息。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void print_dns_result(const char *domain) {
    struct addrinfo hints, *res, *p;
    char ipstr[INET6_ADDRSTRLEN];

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    printf("正在查询: %s\n", domain);
    printf("========================\n");

    int status = getaddrinfo(domain, NULL, &hints, &res);
    if (status != 0) {
        fprintf(stderr, "查询失败: %s\n", gai_strerror(status));
        return;
    }

    int count = 0;
    for (p = res; p != NULL; p = p->ai_next) {
        void *addr;
        const char *type;

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

        inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));
        printf("[%d] %s: %s\n", ++count, type, ipstr);
    }

    if (count == 0) {
        printf("未找到任何记录\n");
    }

    freeaddrinfo(res);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "用法: %s <域名>\n", argv[0]);
        return 1;
    }

    print_dns_result(argv[1]);
    return 0;
}

编译运行:

gcc -o dns_query dns_query.c
./dns_query www.baidu.com

输出示例:

正在查询: www.baidu.com
========================
[1] IPv4: 183.232.231.172
[2] IPv4: 183.232.231.174

13.6 知识体系总览

下面这张图帮你理清本章的知识脉络:

域名解析与地址转换知识体系 用户输入域名 gethostbyname(旧) getaddrinfo(推荐) inet_ntoa(旧,不推荐) inet_pton / inet_ntop(推荐) IP地址(IPv4 / IPv6) socket 连接 / 网络通信 传统方式 现代方式

13.7 避坑指南

做网络编程这么多年,我在域名解析上踩过的坑还真不少。分享几个典型的:

  • 忘记释放内存getaddrinfo() 分配的内存必须用 freeaddrinfo() 释放。我曾经在一个循环里忘了释放,结果内存泄漏,服务跑了三天就挂了。
  • 线程安全问题gethostbyname() 内部用了静态缓冲区,多线程下会互相覆盖。换成 getaddrinfo() 就没事了。
  • IPv6缓冲区大小:用 inet_ntop() 时,缓冲区至少要用 INET6_ADDRSTRLEN(46字节),别用 INET_ADDRSTRLEN(16字节),否则IPv6地址会截断。
  • DNS超时处理getaddrinfo() 默认会阻塞,如果DNS服务器挂了,可能卡住几十秒。建议配合超时机制或异步解析。

💡 我的习惯:写网络程序时,我会把域名解析结果缓存起来,避免每次连接都去查DNS。但要注意缓存过期时间,不然域名换了IP你还不知道。

13.8 小结

域名解析是网络编程的基础功。记住几个关键点:

  • 新项目用 getaddrinfo(),别再用 gethostbyname()
  • 地址转换用 inet_pton() / inet_ntop(),支持IPv6
  • 记得释放内存、注意线程安全
  • 实际项目中要考虑DNS缓存和超时处理

把这些搞明白了,你的网络程序在域名解析这块就不会出大问题。下一章我们聊聊更深入的话题——非阻塞I/O与多路复用。


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