欢迎光临
我们一直在努力

《UNIX 网络编程-卷1》阅读笔记12: 名字与地址转换

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


一、章节概述

1.1 本章焦点

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

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

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

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

二、域名系统(DNS)

2.1 DNS 的基本概念

域名系统(DNS)主要用于主机名和 IP 地址之间的映射。主机名既可以是一个简单名字(simple name),如 solaris 或 bsdi,也可以是一个全限定域名(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 服务器。常见的解析器函数是 gethostbyname 和 gethostbyaddr,前者把主机名映射为 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?

gethostbyname 和 gethostbyaddr 这两个函数仅仅支持 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_flags、ai_family、ai_socktype 和 ai_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_r、gethostbyaddr_r 等。这些函数需要调用者提供缓冲区,避免了使用静态数据的竞争问题。

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

💡 最佳实践:getaddrinfo 和 getnameinfo 本身就是可重入的,应优先使用它们,而不是 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 结构 调用前用 bzero 或 memset 清零

⚠️ 重要:在服务器端使用 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_connect、tcp_listen、host_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 的互操作性

第十二章将详细讲解:

  • IPv4 客户端与 IPv6 服务器的通信:当服务器是 IPv6 但客户端只支持 IPv4 时,如何实现互操作?
  • IPv6 客户端与 IPv4 服务器的通信:IPv6 客户端如何连接到 IPv4 服务器?
  • 双栈主机的地址映射机制:IPv4 映射的 IPv6 地址格式(::ffff:0:0/96)
  • IPV6_V6ONLY 套接字选项:限制套接字只处理 IPv6 请求
  • 协议无关的编程实践:如何编写能够同时处理 IPv4 和 IPv6 客户端的服务器程序
  • IPv6 地址测试宏:IN6_IS_ADDR_V4MAPPED 等宏的用法
  • 学习目标:学完第十二章后,你将能够——

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

    敬请期待!

    参考资料

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

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

    赞(0)
    未经允许不得转载:171主机测评 » 《UNIX 网络编程-卷1》阅读笔记12: 名字与地址转换
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址