《UNIX网络编程卷1:套接字联网API》第3章 套接字编程简介
《UNIX网络编程卷1:套接字联网API》第3章 套接字编程简介
3.1 套接字地址结构:网络通信的基石
套接字地址结构是网络编程的核心数据结构,定义了通信实体的网络标识与端口信息。不同协议族(如IPv4、IPv6、Unix域)拥有独立的地址结构,但均通过通用结构 struct sockaddr
实现类型统一。
3.1.1 IPv4地址结构(sockaddr_in)
#include <netinet/in.h>
struct sockaddr_in {
uint8_t sin_len; // 结构体长度(BSD系统特有)
sa_family_t sin_family; // 地址族(AF_INET)
in_port_t sin_port; // 16位端口号(网络字节序)
struct in_addr sin_addr; // 32位IPv4地址(网络字节序)
char sin_zero[8]; // 填充字段(全零)
};
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址(网络字节序)
};
关键点:
sin_port
和sin_addr
必须使用网络字节序(大端模式);sin_zero
字段用于填充,确保结构体与sockaddr
对齐;- 实际编程中需使用
bzero
或memset
初始化结构体,避免内存残留。
3.1.2 IPv6地址结构(sockaddr_in6)
struct sockaddr_in6 {
uint8_t sin6_len; // 结构体长度
sa_family_t sin6_family; // 地址族(AF_INET6)
in_port_t sin6_port; // 16位端口号(网络字节序)
uint32_t sin6_flowinfo; // 流标签(QoS支持)
struct in6_addr sin6_addr; // 128位IPv6地址
uint32_t sin6_scope_id; // 作用域标识符(链路本地地址使用)
};
struct in6_addr {
uint8_t s6_addr[16]; // 128位IPv6地址
};
特点:
- 支持更大的地址空间(128位)和流标签(用于区分数据流优先级);
- 作用域标识符用于处理链路本地地址(如
fe80::1%eth0
)。
3.1.3 通用地址结构(sockaddr与sockaddr_storage)
struct sockaddr {
uint8_t sa_len; // 结构体长度
sa_family_t sa_family; // 地址族(AF_xxx)
char sa_data[14];// 协议地址(IP+端口)
};
struct sockaddr_storage {
sa_family_t ss_family; // 地址族
char __ss_pad1[_SS_PAD1SIZE]; // 填充字段
// ... 其他对齐字段(总长度通常为128字节)
};
设计意义:
sockaddr
用于兼容旧版API,但无法容纳IPv6地址;sockaddr_storage
是通用解决方案,可存储任意协议族的地址结构。
内存对齐图:
偏移量 | 成员名称 | 大小(字节) | 对齐方式 | 说明
-------------------------------------------------------------
0x00 | ss_family | 2 | 自然对齐(2字节)| 地址族标识(如 AF_INET)
0x02 | __ss_pad1 | 6 | 填充至 8 字节对齐| 确保后续字段对齐
0x08 | __ss_align | 8 | 8 字节对齐 | 强制 64 位对齐的占位符
0x10 | __ss_pad2 | 112 | 填充至 128 字节 | 剩余空间填充
-------------------------------------------------------------
总大小:128 字节
说明:sockaddr_storage通过填充字段确保内存对齐,避免平台兼容性问题。
以下是 sockaddr_storage
结构体的内存对齐图及详细分析,结合 Linux 和 Windows 系统的实现差异进行说明:
关键设计解析
-
多平台兼容性
- Linux:
ss_family
(2字节)后通过__ss_pad1
(6字节)填充至 8 字节边界,使__ss_align
(8字节)满足 64 位对齐。 - Windows:
ss_family
(2字节)后直接填充 48 字节(__ss_pad1
),再放置__ss_align
(8字节),最后通过__ss_pad2
(72字节)填充至 128 字节。
- Linux:
-
对齐规则
- 自然对齐:首个成员
ss_family
从偏移量 0 开始,占用 2 字节。 - 强制对齐:
__ss_align
需对齐到 8 字节边界(编译器默认对齐数),确保后续字段(如 IPv6 地址)无错位访问。 - 总大小约束:结构体总大小需为最大对齐数(8字节)的整数倍,故填充至 128 字节。
- 自然对齐:首个成员
-
功能目的
- 通用存储:128 字节空间可容纳 IPv4(16字节)、IPv6(28字节)等地址,避免动态类型判断。
- 类型转换安全:强制对齐后,可直接将协议特定地址(如
sockaddr_in6
)强制转换为sockaddr_storage
,避免内存越界。
实际应用场景
// 示例:接收任意协议地址
struct sockaddr_storage addr;
socklen_t addr_len = sizeof(addr);
recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&addr, &addr_len);
// 根据 ss_family 处理不同协议
if (addr.ss_family == AF_INET) {
struct sockaddr_in *ipv4_addr = (struct sockaddr_in*)&addr;
// 处理 IPv4 地址
} else if (addr.ss_family == AF_INET6) {
struct sockaddr_in6 *ipv6_addr = (struct sockaddr_in6*)&addr;
// 处理 IPv6 地址
}
总结
sockaddr_storage
通过精心设计的填充字段和对齐机制,在以下方面达到平衡:
- 空间效率:固定 128 字节,覆盖所有已知协议地址。
- 访问安全性:强制对齐避免 CPU 非对齐访问异常。
- 代码简洁性:统一接口处理多协议地址,降低网络编程复杂度。
3.2 字节序转换:跨越异构系统的桥梁
网络通信要求数据以**网络字节序(大端模式)**传输,主机字节序可能为小端(如x86)或大端(如PowerPC),需通过转换函数统一。
3.2.1 转换函数详解
#include <netinet/in.h>
// 主机字节序 → 网络字节序
uint16_t htons(uint16_t hostshort); // 短整型(端口号)
uint32_t htonl(uint32_t hostlong); // 长整型(IP地址)
// 网络字节序 → 主机字节序
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
嵌入式应用场景:
- ARM设备(小端)与DSP处理器(大端)通信时需显式转换;
- 示例:设置端口号
servaddr.sin_port = htons(8080);
3.2.2 结构体字段转换实践
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9999); // 转换端口号
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr); // IP地址转换
3.3 地址转换函数:字符串与二进制的互操作
IPv4/IPv6地址常需在可读字符串(如"192.168.1.1")和二进制格式间转换,推荐使用现代函数 inet_pton
和 inet_ntop
。
3.3.1 函数原型与参数解析
#include <arpa/inet.h>
// 字符串 → 二进制(支持IPv4/IPv6)
int inet_pton(int af, const char *src, void *dst);
// 二进制 → 字符串(需预分配缓冲区)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数说明:
af
:地址族(AF_INET
或AF_INET6
);src
:输入数据指针;dst
:输出缓冲区;size
:缓冲区长度(推荐使用INET_ADDRSTRLEN
或INET6_ADDRSTRLEN
)。
代码示例:
char ipv4_str[INET_ADDRSTRLEN];
struct in_addr ipv4_addr;
inet_pton(AF_INET, "203.0.113.1", &ipv4_addr); // 字符串转二进制
inet_ntop(AF_INET, &ipv4_addr, ipv4_str, INET_ADDRSTRLEN); // 二进制转字符串
3.3.2 弃用函数警告
inet_addr
和inet_aton
仅支持IPv4,且无错误返回;inet_ntoa
使用静态缓冲区,非线程安全。
3.4 套接字API核心函数解析
3.4.1 socket():创建通信端点
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数详解:
domain
:协议族(如AF_INET
、AF_INET6
、AF_UNIX
);type
:套接字类型(SOCK_STREAM
、SOCK_DGRAM
);protocol
:通常为0(自动选择默认协议)。
返回值:
- 成功:返回非负套接字描述符;
- 失败:返回-1,设置
errno
(如EPROTONOSUPPORT
协议不支持)。
代码示例:
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
3.4.2 bind():绑定本地地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
关键点:
- 服务器必须调用
bind
以指定监听地址和端口; - 客户端通常无需显式绑定,由内核自动分配临时端口。
错误处理:
EADDRINUSE
:端口被占用;EACCES
:尝试绑定特权端口(<1024)无权限。
代码示例:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口
servaddr.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
3.4.3 listen():开启监听模式
int listen(int sockfd, int backlog);
参数解析:
backlog
:已完成连接队列(ESTABLISHED)的最大长度;- Linux内核4.3+后,
backlog
仅限制已完成队列,由net.core.somaxconn
定义上限。
最佳实践:
if (listen(sockfd, SOMAXCONN) < 0) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
3.4.4 accept():接受客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
值-结果参数:
addr
:客户端地址结构指针;addrlen
:输入时为缓冲区大小,返回时为实际地址长度。
代码示例:
struct sockaddr_in cliaddr;
socklen_t cliaddr_len = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
if (connfd < 0) {
perror("accept failed");
continue; // 非致命错误可继续运行
}
3.4.5 connect():发起连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
错误处理:
ETIMEDOUT
:服务器无响应(网络不通或防火墙拦截);ECONNREFUSED
:目标端口无服务监听;EHOSTUNREACH
:路由不可达。
代码示例:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "203.0.113.1", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
3.5 值-结果参数:内核与用户空间的协作
套接字API中,地址长度参数(如 accept
的 addrlen
)采用值-结果传递机制:
- 输入:用户告知内核缓冲区大小,避免内存溢出;
- 输出:内核返回实际地址长度,用户可验证协议类型。
示例分析:
socklen_t len = sizeof(struct sockaddr_storage);
struct sockaddr_storage client_addr;
int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
// 根据实际协议族处理连接
if (client_addr.ss_family == AF_INET) {
struct sockaddr_in *ipv4_addr = (struct sockaddr_in*)&client_addr;
// 处理IPv4地址
} else if (client_addr.ss_family == AF_INET6) {
struct sockaddr_in6 *ipv6_addr = (struct sockaddr_in6*)&client_addr;
// 处理IPv6地址
}
3.6 错误处理与包裹函数设计
3.6.1 系统调用错误处理模式
UNIX系统调用通过返回-1并设置 errno
指示错误类型:
if ((n = read(fd, buf, size)) < 0) {
if (errno == EINTR) // 被信号中断
goto retry;
else
err_sys("read error"); // 终止程序
}
3.6.2 包裹函数封装
通过包裹函数简化错误处理,提升代码可读性:
int Socket(int family, int type, int protocol) {
int n;
if ((n = socket(family, type, protocol)) < 0)
err_sys("socket error");
return n;
}
void Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
if (connect(sockfd, addr, addrlen) < 0)
err_sys("connect error");
}
优点:
- 统一错误处理逻辑;
- 减少代码冗余;
- 支持调试日志插入。
3.7 实战:完整TCP回射服务器/客户端
3.7.1 服务器端代码
#include "unp.h"
void str_echo(int sockfd) {
ssize_t n;
char buf[MAXLINE];
while ((n = Read(sockfd, buf, MAXLINE)) > 0)
Writen(sockfd, buf, n);
}
int main() {
int listenfd = Socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for (;;) {
int connfd = Accept(listenfd, NULL, NULL);
if (fork() == 0) { // 子进程
Close(listenfd);
str_echo(connfd);
Close(connfd);
exit(0);
}
Close(connfd); // 父进程关闭连接套接字
}
}
3.7.2 客户端代码
#include "unp.h"
void str_cli(FILE *fp, int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, strlen(sendline));
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
int main(int argc, char **argv) {
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
int sockfd = Socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9999);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
str_cli(stdin, sockfd);
exit(0);
}
3.8 图文说明与调试技巧
-
TCP三次握手与套接字函数关联图
说明:socket()、bind()、listen()在服务器端准备连接,accept()阻塞等待SYN。
-
值-结果参数传递示意图
+----------------+ +----------------+ | 用户空间 | | 内核空间 | | addrlen=16 | ---> | 检查缓冲区大小 | | (输入值) | <--- | 设置实际长度 | +----------------+ +----------------+
-
常见错误排查表
错误码 原因 解决方案 EADDRINUSE 端口被占用 更换端口或设置SO_REUSEADDR ECONNREFUSED 目标端口无服务 检查服务器是否运行 ENETUNREACH 网络不可达 检查路由与防火墙配置
3.9 本章小结与进阶习题
小结:本章深入解析了套接字地址结构、核心API函数及错误处理机制,通过完整的TCP案例演示了网络编程的基础流程,为后续高级主题奠定基础。
习题:
- 修改回射服务器支持IPv6,并测试客户端连接;
- 实现基于UDP的简单聊天程序,支持多客户端通信;
- 使用Wireshark抓取TCP握手过程,分析序列号与确认号变化。
付费用户专属资源:
-
完整代码工程(含IPv4/IPv6双栈支持);
-
套接字API调用流程图(矢量图);
-
扩展阅读:《UNIX网络编程中的原子操作与线程安全》。
| 目标端口无服务 | 检查服务器是否运行 |
| ENETUNREACH | 网络不可达 | 检查路由与防火墙配置 |
3.9 本章小结与进阶习题
小结:本章深入解析了套接字地址结构、核心API函数及错误处理机制,通过完整的TCP案例演示了网络编程的基础流程,为后续高级主题奠定基础。
习题:
- 修改回射服务器支持IPv6,并测试客户端连接;
- 实现基于UDP的简单聊天程序,支持多客户端通信;
- 使用Wireshark抓取TCP握手过程,分析序列号与确认号变化。
付费用户专属资源:
- 完整代码工程(含IPv4/IPv6双栈支持);
- 套接字API调用流程图(矢量图);
- 扩展阅读:《UNIX网络编程中的原子操作与线程安全》。
通过本章学习,读者将掌握套接字编程的核心技术,并能够开发健壮的网络应用。