为什么需要 getaddrinfo?
在 getaddrinfo 出现之前,程序员通常使用一套古老的函数,如 gethostbyname() 和 getservbyname(),这套方法存在很多问题:

(图片来源网络,侵删)
- 线程不安全:
gethostbyname()使用静态的内部缓冲区来存储结果,如果多个线程同时调用它,会导致数据竞争和数据错乱。 - 仅支持 IPv4:
gethostbyname()主要设计用于 IPv4,对 IPv6 的支持非常有限且不统一。 - 接口复杂:需要手动处理不同类型的地址结构(
struct in_addrfor IPv4,struct in6_addrfor IPv6),代码冗长且容易出错。 - 协议和地址族耦合:它隐式地假设使用 IPv4。
getaddrinfo 就是为了解决以上所有问题而设计的现代替代方案,它的核心优势是:
- 协议无关:它可以同时处理 IPv4 和 IPv6 地址。
- 线程安全:它通过调用者提供的结构体来返回结果,而不是使用静态缓冲区。
- 接口统一:将主机名、服务名(如 "http" 或 "80")和地址族信息统一到一个函数调用中,大大简化了代码。
- 功能强大:可以处理主机名、服务名、IP地址字符串等多种输入,并返回一个链表,方便程序遍历所有可能的地址组合。
函数原型
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // 节点名(主机名或IP地址)
const char *service, // 服务名(如 "http")或端口号(如 "80")
const struct addrinfo *hints, // 提供提示的过滤结构体
struct addrinfo **res); // 指向结果链表的指针
- 返回值:
- 成功时返回
0。 - 失败时返回一个非零的错误码,可以使用
gai_strerror()函数将错误码转换为可读的字符串。
- 成功时返回
- 参数:
node: 可以是主机名(如"www.google.com"),也可以是 IP 地址的字符串形式(如"8.8.8.8"或"2001:4860:4860::8888")。service: 可以是服务名(如"http","ftp","ssh"),也可以是十进制的端口号字符串(如"80","22")。hints: 一个struct addrinfo类型的指针,用于告诉getaddrinfo我们期望什么样的结果,如果不需要过滤,可以传递NULL。res: 一个指向struct addrinfo指针的指针。getaddrinfo会在其中动态分配一个链表,链表的每个节点都包含一个可能的地址信息。调用者必须负责在用完后释放这个链表。
hints 结构体详解
hints 是 getaddrinfo 功能强大的关键,通过设置它的字段,我们可以过滤掉不符合我们要求的结果。
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
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; // 地址长度
char *ai_canonname;// 规范主机名
struct sockaddr *ai_addr; // 指向地址结构体的指针
struct addrinfo *ai_next; // 指向链表中下一个节点的指针
};
最常用的 hints 字段:
ai_family: 地址族。AF_INET: 只获取 IPv4 地址。AF_INET6: 只获取 IPv6 地址。AF_UNSPEC: 获取 IPv4 和 IPv6 地址(推荐,让程序更具适应性)。
ai_socktype: 套接字类型。SOCK_STREAM: 流式套接字(用于 TCP)。SOCK_DGRAM: 数据报套接字(用于 UDP)。0: 获取任何类型的套接字。
ai_flags: 控制标志位。AI_PASSIVE: 如果设置,返回的地址将用于bind()作为监听套接字。node参数可以传NULL,表示绑定到所有可用的网络接口。AI_CANONNAME: 如果设置,ai_canonname字段将包含主机的规范名称。AI_NUMERICHOST: 如果设置,node必须是一个数字地址字符串(IP地址),否则调用会失败,这可以防止getaddrinfo进行 DNS 查询,提高效率。
示例 hints 初始化:

(图片来源网络,侵删)
struct addrinfo hints; memset(&hints, 0, sizeof(hints)); // 清零,非常重要! hints.ai_family = AF_UNSPEC; // IPv4 或 IPv6 都可以 hints.ai_socktype = SOCK_STREAM; // TCP 流
如何使用 getaddrinfo(客户端示例)
下面是一个完整的客户端示例,它使用 getaddrinfo 来解析主机名并创建一个 TCP 套接字进行连接。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <hostname> <port>\n", argv[0]);
return 1;
}
const char *node = argv[1];
const char *service = argv[2]; // e.g., "80" or "http"
struct addrinfo hints;
struct addrinfo *res, *p;
int sockfd;
int status;
// 1. 准备 hints 结构体
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // 支持 IPv4 和 IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
// 2. 调用 getaddrinfo
if ((status = getaddrinfo(node, service, &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return 2;
}
printf("Address information for %s:%s\n", node, service);
// 3. 遍历结果链表,直到找到一个可以成功创建和连接的套接字
for (p = res; p != NULL; p = p->ai_next) {
// 4. 创建套接字
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) {
perror("socket");
continue; // 尝试下一个地址
}
// 5. 连接 (这里只展示连接,实际应用中可能需要处理非阻塞等)
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
perror("connect");
close(sockfd);
continue; // 尝试下一个地址
}
// 连接成功
printf("Successfully connected to %s using ", node);
if (p->ai_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
char ipstr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(ipv4->sin_addr), ipstr, sizeof(ipstr));
printf("IPv4: %s\n", ipstr);
} else { // AF_INET6
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
char ipstr[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &(ipv6->sin6_addr), ipstr, sizeof(ipstr));
printf("IPv6: %s\n", ipstr);
}
close(sockfd); // 在这个简单示例中,连接后立即关闭
break; // 成功后退出循环
}
// 6. 释放链表
freeaddrinfo(res);
if (p == NULL) {
fprintf(stderr, "Failed to connect to any address.\n");
return 3;
}
return 0;
}
编译和运行:
gcc client.c -o client ./www.google.com 80
如何使用 getaddrinfo(服务器示例)
服务器使用 getaddrinfo 的一个关键区别是设置 AI_PASSIVE 标志,并传递 NULL 作为 node 参数,以便绑定到所有可用的接口。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
struct addrinfo hints;
struct addrinfo *res, *p;
int sockfd, new_fd;
int status;
// 1. 准备 hints 结构体
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // 支持 IPv4 和 IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_flags = AI_PASSIVE; // 用于 bind,绑定到所有接口
// 2. 调用 getaddrinfo,node 传 NULL
if ((status = getaddrinfo(NULL, "3490", &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return 1;
}
printf("Setting up server on port 3490...\n");
// 3. 遍历结果链表,创建并绑定套接字
for (p = res; p != NULL; p = p->ai_next) {
// 4. 创建套接字
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) {
perror("socket");
continue;
}
// 5. 设置地址复用选项,避免 "Address already in use" 错误
int yes = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
perror("setsockopt");
close(sockfd);
continue;
}
// 6. 绑定套接字
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
perror("bind");
close(sockfd);
continue;
}
// 绑定成功
printf("Successfully bound socket.\n");
break; // 成功后退出循环
}
freeaddrinfo(res); // 绑定完成后,可以释放链表
if (p == NULL) {
fprintf(stderr, "Failed to bind to any address.\n");
return 2;
}
// 7. 监听
if (listen(sockfd, 10) == -1) {
perror("listen");
return 3;
}
printf("Server is listening for connections...\n");
// 8. 接受连接 (这是一个阻塞调用)
struct sockaddr_storage their_addr;
socklen_t addr_size = sizeof(their_addr);
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
if (new_fd == -1) {
perror("accept");
return 4;
}
// 打印客户端信息
char ipstr[INET6_ADDRSTRLEN];
if (their_addr.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&their_addr;
inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof(ipstr));
} else { // AF_INET6
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&their_addr;
inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof(ipstr));
}
printf("Accepted connection from %s\n", ipstr);
// ... 在 new_fd 上进行数据收发 ...
close(new_fd);
close(sockfd);
return 0;
}
释放资源
getaddrinfo 使用 malloc 为结果链表分配内存。必须在用完后调用 freeaddrinfo() 来释放这些内存,否则会导致内存泄漏。
struct addrinfo *res; // ... 调用 getaddrinfo ... // ... 使用结果 ... freeaddrinfo(res); // 释放!
| 特性 | gethostbyname() (旧) |
getaddrinfo() (新) |
|---|---|---|
| 地址族 | 主要 IPv4 | IPv4 和 IPv6 |
| 线程安全 | 否 (使用静态缓冲区) | 是 (动态分配) |
| 接口 | 分离 (gethostbyname, getservbyname) |
统一 |
| 输入 | 仅主机名 | 主机名、IP地址、服务名、端口号 |
| 输出 | 单个 struct hostent |
地址链表 (struct addrinfo) |
| 灵活性 | 低 | 高 (通过 hints 结构体) |
| 推荐度 | 不推荐 | 强烈推荐 |
getaddrinfo 是现代 C 网络编程的事实标准,虽然它的接口比旧函数更复杂一些(因为需要理解 hints 结构体和链表遍历),但它带来的好处(协议无关性、线程安全、强大功能)使其成为编写健壮、可移植网络应用程序的唯一正确选择。
