作者: andylin02
学习章节: 第十一章 名字与地址转换
关键词: DNS, getaddrinfo, getnameinfo, gethostbyname, 可重入, 协议无关, 服务名解析, addrinfo, freeaddrinfo


一、章节概述

1.1 本章焦点

第十一章是《UNIX网络编程》中极其关键的一章。到此为止,书中所有例子都使用数值地址表示主机(如 206.6.226.33),使用数值端口号标识服务器(如端口 13 代表 daytime 服务器)。但出于三个理由,我们应该使用名字而非数值:名字更容易记住;数值地址可以变动而名字保持不变;随着向 IPv6 过渡,数值地址变得非常长,手工键入数值地址更容易出错。

本章的核心主题是实现主机名与 IP 地址以及服务名与端口号之间的转换。早期的函数(如 gethostbynamegethostbyaddrgetservbynamegetservbyport)仅支持 IPv4,且存在线程不安全和不可重入的问题。本章的重点在于介绍协议无关的新函数 getaddrinfogetnameinfo,它们支持 IPv4 和 IPv6,为应用程序提供协议独立性。

💡 本章核心价值:读完第十一章,你将能够——

  • 理解 DNS 的基本概念和资源记录类型
  • 熟练使用 getaddrinfogetnameinfo 编写协议无关的网络程序
  • 理解可重入函数的概念,知道为什么早期函数不安全
  • 使用 tcp_connecttcp_listen 等封装函数简化编程工作

二、域名系统(DNS)

2.1 DNS 的基本概念

域名系统(DNS)主要用于主机名和 IP 地址之间的映射。主机名既可以是一个简单名字(simple name),如 solarisbsdi,也可以是一个全限定域名(FQDN,Fully Qualified Domain Name),如 solaris.unpbook.com。严格来说,FQDN 也称为绝对名字(absolute name),且必须以一个点号来结尾,但用户往往省略结尾的点号,这个点号告知 DNS 解析器该名字是全限定的,从而不必搜索解析器自己维护的可能域名列表。

DNS 中的条目称为资源记录(RR,Resource Record)。我们感兴趣的 RR 类型主要有以下五种:

RR 类型 名称 作用 示例
A IPv4 地址记录 将主机名映射为 32 位的 IPv4 地址 freebsd in A 12.106.32.254
AAAA IPv6 地址记录 将主机名映射为 128 位的 IPv6 地址 freebsd in AAAA 3ffe:b80:1f8d:1:a00:20ff:fea7:686b
PTR 指针记录 将 IP 地址反向映射为主机名 IPv4: 254.32.106.12.in-addr.arpa
MX 邮件交换记录 将主机指定为给定主机的邮件交换器 freebsd in MX 5 mail.unpbook.com
CNAME 规范名字记录 将别名映射到规范主机名 ftp in CNAME linux.unpbook.com

💡 关键理解:PTR 记录用于反向查找。对于 IPv4 地址,32 位地址的 4 个字节先反转顺序,每个字节转换成十进制 ASCII 值,最后加上 .in-addr.arpa。对于 IPv6 地址,128 位地址的 32 个四位组反转顺序,每个四位组转换成十六进制 ASCII 值,后面加上 .ip6.arpa

2.2 解析器与名字服务器

每个组织机构通常运行一个或多个名字服务器,最常见的是 BIND(Berkeley Internet Name Domain)程序。客户和服务器通过调用称为解析器的函数库中的函数接触 DNS 服务器。常见的解析器函数是 gethostbynamegethostbyaddr,前者把主机名映射为 IPv4 地址,后者执行相反的映射。

解析器代码会读取系统配置文件来确定本组织名字服务器的位置。/etc/resolv.conf 文件中通常存储了本地名字服务器主机的 IP 地址。解析器用 UDP 向本地名字服务器发送查询,如果本地名字服务器不知道答案,它会去查询其他名字服务器。如果消息太长,就会转成 TCP。

💡 DNS 的替代方法:除了 DNS,还有静态主机文件(/etc/hosts)、网络信息系统(NIS)和轻量级目录访问协议(LDAP)等替代方案。

三、IPv4 专用转换函数(已过时,了解即可)

在进入协议无关的函数之前,先简要了解早期仅支持 IPv4 的四个函数。这些函数在现代编程中应避免使用。

3.1 gethostbyname 和 gethostbyaddr

#include <netdb.h>

struct hostent *gethostbyname(const char *hostname);
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);
// 返回值:成功返回非空指针,出错返回 NULL 且设置 h_errno

hostent 结构体

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

3.2 getservbyname 和 getservbyport

#include <netdb.h>

struct servent *getservbyname(const char *servname, const char *protoname);
struct servent *getservbyport(int port, const char *protoname);
// 返回值:成功返回非空指针,出错返回 NULL

servent 结构体

struct servent {
    char  *s_name;      // 服务规范名字
    char **s_aliases;   // 服务别名列表
    int    s_port;      // 端口号(网络字节序)
    char  *s_proto;     // 使用的协议
};

⚠️ 局限性:这些函数仅支持 IPv4,且存在线程不安全的问题(使用静态数据存储结果),在多线程环境中调用可能导致数据被覆盖。

四、协议无关函数:getaddrinfo

4.1 为什么需要 getaddrinfo?

gethostbynamegethostbyaddr 这两个函数仅仅支持 IPv4,无法满足 IPv6 时代的需求。getaddrinfo 函数应运而生,它能够处理名字到地址以及服务到端口这两种转换,返回的是一个 sockaddr 结构而不是一个地址列表,这些 sockaddr 结构随后可由套接字函数直接使用。getaddrinfo 函数把协议相关性完全隐藏在这个库函数内部,应用程序只需处理由 getaddrinfo 填写的套接字地址结构。

4.2 函数原型

#include <netdb.h>

int getaddrinfo(const char *hostname, const char *service,
                const struct addrinfo *hints, struct addrinfo **result);
// 返回值:成功返回 0,出错返回非 0 错误码

4.3 addrinfo 结构体

getaddrinfo 通过 result 指针返回一个指向 addrinfo 结构链表的指针:

struct addrinfo {
    int              ai_flags;       // AI_PASSIVE, AI_CANONNAME 等
    int              ai_family;      // AF_xxx(AF_INET, AF_INET6, AF_UNSPEC)
    int              ai_socktype;    // SOCK_xxx(SOCK_STREAM, SOCK_DGRAM)
    int              ai_protocol;    // 0 或 IPPROTO_xxx
    socklen_t        ai_addrlen;     // ai_addr 的长度
    char            *ai_canonname;   // 主机的规范名字
    struct sockaddr *ai_addr;        // 指向套接字地址结构的指针
    struct addrinfo *ai_next;        // 指向链表下一个结构的指针
};

4.4 hints 参数详解

hints 参数可以是一个空指针,也可以指向一个调用者预先设计好的 addrinfo 结构,用于对返回信息类型提供线索。hints 结构中调用者可以设置的成员有 ai_flagsai_familyai_socktypeai_protocol

ai_flags 成员可用的标志值及其含义:

标志 含义
AI_PASSIVE 套接字将用于被动打开(服务器),hostname 应为 NULL
AI_CANONNAME 告知返回主机的规范名字
AI_NUMERICHOST 防止任何类型的名字到地址映射,hostname 必须是地址串
AI_NUMERICSERV 防止任何类型的名字到服务映射,service 必须是十进制端口号数串
AI_V4MAPPED 如果没有找到 IPv6 地址,返回 IPv4 映射的 IPv6 地址
AI_ALL 与 AI_V4MAPPED 一起使用时,返回所有匹配的 IPv6 和 IPv4 地址

4.5 freeaddrinfo 和 gai_strerror

getaddrinfo 返回的存储空间(包括 addrinfo 结构、ai_addr 结构和 ai_canonname 字符串)都是用 malloc 动态获取的。这些空间必须通过 freeaddrinfo 释放,避免内存泄漏。

#include <netdb.h>

void freeaddrinfo(struct addrinfo *ai);
char *gai_strerror(int error);

💡 关键理解ai 应指向 getaddrinfo 返回的第一个 addrinfo 结构。释放时,链表中的所有结构及其指向的动态存储空间都将被释放。

五、协议无关函数:getnameinfo

5.1 函数原型

getnameinfo 函数与 getaddrinfo 互补:它以一个套接字地址为参数,返回一个描述主机的字符串和一个描述服务的字符串。

#include <netdb.h>

int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen,
                char *host, socklen_t hostlen,
                char *serv, socklen_t servlen, int flags);
// 返回值:成功返回 0,出错返回非 0

参数说明

参数 说明
sockaddr 指向包含协议地址的套接字地址结构
addrlen 套接字地址结构的长度
host 存放返回主机名的缓冲区
hostlen 主机缓冲区的大小
serv 存放返回服务名的缓冲区
servlen 服务缓冲区的大小
flags 控制转换方式的标志

5.2 flags 标志

标志 含义
NI_NAMEREQD 如果无法解析主机名,返回错误
NI_DGRAM 指定服务是基于 UDP 的
NI_NUMERICHOST 强制返回数值格式的主机地址
NI_NUMERICSERV 强制返回数值格式的端口号

💡 关键理解getnameinfo 返回的主机名和服务名总是以 null 结尾的字符串。如果不需要返回某个名字,可以将对应的缓冲区大小设置为 0。

六、getaddrinfo 完整使用流程

6.1 使用 getaddrinfo 的通用模板

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_storage ss;     // 足够大的通用地址结构
    struct addrinfo hints, *res, *ressave;

    if (argc != 2)
        err_quit("usage: %s <IPaddress>", argv[0]);

    // 1. 初始化 hints 结构
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;    // 不指定协议族,接受 IPv4 或 IPv6
    hints.ai_socktype = SOCK_STREAM;// TCP 流套接字

    // 2. 调用 getaddrinfo 解析主机名
    if ((n = getaddrinfo(argv[1], "daytime", &hints, &res)) != 0)
        err_quit("getaddrinfo error: %s", gai_strerror(n));

    // 3. 保存链表头(用于后续释放)
    ressave = res;

    // 4. 遍历链表,尝试建立连接
    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;   // 创建失败,尝试下一个地址

        if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
            break;      // 连接成功

        close(sockfd);  // 连接失败,关闭套接字后继续
    } while ((res = res->ai_next) != NULL);

    if (res == NULL)
        err_quit("tcp_connect error for %s, %s", argv[1], "daytime");

    // 5. 读取并打印数据
    while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;
        fputs(recvline, stdout);
    }

    close(sockfd);
    freeaddrinfo(ressave);  // 释放动态分配的内存
    return 0;
}

6.2 使用流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                    getaddrinfo 使用完整流程                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌──────────────────────┐                                                 │
│   │ 1. 初始化 hints 结构   │                                                 │
│   │    bzero(&hints, ...) │                                                 │
│   │    hints.ai_family =  │                                                 │
│   │        AF_UNSPEC      │                                                 │
│   └──────────┬───────────┘                                                 │
│              │                                                              │
│              ▼                                                              │
│   ┌──────────────────────┐                                                 │
│   │ 2. getaddrinfo()      │                                                 │
│   │    解析主机名和服务名   │                                                 │
│   └──────────┬───────────┘                                                 │
│              │                                                              │
│              ▼                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                      addrinfo 链表                                   │   │
│   │  ┌─────────┐   ┌─────────┐   ┌─────────┐                           │   │
│   │  │ IPv4    │ → │ IPv6    │ → │ IPv4    │ → ...                      │   │
│   │  │ 地址1   │   │ 地址1   │   │ 地址2   │                           │   │
│   │  └─────────┘   └─────────┘   └─────────┘                           │   │
│   └──────────┬──────────────────────────────────────────────────────────┘   │
│              │                                                              │
│              ▼                                                              │
│   ┌──────────────────────┐                                                 │
│   │ 3. 遍历链表            │                                                 │
│   │    socket() → 创建套接字│                                                │
│   │    connect() → 尝试连接│                                                │
│   └──────────┬───────────┘                                                 │
│              │                                                              │
│              ▼                                                              │
│   ┌──────────────────────┐                                                 │
│   │ 4. 连接成功            │                                                 │
│   │    进行数据传输         │                                                │
│   └──────────┬───────────┘                                                 │
│              │                                                              │
│              ▼                                                              │
│   ┌──────────────────────┐                                                 │
│   │ 5. freeaddrinfo()     │                                                 │
│   │    释放动态内存         │                                                │
│   └──────────────────────┘                                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

七、实用封装函数

getaddrinfo 虽然功能强大,但使用起来比较繁琐。本书作者设计了多个实用封装函数来简化编程工作。

7.1 host_serv——简化 getaddrinfo 调用

#include "unp.h"

struct addrinfo *host_serv(const char *host, const char *serv,
                           int family, int socktype)
{
    int n;
    struct addrinfo hints, *res;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_CANONNAME;      // 总是返回规范名字
    hints.ai_family = family;           // AF_UNSPEC, AF_INET, AF_INET6
    hints.ai_socktype = socktype;       // 0, SOCK_STREAM, SOCK_DGRAM

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
        return NULL;

    return res;     // 调用者需要自行调用 freeaddrinfo
}

7.2 tcp_connect——TCP 客户端连接函数

#include "unp.h"

int tcp_connect(const char *host, const char *serv)
{
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
        err_quit("tcp_connect error for %s, %s: %s",
                 host, serv, gai_strerror(n));

    ressave = res;
    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;

        if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
            break;      // 连接成功

        close(sockfd);
    } while ((res = res->ai_next) != NULL);

    if (res == NULL)
        err_quit("tcp_connect error for %s, %s", host, serv);

    freeaddrinfo(ressave);
    return sockfd;
}

7.3 tcp_listen——TCP 服务器监听函数

#include "unp.h"

int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
    int listenfd, n;
    const int on = 1;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_PASSIVE;    // 服务器使用
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
        err_quit("tcp_listen error for %s, %s: %s",
                 host, serv, gai_strerror(n));

    ressave = res;
    do {
        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (listenfd < 0)
            continue;

        // 设置 SO_REUSEADDR 选项,允许端口重用
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

        if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
            break;      // 绑定成功

        close(listenfd);
    } while ((res = res->ai_next) != NULL);

    if (res == NULL)
        err_quit("tcp_listen error for %s, %s", host, serv);

    listen(listenfd, LISTENQ);

    if (addrlenp)
        *addrlenp = res->ai_addrlen;    // 返回地址结构长度

    freeaddrinfo(ressave);
    return listenfd;
}

7.4 封装函数速查表

函数 用途 适用场景
host_serv 基于主机名和服务名获取 addrinfo 通用解析
tcp_connect 创建 TCP 套接字并连接到服务器 TCP 客户端
tcp_listen 创建 TCP 套接字,绑定并监听 TCP 服务器
udp_client 创建 UDP 套接字并连接到服务器 UDP 客户端
udp_connect 创建已连接 UDP 套接字 UDP 客户端
udp_server 创建 UDP 套接字并绑定 UDP 服务器

八、可重入函数

8.1 可重入与不可重入的概念

可重入:一个函数被多个控制流(信号处理函数、多线程)同时调用时,不会产生不确定的结果。不可重入的函数通常有以下特征:使用静态数据;返回指向静态数据的指针;使用标准 I/O 函数。

8.2 常见函数的可重入性

函数 可重入性 原因
gethostbyname ❌ 不可重入 使用静态数据存储结果
gethostbyaddr ❌ 不可重入 使用静态数据存储结果
getservbyname ❌ 不可重入 使用静态数据存储结果
getservbyport ❌ 不可重入 使用静态数据存储结果
inet_ntoa ❌ 不可重入 返回指向静态缓冲区的指针
inet_ntop ✅ 可重入 由调用者提供缓冲区
inet_pton ✅ 可重入 纯函数,无静态状态
getaddrinfo ✅ 可重入 动态分配内存,无静态状态
getnameinfo ✅ 可重入 由调用者提供缓冲区

⚠️ 注意errno 虽然是进程独有(不是线程独有),在多线程环境中也存在风险。解决方案是在信号处理函数开始处保存 errno,在结束处恢复。

8.3 gethostbyname_r 等可重入版本

对于不可重入的函数,大多数系统提供了可重入版本,如 gethostbyname_rgethostbyaddr_r 等。这些函数需要调用者提供缓冲区,避免了使用静态数据的竞争问题。

int gethostbyname_r(const char *name,
                    struct hostent *ret, char *buf, size_t buflen,
                    struct hostent **result, int *h_errnop);

💡 最佳实践getaddrinfogetnameinfo 本身就是可重入的,应优先使用它们,而不是 gethostbyname_r 等函数。此外,gethostbyname 系列函数仅支持 IPv4,而 getaddrinfo 同时支持 IPv4 和 IPv6,完全不需要考虑 IPv6 兼容性问题。

九、关键图表

9.1 名字与地址转换函数对比图

┌─────────────────────────────────────────────────────────────────────────────┐
│                    名字与地址转换函数演进对比                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  早期 IPv4 专用函数(过时,不可重入)                                          │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                                                                     │   │
│  │   主机名 ────→ gethostbyname() ────→ IP 地址列表(hostent)         │   │
│  │   IP 地址 ───→ gethostbyaddr() ────→ 主机名                         │   │
│  │                                                                     │   │
│  │   服务名 ────→ getservbyname() ────→ 端口号(servent)              │   │
│  │   端口号 ────→ getservbyport() ────→ 服务名                         │   │
│  │                                                                     │   │
│  │   ⚠️ 仅支持 IPv4,线程不安全,使用静态数据                           │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│                                    │                                        │
│                                    ▼                                        │
│                                                                             │
│  现代协议无关函数(推荐)                                                     │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                                                                     │   │
│  │   [主机名] + [服务名] ────→ getaddrinfo() ────→ addrinfo 链表       │   │
│  │                              ↓                                       │   │
│  │                         socket(), bind(), connect()                  │   │
│  │                                                                     │   │
│  │   [sockaddr] ────→ getnameinfo() ────→ [主机名] + [服务名]          │   │
│  │                                                                     │   │
│  │   ✅ 支持 IPv4 + IPv6,可重入,协议无关                             │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

9.2 DNS 解析流程全图

┌─────────────────────────────────────────────────────────────────────────────┐
│                          DNS 解析流程                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   应用程序                                                                   │
│      │                                                                      │
│      │ getaddrinfo("www.example.com", "http", ...)                          │
│      ▼                                                                      │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                         解析器(Resolver)                           │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│      │                                                                      │
│      │ 读取 /etc/resolv.conf 获取 DNS 服务器地址                            │
│      ▼                                                                      │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │              本地名字服务器(Local Name Server)                      │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│      │                                                                      │
│      │ 如果本地缓存没有 → 递归/迭代查询                                      │
│      ▼                                                                      │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐                │
│   │根域名服 │ →  │顶级域名 │ →  │权威域名 │ →  │目标服务器│                │
│   │务器     │    │服务器   │    │服务器   │    │         │                │
│   │(.)      │    │(.com)   │    │(example)│    │(www)    │                │
│   └─────────┘    └─────────┘    └─────────┘    └─────────┘                │
│                                                                             │
│   返回结果:                                                                │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │ A 记录:   93.184.216.34(IPv4)                                     │   │
│   │ AAAA 记录:2606:2800:220:1:248:1893:25c8:1946(IPv6)                │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

9.3 addrinfo 链表结构图

┌─────────────────────────────────────────────────────────────────────────────┐
│                    addrinfo 链表结构示意图                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   res = getaddrinfo() 返回值                                                │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │ struct addrinfo (节点1)                                              │   │
│   │ ┌─────────────────────────────────────────────────────────────────┐ │   │
│   │ │ ai_flags = 0          │ ai_family = AF_INET6                    │ │   │
│   │ │ ai_socktype = SOCK_STREAM                                       │ │   │
│   │ │ ai_addrlen = 28       │ ai_canonname = "www.example.com"        │ │   │
│   │ │ ai_addr ─────────────→┌─────────────────────────────────────┐   │ │   │
│   │ │                        │ struct sockaddr_in6                │   │ │   │
│   │ │                        │ sin6_family = AF_INET6             │   │ │   │
│   │ │                        │ sin6_addr = 2606:2800:220:1:...    │   │ │   │
│   │ │                        │ sin6_port = htons(80)              │   │ │   │
│   │ │                        └─────────────────────────────────────┘   │ │   │
│   │ │ ai_next ────────────────────────────────────────────────────┐   │ │   │
│   │ └─────────────────────────────────────────────────────────────│─┘ │   │
│   └─────────────────────────────────────────────────────────────────│─┘   │
│                                                                     │     │
│   ┌─────────────────────────────────────────────────────────────────│─┐   │
│   │ struct addrinfo (节点2)                                        │ │   │
│   │ ┌─────────────────────────────────────────────────────────────┐ │ │   │
│   │ │ ai_family = AF_INET                                         │ │ │   │
│   │ │ ai_addr ─────────────→ struct sockaddr_in                  │ │ │   │
│   │ │                        sin_addr = 93.184.216.34             │ │ │   │
│   │ │                        sin_port = htons(80)                 │ │ │   │
│   │ │ ai_next = NULL                                              │ │ │   │
│   │ └─────────────────────────────────────────────────────────────┘ │ │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│   💡 遍历链表:使用 res->ai_next 逐个尝试,直到成功                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

十、完整示例:协议无关的 TCP 时间获取客户端

以下是一个使用 getaddrinfo 实现的协议无关 TCP 客户端,它能同时支持 IPv4 和 IPv6:

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    socklen_t salen;
    struct sockaddr *sa;
    struct addrinfo hints, *res, *ressave;

    if (argc != 2)
        err_quit("usage: %s <hostname>", argv[0]);

    // 1. 设置 hints 结构
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;        // 接受 IPv4 或 IPv6
    hints.ai_socktype = SOCK_STREAM;    // TCP 套接字

    // 2. 解析主机名和服务名(daytime 服务端口 13)
    if ((n = getaddrinfo(argv[1], "daytime", &hints, &res)) != 0)
        err_quit("getaddrinfo error: %s", gai_strerror(n));

    ressave = res;
    salen = res->ai_addrlen;
    sa = malloc(salen);
    if (sa == NULL)
        err_sys("malloc error");
    memcpy(sa, res->ai_addr, salen);

    // 3. 尝试建立连接
    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sockfd < 0)
            continue;

        if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
            break;

        close(sockfd);
    } while ((res = res->ai_next) != NULL);

    if (res == NULL)
        err_quit("tcp_connect error for %s", argv[1]);

    // 4. 打印地址信息(使用 getnameinfo 转换)
    if (getnameinfo(sa, salen, recvline, MAXLINE, NULL, 0, NI_NUMERICHOST) == 0)
        printf("Connected to %s\n", recvline);

    free(sa);
    freeaddrinfo(ressave);

    // 5. 读取并打印数据
    while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;
        fputs(recvline, stdout);
    }

    close(sockfd);
    return 0;
}

十一、常见错误与注意事项

常见错误 原因 解决方案
getaddrinfo 返回 EAI_NONAME 主机名或服务名无法解析 检查网络连接和 DNS 配置
内存泄漏 忘记调用 freeaddrinfo 在函数退出前调用 freeaddrinfo
连接失败 主机不支持该地址族 遍历 addrinfo 链表尝试所有地址
多线程环境崩溃 使用了不可重入的 gethostbyname 改用 getaddrinfo
IPv6 地址显示为 ::ffff:192.168.1.1 使用了 IPv4 映射的 IPv6 地址 检查 hints.ai_flags 设置
hints 未正确初始化 使用了未清零的 hints 结构 调用前用 bzeromemset 清零

⚠️ 重要:在服务器端使用 AI_PASSIVE 标志时,hostname 参数应设为 NULL,这样返回的地址将使用 INADDR_ANY(IPv4)或 in6addr_any(IPv6)。

十二、本章小结

12.1 核心知识点回顾

知识点 关键要点
DNS 将主机名映射为 IP 地址(A/AAAA 记录),或将 IP 地址反向映射为主机名(PTR 记录)
gethostbyname 等 仅支持 IPv4,不可重入,已过时,仅用于维护旧代码
getaddrinfo 协议无关,同时支持主机名→地址和服务名→端口转换,返回 addrinfo 链表
hints 参数 控制返回结果类型,AI_PASSIVE 用于服务器,AI_CANONNAME 用于获取规范名
freeaddrinfo 必须调用以释放 getaddrinfo 动态分配的内存
getnameinfo getaddrinfo 的反函数,将 sockaddr 转换为主机名和服务名
可重入性 getaddrinfo/getnameinfo 可重入,旧函数不可重入
封装函数 tcp_connecttcp_listenhost_serv 等简化编程

12.2 本章思维导图

第十一章 名字与地址转换
├── DNS(域名系统)
│   ├── 资源记录:A, AAAA, PTR, MX, CNAME
│   ├── 解析器:gethostbyname, gethostbyaddr
│   └── 名字服务器:BIND, /etc/resolv.conf
├── 旧式函数(IPv4专用,不可重入)
│   ├── gethostbyname / gethostbyaddr
│   ├── getservbyname / getservbyport
│   └── hostent / servent 结构
├── 协议无关函数
│   ├── getaddrinfo(名称→地址+端口)
│   │   ├── addrinfo 结构(链表)
│   │   ├── hints 参数(AI_PASSIVE, AI_CANONNAME...)
│   │   ├── freeaddrinfo(释放内存)
│   │   └── gai_strerror(错误信息)
│   └── getnameinfo(地址+端口→名称)
│       └── flags 参数(NI_NUMERICHOST, NI_NUMERICSERV...)
├── 实用封装函数
│   ├── host_serv
│   ├── tcp_connect
│   ├── tcp_listen
│   ├── udp_client / udp_connect
│   └── udp_server
└── 可重入函数
    ├── 不可重入:gethostbyname, inet_ntoa...
    ├── 可重入:getaddrinfo, getnameinfo, inet_ntop...
    └── 可重入版本:gethostbyname_r, gethostbyaddr_r...

十三、下一章预告

📌 下一篇:《UNIX网络编程》读书笔记(十二):第十二章 IPv4 与 IPv6 的互操作性

第十二章将详细讲解:

  1. IPv4 客户端与 IPv6 服务器的通信:当服务器是 IPv6 但客户端只支持 IPv4 时,如何实现互操作?
  2. IPv6 客户端与 IPv4 服务器的通信:IPv6 客户端如何连接到 IPv4 服务器?
  3. 双栈主机的地址映射机制:IPv4 映射的 IPv6 地址格式(::ffff:0:0/96
  4. IPV6_V6ONLY 套接字选项:限制套接字只处理 IPv6 请求
  5. 协议无关的编程实践:如何编写能够同时处理 IPv4 和 IPv6 客户端的服务器程序
  6. IPv6 地址测试宏IN6_IS_ADDR_V4MAPPED 等宏的用法

学习目标:学完第十二章后,你将能够——

  • 理解 IPv4 和 IPv6 之间互操作的核心机制
  • 编写能够同时支持 IPv4 和 IPv6 客户端的服务器程序
  • 使用 IPv4 映射的 IPv6 地址实现 IPv4 客户端与 IPv6 服务器的通信
  • 正确使用 IPV6_V6ONLY 套接字选项控制监听行为

敬请期待!

参考资料

  1. W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
  2. UNIX网络编程卷一 学习笔记 第十一章 名字与地址转换,博客园,https://www.cnblogs.com/gblog6/p/17518070.html
  3. UNIX网络编程(UNP) 第十一章学习笔记,CSDN,https://blog.csdn.net/a348752377/article/details/103743997
  4. Unix系统编程学习笔记 第11章:名字与地址转换,CSDN,https://blog.csdn.net/weixin_44423941/article/details/129911595
  5. UNP Chapter 11 - 高级名字与地址转换,CSDN,https://blog.csdn.net/weixin_30702413/article/details/96163047
  6. UNPv1第十一章:高级名字与地址转换,CSDN,https://blog.csdn.net/lxj1137800599/article/details/51248576
  7. UNP编程:名字与地址转换之(地址/服务解析函数),51CTO,https://blog.51cto.com/u_15346415/5171346
  8. Unix网络编程学习笔记之第11章 名字与地址转换,CSDN,https://blog.csdn.net/u013696062/article/details/46819407

本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐