名字与地址转换:gethostbyname()、getaddrinfo()、inet_pton()与inet_ntop()

做网络编程,说白了就是在跟「地址」打交道。你写代码时用的是 www.example.com 这样的域名,可内核只认 192.168.1.1 这种二进制地址。这中间怎么转?转的时候要注意什么?今天我们就来聊聊这几个核心函数。

我个人习惯把这类函数分成两组:一组是「名字 ↔ 地址」的转换,另一组是「文本 ↔ 二进制」的转换。前者解决的是人类可读与机器可读之间的鸿沟,后者解决的是字符串与结构体之间的鸿沟。你想想看,这两件事其实贯穿了整个网络编程的始终。

gethostbyname():老派但还在用

这个函数我刚开始学网络编程时就接触了。它的作用很简单:给你一个域名,返回一个 struct hostent,里面装着 IP 地址列表。

#include <netdb.h>
struct hostent *gethostbyname(const char *name);

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

嗯,这里要注意:h_addr_list 里存的是二进制地址,不是字符串。你得用 inet_ntoa() 转成点分十进制才能打印出来。

核心要点:gethostbyname() 只支持 IPv4。它不可重入,内部使用了静态缓冲区。多线程环境下要小心。

我在项目中遇到过一个问题:用 gethostbyname() 查一个不存在的域名,返回的是 NULL,但 h_errno 里并没有明确的错误码。后来排查发现,是 DNS 服务器超时了。所以我的建议是:能用 getaddrinfo() 就别用这个了。

getaddrinfo():现代网络编程的基石

这个函数是我现在的主力。它把 gethostbyname()getservbyname() 的功能合二为一,而且同时支持 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);  // 结果链表

void freeaddrinfo(struct addrinfo *res); // 释放内存

struct addrinfo {
    int              ai_flags;     // AI_PASSIVE, AI_CANONNAME 等
    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;      // 下一个节点
};

你看这个结构体,它把地址、协议、端口全打包在一起了。拿到 addrinfo 链表后,你直接遍历,找到第一个能用的就创建 socket 去连接,非常方便。

个人经验:我写客户端程序时,hints.ai_family 通常设成 AF_UNSPEC,让系统自动选择 IPv4 或 IPv6。但服务器端我建议明确指定 AF_INETAF_INET6,避免出现 bind 失败的问题。

我曾经踩过一个坑:getaddrinfo() 返回成功,但 res 链表里的地址全是 IPv6 的。我的代码只处理了 IPv4,结果连接全部失败。从那以后,我遍历链表时都会检查 ai_family

inet_pton() 与 inet_ntop():文本与二进制的桥梁

这两个函数是 inet_aton()inet_ntoa() 的升级版。它们支持 IPv6,而且接口更安全。

#include <arpa/inet.h>

// 文本 → 二进制(presentation to network)
int inet_pton(int af, const char *src, void *dst);
// 返回:1 成功,0 输入无效,-1 出错

// 二进制 → 文本(network to presentation)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// 返回:成功返回 dst 指针,失败返回 NULL

af 参数只能是 AF_INETAF_INET6dst 缓冲区的大小,IPv4 用 INET_ADDRSTRLEN(16字节),IPv6 用 INET6_ADDRSTRLEN(46字节)。

注意inet_ntop() 的最后一个参数 size 一定要传对。传小了,函数会返回 NULL 并设置 errnoENOSPC。我见过有人传 sizeof(char*) 而不是 sizeof(buffer),结果调试了一整天。

举个例子,把点分十进制转成 struct in_addr

struct in_addr addr;
if (inet_pton(AF_INET, "192.168.1.1", &addr) != 1) {
    perror("inet_pton");
    exit(1);
}
// 现在 addr.s_addr 就是网络字节序的二进制地址

反过来,把二进制地址打印成字符串:

char buf[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &addr, buf, sizeof(buf)) == NULL) {
    perror("inet_ntop");
    exit(1);
}
printf("IP: %s\n", buf);

知识体系总览

下面这张图把今天讲的四个函数串起来了。你一看就明白它们各自负责什么环节。

名字与地址转换函数体系 人类视角 域名 / 服务名 机器视角 struct sockaddr / 二进制IP 转换层 名字 ↔ 地址 gethostbyname() 域名 → struct hostent 仅 IPv4 · 不可重入 已过时,建议迁移 getaddrinfo() 域名+服务 → addrinfo链表 IPv4+IPv6 · 可重入 现代首选方案 inet_pton() 文本IP → 二进制结构体 IPv4+IPv6 · 安全 替代 inet_aton() inet_ntop() 二进制结构体 → 文本IP IPv4+IPv6 · 安全 替代 inet_ntoa() 名字 ↔ 地址转换 名字 ↔ 地址转换 文本 ↔ 二进制转换 文本 ↔ 二进制转换

实战对比:新老函数怎么选

场景 老函数 新函数 我的建议
域名解析 gethostbyname() getaddrinfo() 用新的,除非你在维护古董代码
服务名转端口 getservbyname() getaddrinfo() getaddrinfo 一步到位
文本IP转二进制 inet_aton() / inet_addr() inet_pton() inet_pton 支持 IPv6,更安全
二进制转文本IP inet_ntoa() inet_ntop() inet_ntop 线程安全,缓冲区可控

避坑指南:我曾经在代码里混用了 inet_ntoa()inet_ntop(),结果在打印 IPv6 地址时程序崩溃了。后来我统一改用 inet_ntop(),再也没出过问题。记住:统一接口,减少心智负担。

好了,今天的内容就到这里。这四个函数是网络编程的入门必修课。你写代码时,优先用 getaddrinfo() 做名字解析,用 inet_pton()inet_ntop() 做地址转换。这套组合拳打下来,你的代码既支持 IPv4 也支持 IPv6,而且线程安全,何乐而不为呢?

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