名字与地址转换: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_INET 或 AF_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_INET 或 AF_INET6。dst 缓冲区的大小,IPv4 用 INET_ADDRSTRLEN(16字节),IPv6 用 INET6_ADDRSTRLEN(46字节)。
注意:inet_ntop() 的最后一个参数 size 一定要传对。传小了,函数会返回 NULL 并设置 errno 为 ENOSPC。我见过有人传 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);
知识体系总览
下面这张图把今天讲的四个函数串起来了。你一看就明白它们各自负责什么环节。
实战对比:新老函数怎么选
| 场景 | 老函数 | 新函数 | 我的建议 |
|---|---|---|---|
| 域名解析 | 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,而且线程安全,何乐而不为呢?