getaddrinfo函数在C语言中如何正确使用?

99ANYc3cd6
预计阅读时长 33 分钟
位置: 首页 C语言 正文

为什么需要 getaddrinfo

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

C语言getaddrinfo
(图片来源网络,侵删)
  • 线程不安全gethostbyname() 使用静态的内部缓冲区来存储结果,如果多个线程同时调用它,会导致数据竞争和数据错乱。
  • 仅支持 IPv4gethostbyname() 主要设计用于 IPv4,对 IPv6 的支持非常有限且不统一。
  • 接口复杂:需要手动处理不同类型的地址结构(struct in_addr for IPv4, struct in6_addr for 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 结构体详解

hintsgetaddrinfo 功能强大的关键,通过设置它的字段,我们可以过滤掉不符合我们要求的结果。

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 初始化:

C语言getaddrinfo
(图片来源网络,侵删)
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 结构体和链表遍历),但它带来的好处(协议无关性、线程安全、强大功能)使其成为编写健壮、可移植网络应用程序的唯一正确选择。

-- 展开阅读全文 --
头像
织梦生成路径为何会跑到后台?
« 上一篇 01-30
Linux系统下DEDE权限如何正确设置?
下一篇 » 01-30

相关文章

取消
微信二维码
支付宝二维码

目录[+]