在网络编程中,地址解析是一个绕不开的话题。想象一下你要给朋友寄信,首先得知道对方的详细地址。在网络世界里,getaddrinfo 就是这样一个帮你查找"网络地址"的得力助手。
这个函数最厉害的地方在于它能同时处理 IPv4 和 IPv6 地址,就像是一个精通多国语言的翻译官。传统的 gethostbyname 和 gethostbyaddr 函数只能处理 IPv4,在当今这个 IPv4 和 IPv6 并存的时代显然不够用了。
getaddrinfo 的函数原型是这样的:
c复制#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
四个参数各司其职:
我曾经在一个项目中犯过一个典型错误:没有正确初始化 hints 结构体,结果程序在不同平台上表现不一致。后来才明白,必须先用 memset 清零 hints,然后再设置需要的字段。
getaddrinfo 返回的结果是一个 addrinfo 结构体链表,这个结构体包含了我们需要的所有地址信息。它的定义如下:
c复制struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
每个字段都有其特殊用途:
在实际项目中,我经常需要遍历这个链表来选择合适的地址。比如在一个双栈(同时支持IPv4和IPv6)服务器上,我会优先选择IPv6地址,同时保留IPv4地址作为备选。
现在让我们动手实现一个能适应各种网络环境的服务端。关键点在于正确设置 hints 参数和错误处理。
下面是一个典型的服务端初始化代码框架:
c复制struct addrinfo hints, *servinfo, *p;
int sockfd, yes=1;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // IPv4或IPv6都可以
hints.ai_socktype = SOCK_STREAM; // TCP套接字
hints.ai_flags = AI_PASSIVE; // 自动填充本机IP
int rv = getaddrinfo(NULL, "8080", &hints, &servinfo);
if (rv != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
exit(1);
}
这里有几个要点需要注意:
获取地址列表后,我们需要遍历并尝试创建套接字:
c复制for(p = servinfo; p != NULL; p = p->ai_next) {
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) {
perror("socket");
continue;
}
// 避免"address already in use"错误
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
close(sockfd);
perror("bind");
continue;
}
break; // 成功则跳出循环
}
if (p == NULL) {
fprintf(stderr, "failed to bind\n");
exit(2);
}
freeaddrinfo(servinfo); // 释放地址信息
这段代码展示了几个最佳实践:
在多网卡服务器上,我们可能需要更精细地控制地址选择。这时可以使用 AI_ADDRCONFIG 标志:
c复制hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
AI_ADDRCONFIG 会确保返回的地址类型与本地配置的网络接口相匹配。比如,如果本地没有配置IPv6地址,就不会返回IPv6地址。
在大并发场景下,DNS解析可能成为性能瓶颈。我曾在项目中遇到因为DNS查询超时而导致的连接延迟问题。解决方案包括:
健壮的错误处理是网络编程的关键。对于 getaddrinfo,应该:
c复制int rv = getaddrinfo(hostname, port, &hints, &res);
if (rv != 0) {
if (rv == EAI_AGAIN) {
// 临时错误,可以重试
fprintf(stderr, "Temporary failure in name resolution\n");
} else {
// 永久性错误
fprintf(stderr, "Cannot resolve hostname: %s\n", gai_strerror(rv));
}
return -1;
}
最后,让我们看一个完整的回声服务器示例,它同时支持IPv4和IPv6:
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT "8080"
#define BACKLOG 10
#define MAXDATASIZE 100
void *get_in_addr(struct sockaddr *sa) {
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void) {
struct addrinfo hints, *servinfo, *p;
int sockfd, new_fd;
char s[INET6_ADDRSTRLEN];
int rv;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
// 遍历所有结果并绑定到第一个可用的
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("socket");
continue;
}
int yes = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
close(sockfd);
perror("bind");
continue;
}
break;
}
if (p == NULL) {
fprintf(stderr, "failed to bind\n");
return 2;
}
freeaddrinfo(servinfo);
if (listen(sockfd, BACKLOG) == -1) {
perror("listen");
exit(1);
}
printf("server: waiting for connections...\n");
while(1) {
struct sockaddr_storage their_addr;
socklen_t sin_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1) {
perror("accept");
continue;
}
inet_ntop(their_addr.ss_family,
get_in_addr((struct sockaddr *)&their_addr),
s, sizeof s);
printf("server: got connection from %s\n", s);
char buf[MAXDATASIZE];
int numbytes = recv(new_fd, buf, MAXDATASIZE-1, 0);
if (numbytes == -1) {
perror("recv");
close(new_fd);
continue;
}
buf[numbytes] = '\0';
printf("server: received '%s'\n", buf);
if (send(new_fd, buf, numbytes, 0) == -1) {
perror("send");
}
close(new_fd);
}
return 0;
}
这个示例展示了如何:
在实际部署时,你可能还需要考虑添加信号处理、日志记录、连接超时等功能来进一步增强健壮性。