《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
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)