第 59 章 Sockets: Internet 域 (Sockets: Internet Domains)
核心结论
-
Internet 域 socket 是基于 TCP/IP 的进程间通信:流式 socket (SOCK_STREAM) 在 TCP 之上提供可靠、双向字节流;数据报 socket (SOCK_DGRAM) 在 UDP 之上提供不可靠、无连接、消息边界通信。
-
网络字节序为大端:不同硬件采用不同字节序 (big-endian vs little-endian),跨网络传输必须用
htonl/htons/ntohl/ntohs转换为统一的 网络字节序 (大端) 以保证跨平台兼容。 -
IPv4 / IPv6 地址结构:
struct sockaddr_in(16 字节) 存 IPv4 地址 + 端口;struct sockaddr_in6存 IPv6 (128 位) + 端口;struct sockaddr_storage是大小足够的通用容器,可透明容纳 IPv4/IPv6,是协议无关编程的基石。 -
Protocol-independent 转换函数:
getaddrinfo()接受主机名 + 服务名,返回addrinfo链表,内含sockaddr及 ai_family/ai_socktype;getnameinfo()反向;二者替代过时的gethostbyname/getservbyname,并同时处理 IPv4/IPv6。 -
inet_pton / inet_ntop 通用转换:
p= presentation、n= network;支持 IPv4 点分十进制与 IPv6 十六进制字符串与二进制形式互转;与过时的inet_aton / inet_ntoa(仅 IPv4) 形成对比。 -
DNS 与 /etc/services:DNS 提供分布式 hostname → IP 映射(递归查询 + 迭代查询 + 缓存);
/etc/services静态映射 service-name ↔ port-number(IANA 集中维护);二者共同为上层 API 提供「名字 → 数字」的间接层。
|
本章主旨
本章是第 56-58 章概念铺垫后的 Internet 域编程实战。读者需要掌握:(1) 字节序问题与 |
一、核心概念
本章围绕 6 个核心概念展开:网络字节序、跨架构数据表示、Internet 地址结构、IP 地址文本 ↔ 二进制转换、协议无关的 host/service 解析、DNS 与服务名映射。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
网络字节序 (Network Byte Order) |
跨网络传输整数时统一使用的大端序;不同机器的 host byte order 不同(小端如 x86 / 大端如 PowerPC 等),必须通过 |
§59.2;网络字节序 = 大端;函数名含义:host-to-network-long 等;常用于 |
跨架构数据表示 (Data Representation) |
不同 C 数据类型在不同实现中长度不同( |
§59.3;TLPI 第 59 章示例程序把整数编码为 |
Internet 域地址结构 |
|
§59.4;定义于 |
inet_pton/inet_ntop 转换 |
|
§59.6;定义 |
getaddrinfo / getnameinfo (协议无关) |
|
§59.10;addrinfo 结构关键字段:ai_family/ai_socktype/ai_protocol/ai_addr/ai_addrlen/ai_canonname/ai_next;新代码必须用这两个函数,不用 |
DNS 与 /etc/services |
DNS 是 hostname ↔ IP 的分布式层级数据库(根域名 → TLD → 二级域 → 子域);客户端发递归请求至本地 DNS,后者做迭代查询; |
§59.8–59.9;DNS 查询: |
二、详细笔记
59.1 Internet 域 socket 概览
What:Internet 域 socket 的两种语义——基于 TCP 的 SOCK_STREAM 提供可靠、双向字节流;基于 UDP 的 SOCK_DGRAM 提供无连接、可能丢失 / 乱序 / 重复的消息服务。
Why:理解 Internet 与 UNIX 域的差异——Internet 域 socket 地址由 (IP, port) 组成;UNIX 域由 (pathname) 组成;前者穿越主机边界,后者不。
How:
| 维度 | Internet 域 | UNIX 域 |
|---|---|---|
寻址 |
(IP, port) |
pathname |
通信域 |
跨主机 |
同主机 |
UDP 数据报可靠性 |
不可靠(可能丢失/乱序/重复) |
UNIX 数据报可靠(基于本地 IPC) |
UDP 满队列处理 |
静默丢弃 |
send 阻塞 |
流式 socket 协议 |
TCP(连接建立 + 可靠字节流) |
本地字节流 |
When:(1) 跨主机通信必须用 Internet 域;(2) 同主机且追求性能/可传 fd/凭证时可考虑 UNIX 域(第 57 章)。
Example:典型 Internet 域 UDP echo 的 server recvfrom 收到 (claddr, len) 后回送;客户端 sendto 到 (svaddr)。
59.2 网络字节序
What:所有跨网络传输的多字节整数(sin_port、sin_addr)必须使用统一的大端字节序(网络字节序);x86 等小端主机需用 htonl/htons 转换。
Why:不同硬件在不同时代各自演化出不同的字节序——没有统一标准,跨平台协议就破环;TCP/IP RFC 793 选择大端作为标准,现代协议栈一律遵循。
How:
| 函数 | 头文件 | 语义 |
|---|---|---|
|
|
host 16-bit → network 16-bit |
|
|
host 32-bit → network 32-bit |
|
|
network 16-bit → host 16-bit |
|
|
network 32-bit → host 32-bit |
函数定义通常为宏,在主机已经是大端时返回原值;名字源于早期 16/32-bit 区分。uint16_t/uint32_t 严格对应 16/32 位无符号整数。
When:(1) 任何时候在 socket API 中把整数写入 sockaddr_in.sin_port / sin_addr;(2) 任何时候从这些字段读出整数在主机上做比较 / 数值运算。
Example:
// 摘自《The Linux Programming Interface》 第 59 章
#include <arpa/inet.h>
struct sockaddr_in svaddr;
memset(&svaddr, 0, sizeof(svaddr));
svaddr.sin_family = AF_INET;
svaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host → network */
svaddr.sin_port = htons(PORT_NUM); /* host → network */
/* 读出时反向 */
uint16_t port = ntohs(claddr.sin_port);
59.3 数据表示与 marshalling
What:异构机器之间的二进制结构体不能直接 write/read,因为 C 实现可能在字节序、int/long 宽度、结构 padding 上各不相同;解决办法是「marshalling」——按某种标准格式编码所有数据项。
Why:一旦通讯双方机器架构 / 编译器 / 库版本不同,二进制直接交换会解错。即使本机暂同构,迁移到云端 / 跨平台时代码会失效。
How:两种策略:
-
完整 marshalling:XDR (RFC 1014)、ASN.1-BER、CORBA、XML;为每种数据类型定义固定字节表示 + 类型 tag。
-
简化方案(多数应用采用):把数据编码成
\n-结束的文本(类似 HTTP 行分隔),可以用 telnet 手工调试。
TLPI 提供 readLine()(Listing 59-1)从流式 socket 一次读一行的工具:
// 摘自《The Linux Programming Interface》 第 59 章 Listing 59-1
#include <unistd.h>
#include <errno.h>
ssize_t
readLine(int fd, void *buffer, size_t n)
{
ssize_t numRead; size_t totRead; char *buf; char ch;
if (n <= 0 || buffer == NULL) { errno = EINVAL; return -1; }
buf = buffer; totRead = 0;
for (;;) {
numRead = read(fd, &ch, 1);
if (numRead == -1) {
if (errno == EINTR) continue;
else return -1;
} else if (numRead == 0) {
if (totRead == 0) return 0;
else break;
} else {
if (totRead < n - 1) { totRead++; *buf++ = ch; }
if (ch == '\n') break;
}
}
*buf = '\0';
return totRead;
}
When:(1) 同种协议(同为 TCP)但运行在不同 arch 时——必须 marshall;(2) 大型 RPC / 中间件——用完整 XDR/ASN.1;(3) 简单文本协议——用 \n 分隔 + readLine。
Example:TLPI 的 is_seqnum_sv / is_seqnum_cl 把整数发给客户端用 snprintf("%d\n", seqNum),接收用 readLine。
59.4 Internet socket 地址结构
What:IPv4 地址在 struct sockaddr_in(16 字节):sin_family=AF_INET,sin_port,sin_addr.s_addr(32 位 in_addr_t),加上填充对齐到 sockaddr 大小。IPv6 在 struct sockaddr_in6:sin6_family=AF_INET6,sin6_port,sin6_addr (16 字节) + sin6_flowinfo + sin6_scope_id。
Why:协议栈需要统一的「地址结构体」字段视图;有了 sockaddr_storage,调用 bind/connect/accept 时无需知道对端是 v4 还是 v6——向上层 API 提供协议无关性。
How:
| 类型 | 关键字段 | 用法 |
|---|---|---|
|
|
用 |
|
|
wildcard 用 |
|
sin_family=AF_INET, sin_port, sin_addr |
IPv4 bind/connect 参数 |
|
sin6_family=AF_INET6, sin6_port, sin6_addr + flowinfo/scope_id |
IPv6 bind/connect 参数 |
|
足够大 (~128B) |
透明存 v4/v6;用 |
IPv4/v6 共享同一端口号空间——绑 IPv6 端口会占住 IPv4 同样端口(视具体内核行为)。
When:(1) 任何 bind/connect/sendto/accept 地址参数;(2) IPv4/IPv6 双栈代码首选 sockaddr_storage。
Example:
// 摘自《The Linux Programming Interface》 第 59 章
struct sockaddr_in6 svaddr;
memset(&svaddr, 0, sizeof(struct sockaddr_in6));
svaddr.sin6_family = AF_INET6;
svaddr.sin6_addr = in6addr_any; /* wildcard */
svaddr.sin6_port = htons(SOME_PORT_NUM);
bind(sfd, (struct sockaddr *) &svaddr, sizeof(struct sockaddr_in6));
59.5–59.7 IP 地址转换与 datagram 客户端/服务器示例
What:inet_pton (presentation→network) 和 inet_ntop (network→presentation) 处理 v4/v6 的字符串 ↔ 二进制互转;TLPI 给出 IPv6 datagram echo 的完整示例(Listings 59-3 / 59-4)。
Why:人脑爱字符串、协议栈要整数;inet_ntop 比 getnameinfo 廉价(不查 DNS)且保证成功。
How:
| 函数 | 返回 | 失败场景 |
|---|---|---|
|
1 成功 / 0 格式错 / -1 err |
字符串无效 |
|
buf / NULL |
len 太小 → |
缓冲区长度用 INET_ADDRSTRLEN=16 (v4) / INET6_ADDRSTRLEN=46 (v6)。
When:(1) 用户从 argv 传 IP 字符串 → inet_pton;(2) 日志/打印 IP → inet_ntop;(3) v6 数据报服务器日志客户端地址用 inet_ntop(AF_INET6, &claddr.sin6_addr, str, INET6_ADDRSTRLEN)。
Example:TLPI Listing 59-3 的 IPv6 datagram echo server 关键循环:
// 摘自《The Linux Programming Interface》 第 59 章 Listing 59-3
for (;;) {
len = sizeof(struct sockaddr_in6);
numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
inet_ntop(AF_INET6, &claddr.sin6_addr, claddrStr, INET6_ADDRSTRLEN);
printf("Server received %ld bytes from (%s, %u)\n",
(long) numBytes, claddrStr, ntohs(claddr.sin6_port));
/* 大写转换后 sendto 回 client */
}
59.8–59.9 DNS 与 /etc/services
What:DNS 是 hostname ↔ IP 的分布式层级命名系统——根、TLD(com / org / country-code)、二级域、子域,每层由不同组织管理的 zone。/etc/services 是 service-name ↔ port 的本地静态表(IANA 集中注册)。
Why:若没有 DNS,中央管数百万 hostname 不可行;分布式 + zone 授权 + 缓存解决了扩展性。/etc/services 解决「服务名 → 端口号」的小规模映射,没必要走分布式协议。
How:
DNS 查询机制:
. 客户端调用 getaddrinfo("www.kernel.org") → resolver 发 DNS 请求到本地 DNS server(/etc/resolv.conf 指定)。
. 本地 DNS 收到「递归」请求;若无缓存 → 走「迭代」查询:根 → TLD(org.)→ 二级(kernel.org.)→ 返回 A/AAAA 记录。
. 本地 DNS 缓存并返回客户端。
/etc/services 格式:
ssh 22/tcp
http 80/tcp
domain 53/udp # 通常同名同端口,但少数反例(rsh 514/tcp vs syslog 514/udp)
注:IANA 政策约定一个服务如有 v4/v6 都分配相同端口号;存在极少反例。
When:(1) 程序接受 hostname —— getaddrinfo 会查 DNS;(2) 程序接受 service-name —— getaddrinfo 查 /etc/services;(3) 多接口 hostname 解析返回多地址,getaddrinfo 给链表。
Example:手动调试 DNS — dig . NS 列根服务器;dig www.kernel.org A 显式查询 A 记录。
59.10–59.12 协议无关的 getaddrinfo / getnameinfo 与 inet_sockets 库
What:getaddrinfo(host, service, hints, &result) 返回 addrinfo 链表,可同时覆盖 v4/v6 + stream/dgram。getaddrinfo 替代过时的 gethostbyname + getservbyname,是 IPv4/IPv6 通用的关键。getnameinfo 反向,freeaddrinfo 释放,gai_strerror 错误转字符串。
Why:不依赖 getaddrinfo 的代码就只能写两套分支处理 v4/v6;有了它 + sockaddr_storage,任何单一 for (rp = result; rp; rp = rp→ai_next) { … } 循环就足以处理多地址。
How:
addrinfo 结构:
| 字段 | 含义 |
|---|---|
|
AF_INET / AF_INET6 |
|
SOCK_STREAM / SOCK_DGRAM |
|
协议(一般传 0) |
|
实际 socket 地址(sockaddr_in / sockaddr_in6) |
|
仅首个节点,可选填充主名(需 AI_CANONNAME) |
|
链表下一个 |
hints.ai_flags 关键值:
| Flag | 作用 |
|---|---|
AI_PASSIVE |
host=NULL 时返回 wildcard;用于 server bind |
AI_CANONNAME |
首个节点 ai_canonname 指向主名 |
AI_NUMERICHOST |
host 必须为数字串;阻止 DNS 查询 |
AI_NUMERICSERV |
service 为数字端口号;不查 /etc/services |
AI_V4MAPPED |
AF_INET6 无 v6 时返回 v4-mapped v6 |
AI_ADDRCONFIG |
只返回本机有配置的协议栈 |
|
默认返回 loopback |
|
端口填 0 |
gai_strerror() 处理的错误码:EAI_AGAIN / EAI_BADFLAGS / EAI_FAIL / EAI_FAMILY / EAI_MEMORY / EAI_NONAME / EAI_OVERFLOW / EAI_SERVICE / EAI_SOCKTYPE / EAI_SYSTEM。
TLPI Listing 59-8 给出封装库 inet_sockets.c:
| 函数 | 用途 | 实现提示 |
|---|---|---|
|
client connect;返回 fd |
遍历 result 调用 socket+connect |
|
TCP server bind+listen |
通过 inetPassiveSocket(…, doListen=TRUE) |
|
UDP server/client bind |
inetPassiveSocket(…, doListen=FALSE) |
|
sockaddr → "(host, port)" 字符串 |
内部调 getnameinfo |
When:(1) 写新代码——必须用 getaddrinfo/getnameinfo;(2) 想要 IPv4/IPv6 双栈透明——hints.ai_family = AF_UNSPEC + 遍历链表;(3) 想要 faster startup——AI_NUMERICHOST 跳过 DNS;(4) 想要 IP-only 日志——NI_NUMERICHOST 跳过反向。
Example:TLPI Listing 59-6 顺序服务器关键片段:
// 摘自《The Linux Programming Interface》 第 59 章 Listing 59-6
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
if (getaddrinfo(NULL, PORT_NUM, &hints, &result) != 0)
errExit("getaddrinfo");
for (rp = result; rp != NULL; rp = rp->ai_next) {
lfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (lfd == -1) continue;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof(optval)) == -1)
errExit("setsockopt");
if (bind(lfd, rp->ai_addr, rp->ai_addrlen) == 0)
break; /* success */
close(lfd);
}
listen(lfd, BACKLOG);
59.13 过时的 API 与 UNIX vs Internet 域选择
What:inet_aton/inet_ntoa、gethostbyname/gethostbyaddr、getservbyname/getservbyport 是被 SUSv4 移除的过时函数,用法上局限于 IPv4(inet_aton)或非线程安全(全部共享静态返回指针)。同主机应用程序可以在 UNIX 域与 Internet 域之间二选一。
Why:过时函数不可重入、IPv4-only;理解它们只是因为会在历史代码中碰到。新代码必须迁移至 inet_pton/ntop 和 getaddrinfo/nameinfo。
How:
| 过时函数 | 替代 | 备注 |
|---|---|---|
|
|
仅 IPv4 |
|
|
返回静态指针,多线程不安全 |
|
|
不支持 v6、不可重入 |
|
|
同上 |
|
|
同上 |
|
|
同上 |
UNIX vs Internet 同主机选择:
-
UNIX 域性能更好(无协议栈 / 中断 / 拷贝开销)。
-
UNIX 域可用文件系统权限鉴权(sticky bit、owner/group)。
-
UNIX 域支持 fd passing 与 sender credentials(第 61 章)。
-
Internet 域可同时在同机 + 跨主机工作,无需修改。
When:(1) 同主机 IPC + 需要传 fd 或凭证 → UNIX 域;(2) 潜在迁移到多主机 → Internet 域;(3) 兜底——同机用 UNIX 域前先评估两套 API 维护成本。
Example:TLPI 给出 UNIX 域服务器 / 客户端(us_xfr_sv/cl)和 Internet 域版本(is_seqnum_sv/cl);练习 59-3 写 UNIX 域版本的 inet_sockets 库。
三、关键图表
|
关键系统调用 / 库函数
|
|
关键常量与结构体
|
四、思维导图
mindmap
root((第 59 章 Internet 域))
字节序
大端网络序
htonl htons
ntohl ntohs
跨架构兼容
数据表示
C long 宽度差异
Marshalling XDR
文本分隔协议
telnet 调试
地址结构
sockaddr_in IPv4
sockaddr_in6 IPv6
sockaddr_storage
INADDR_ANY LOOPBACK
in6addr_any
IP 字符串转换
inet_pton
inet_ntop
INET_ADDRSTRLEN
取代 inet_aton ntoa
协议无关解析
getaddrinfo
getnameinfo
addrinfo hints
AI_PASSIVE NUMERIC
inet_sockets 库
DNS / services
分布式层级
递归迭代查询
缓存加速
etc services 静态
五、重点与易错点
-
「网络字节序 = 大端」是铁律——
sin_port/sin_addr永远要htons/htonl;忘记转换会在小端机器上误读对端数据。 -
永远
memset全零再填字段——sockaddr_in6还有sin6_flowinfo、sin6_scope_id两个字段虽常设 0,但混用 v4/v6 结构时容易留下脏数据。 -
IPv6 wildcard vs loopback 用变量不用宏——
IN6ADDR_ANY_INIT是 initializer(只能用于声明),赋值语句必须用in6addr_any;loopback 同理。 -
新代码只用
getaddrinfo,禁用gethostbyname——后者是非线程安全、IPv4-only 的过时函数,SUSv4 已删除。新代码中应集中精力学会 hints.ai_flags 的用法。 -
getaddrinfo(NULL, …)vsgetaddrinfo(NULL, …, AI_PASSIVE, …)——前者默认给 loopback(适合 client),后者给 wildcard(适合 server bind)。 -
AI_NUMERICHOST 阻止 DNS 查询——argv 直接传 IP 字符串的场景用它,能省去 DNS 解析时间。
-
traverse
ai_next链表必须 free——freeaddrinfo(&result)释放整条链;中间要复制地址需先memcpy再释放。 -
EAI_错误用gai_strerror不能perror/strerror*——它返回的是 int 错误码(如EAI_AGAIN),不是errno;EAI_SYSTEM才是「看 errno」。 -
/etc/services不是保留机制——文件中的端口号随时可能被占用;要让别人知道你的端口,去 IANA 注册而不是写文件。 -
同主机可选 UNIX 域——性能更好、可传 fd 可鉴权;但代码库多一份维护代价;可仿照练习 59-3 写 UNIX 版 inet_sockets 库。
-
端口 0 让内核分配 ephemeral——bind 后通过
getsockname取实际端口;常用于 client 端,让内核选可用 ephemeral。 -
IPv4-mapped IPv6 地址
::ffff:a.b.c.d——双栈环境 v4 客户端可走 v6 server(若开启);AI_V4MAPPED 控制getaddrinfo是否返回这种地址。 -
telnet 调试协议应用——把数据传输做成
\n-分隔文本,应用启动后telnet host port可手动测试,远胜 ad-hoc 客户端。 -
inet_sockets 库是 TLPI 的精华——实际生产中通常会沉淀这种封装;不要在每个程序里都重复
for (rp = result; …)模板代码。 -
跨章衔接:第 60 章 Sockets: Server Design 演示这套 API 在迭代 / 并发服务器中的真实用法;第 61 章深入 TCP 状态机、
shutdown()、SO_REUSEADDR与 OOB 数据;第 56-58 章铺垫 generic socket 与 TCP/IP;第 62 章起转向终端 / 备选 I/O 模型。