《UNIX 网络编程-卷1》阅读笔记01: 开启基于“套接字联网API”的编程之旅
作者: andylin02
关键词: UNIX网络编程, 套接字, TCP/IP, 并发服务器, I/O复用, unpv13e
一、这本书讲了什么?
1.1 书籍概述
《UNIX网络编程 卷1:套接字联网API》(第3版)是由网络专家Bill Fenner和Andrew M. Rudoff对已故UNIX网络专家W. Richard Stevens博士的经典著作进行全面修订的权威之作。全书以套接字编程为核心,系统解析TCP、UDP、SCTP等协议实现原理,新增了IPv6与密钥管理套接字相关内容,并删除了X/Open传输接口的内容。
这本书的目标读者是那些希望自己编写的程序能使用称为套接字(socket)的API进行彼此通信的人。书中的所有示例都是在UNIX系统上测试通过的真实的、可运行的代码,继承了Stevens一直强调的理念:“学习网络编程的方法就是下载这些程序,对其进行修改和改进。只有这样实际编写代码才能深入理解有关概念和技术”。
1.2 全书内容结构
本书内容详尽且具权威性,共分为四个主要部分:
第一部分:简介和TCP/IP(第1-2章)
- 第1章:网络编程的基本概念、OSI模型、TCP/IP协议栈概述,以及一个简单的时间获取客户程序示例
- 第2章:传输层:TCP、UDP和SCTP协议详解,包括TCP连接的建立与终止、TIME_WAIT状态等
第二部分:基本套接口编程(第3-9章)
- 第3章:套接字编程简介——套接字地址结构、值-结果参数、字节排序函数
- 第4章:基本TCP套接字编程——socket、connect、bind、listen、accept、close函数
- 第5章:TCP客户/服务器程序示例
- 第6章:I/O复用——select和poll函数
- 第7章:套接字选项
- 第8章:基本UDP套接字编程
- 第9章:SCTP套接字编程(第3版新增)
第三部分:高级套接字编程(第10-21章)
- 第10-13章:IPv4与IPv6互操作性、守护进程、高级I/O函数
- 第14章:非阻塞I/O
- 第15章:信号驱动I/O
- 第16章:线程(Threads)
- 第17-21章:IP选项、原始套接口、数据链路访问、广播与多播
第四部分:客户/服务器程序设计(第22-31章)
- 第22-26章:各种客户/服务器设计范式(迭代服务器、并发服务器、预先派生进程/线程服务器等)
- 第27-30章:客户端设计、流设备驱动机制等
二、学完本书能掌握哪些关键知识?
2.1 核心技术点清单
| 技术领域 | 具体知识点 | 难度等级 |
|---|---|---|
| 网络基础 | TCP/IP协议栈、OSI模型、端口、IP地址、字节序 | ⭐⭐ |
| 套接字基础 | socket/bind/listen/accept/connect/close | ⭐⭐ |
| TCP编程 | 三次握手、四次挥手、TIME_WAIT状态、流量控制 | ⭐⭐⭐ |
| UDP编程 | 无连接通信、数据报边界、异步错误 | ⭐⭐ |
| I/O复用 | select/poll/epoll模型 | ⭐⭐⭐⭐ |
| 非阻塞I/O | fcntl设置非阻塞、轮询机制 | ⭐⭐⭐ |
| 信号驱动I/O | SIGIO信号、异步通知 | ⭐⭐⭐ |
| 并发服务器 | 多进程、多线程、线程池 | ⭐⭐⭐⭐ |
| 协议无关性 | IPv4/IPv6兼容、getaddrinfo函数族 | ⭐⭐⭐ |
| 广播与多播 | 组播加入/离开、多播路由 | ⭐⭐⭐ |
| 原始套接口 | ICMP协议实现、ping/traceroute实现 | ⭐⭐⭐⭐ |
| 高级I/O | readv/writev散读集写、sendfile零拷贝 | ⭐⭐⭐⭐ |
2.2 各章节核心知识点详解
第2章:TCP连接的建立与终止
TCP通过三次握手建立连接:
三次握手:
客户端 服务器
CLOSED LISTEN
| |
|------- SYN (seq=x) --------> | (第一次握手)
SYN_SENT SYN_RCVD
| |
|<----- SYN (seq=y, ACK=x+1) --| (第二次握手)
ESTABLISHED SYN_RCVD
| |
|------- ACK (ack=y+1) -------> (第三次握手)
ESTABLISHED ESTABLISHED
四次挥手:
主动关闭方 被动关闭方
ESTABLISHED ESTABLISHED
| |
|------- FIN (seq=u) -------> | (第一次挥手)
FIN_WAIT_1 CLOSE_WAIT
| |
|<------- ACK (ack=u+1) -----| (第二次挥手)
FIN_WAIT_2 CLOSE_WAIT
| |
|<------- FIN (seq=v) -------| (第三次挥手)
TIME_WAIT LAST_ACK
| |
|------- ACK (ack=v+1) -----> | (第四次挥手)
TIME_WAIT (2MSL) CLOSED
|
CLOSED
第3章:套接字地址结构
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr; // 32位的IPv4地址,网络字节序
};
struct sockaddr_in {
uint8_t sin_len; // 结构长度(16字节)
sa_family_t sin_family; // 地址族:AF_INET
in_port_t sin_port; // 16位TCP/UDP端口号,网络字节序
struct in_addr sin_addr; // 32位IPv4地址,网络字节序
char sin_zero[8]; // 未使用,用于填充
};
💡 关键点:sin_family、sin_port和sin_addr是POSIX规范必需的三个字段。当向套接字函数传递地址结构指针时,必须强制转换成
(struct sockaddr*)类型,这是因为套接字函数早于ANSI C标准。
第6章:I/O复用
I/O复用是实现高并发服务器的关键技术,select/poll/epoll是对比:
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 通常1024(FD_SETSIZE) | 无限制 | 无限制 |
| 效率 | O(n) | O(n) | O(1) |
| 触发方式 | 水平触发(LT) | 水平触发(LT) | 边缘触发(ET)/水平触发(LT) |
| 内存拷贝 | 每次调用都拷贝 | 每次调用都拷贝 | 调用epoll_wait时拷贝 |
第22-30章:客户/服务器设计范式
本书第22-30章全面探讨了8种客户/服务器设计范式:
| 范式 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| TCP迭代服务器 | 一次只服务一个客户端 | 简单 | 有阻塞,不能同时处理多个客户 |
| 每个客户fork一个子进程 | 新客户到来时创建新进程 | 稳定,隔离性好 | 进程创建开销大 |
| 预先派生子进程 | 启动时创建进程池 | 减少fork开销 | 存在惊群问题 |
| 每个客户创建一个线程 | 使用pthread_create | 轻量级 | 需要同步 |
| 预先创建线程池 | 启动时创建线程池 | 高性能,减少创建开销 | 实现复杂 |
| I/O复用(select/poll) | 单进程多路复用 | 无并发同步问题 | 大规模连接效率低 |
| I/O复用(epoll) | 事件驱动机制 | 高效处理大量连接 | Linux特有 |
| 混合模型 | 复用+多线程池 | 最高性能 | 实现最复杂 |
三、需要具备什么基础能力?
3.1 硬性要求(必须掌握)
| 能力项 | 具体要求 | 检查标准 |
|---|---|---|
| C语言 | 指针、结构体、动态内存分配、函数指针 | 能独立编写100行以上的C程序 |
| UNIX/Linux基础 | 基本命令、文件权限、I/O重定向、管道 | 能熟练使用ls/cd/grep/find等命令 |
| 编译工具 | gcc基本用法、Makefile | 能手动编译多文件C程序 |
| 调试能力 | gdb基本使用 | 能设置断点、查看变量、单步执行 |
3.2 前置知识要求
建议了解以下基本网络概念:OSI模型、TCP/IP各层功能和IP寻址。能够用C语言编写、编译和调试程序,包括使用指针、结构体和系统调用。熟悉UNIX系统编程概念,如进程创建(fork、exec)和基本输入/输出处理。
3.3 推荐的阅读路线
如果基础不够扎实,建议按以下顺序建立知识体系:
- 先读《The C Programming Language》(K&R) —— 打好C语言基础
- 再读《UNIX环境高级编程》(APUE) —— 学习UNIX系统编程基础
- 最后读《UNIX网络编程》(UNP) —— 进阶网络编程
⚠️ 注意:本书不适合完全的编程初学者。如果你刚开始学编程,建议先完成前两步。
四、需要准备什么工作?
4.1 软件环境搭建
第一步:安装编译工具链
# Ubuntu/Debian
sudo apt update
sudo apt install build-essential gcc gdb make
# 验证安装
gcc --version
make --version
第二步:下载并编译本书源代码
本书源代码可从 http://www.unpbook.com/ 下载得到unpv13e.tar.gz:
# 1. 下载源码并解压
tar zxvf unpv13e.tar.gz
cd unpv13e
# 2. 添加执行权限并配置
chmod u+x configure
./configure
# 3. 编译lib文件
cd lib
make # 出现大量warning可忽略
cd ../libfree
make # 可能遇到错误(见下方解决方案)
cd ../libgai
make
# 4. 回到主目录
cd ..
⚠️ 常见编译错误:在libfree目录下编译inet_ntop.c时可能遇到size_t类型不匹配错误:
// 将inet_ntop.c第60行的 size_t size; // 修改为 socklen_t size;
第三步:设置头文件和库路径
# 1. 修改unp.h,将#include "../config.h"改为#include "config.h"
vim lib/unp.h
# 2. 拷贝头文件到系统目录
sudo cp config.h /usr/include
sudo cp lib/unp.h /usr/include
# 3. 拷贝静态库
sudo cp libunp.a /usr/lib
sudo cp libunp.a /usr/lib64
第四步:开启daytime服务并测试
# 安装xinetd
sudo apt-get install xinetd
# 编辑daytime配置
sudo vim /etc/xinetd.d/daytime
# 将两个disable后面的yes改成no
# 重启xinetd服务
sudo service xinetd restart
# 测试示例代码
cd intro
gcc daytimetcpcli.c -o daytimetcpcli -lunp
./daytimetcpcli 127.0.0.1
如果看到类似 01 JUL 2016 15:41:15 CST 的输出,说明环境配置成功。
五、学习注意事项与常见坑
5.1 常见的UNIX网络编程问题
| 问题类别 | 具体问题 | 解决方案 |
|---|---|---|
| 连接问题 | 如何建立和维护连接,如何处理中断的连接 | 使用心跳机制、设置超时重连 |
| 并发问题 | 如何处理多个客户端同时连接服务器 | 使用多进程、多线程或I/O复用 |
| 数据传输问题 | 分包、粘包问题 | 使用固定长度消息或消息分隔符 |
| 性能问题 | 如何优化网络传输速度,减少延迟 | 使用非阻塞I/O、零拷贝技术 |
| 安全性问题 | 如何保护网络通信的安全性 | 加密、身份验证、防攻击策略 |
| 错误处理 | 网络连接失败、数据传输异常 | 检查所有系统调用返回值 |
| 端口重用 | bind报"address already in use" | 设置SO_REUSEADDR套接字选项 |
| 跨平台问题 | 如何处理不同操作系统的网络特性 | 使用协议无关编程方法 |
5.2 理解TIME_WAIT的重要性
TIME_WAIT是TCP连接关闭时主动关闭方进入的状态,持续时间为2MSL(约1-4分钟)。TIME_WAIT状态有两个存在的理由:
- 可靠地实现TCP全双工连接的终止
- 允许老的重复分节在网络中消逝
当某端口处于TIME_WAIT状态时,是无法被绑定的(bind)。如果在项目中关闭服务器后希望立即重启,而程序却要等1-4分钟,SO_REUSEADDR套接字选项就可以用来解决这个问题,实现瞬间重启服务器。
5.3 错误处理最佳实践
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
void err_sys(const char* msg) {
perror(msg);
exit(1);
}
int Socket(int family, int type, int protocol) {
int n;
if ((n = socket(family, type, protocol)) < 0)
err_sys("socket error");
return n;
}
int Accept(int fd, struct sockaddr* addr, socklen_t* len) {
int n;
// 处理EINTR:被信号中断时重试
while ((n = accept(fd, addr, len)) < 0) {
if (errno != EINTR)
err_sys("accept error");
}
return n;
}
5.4 常见错误码速查
| 错误码 | 含义 | 典型场景 |
|---|---|---|
| EINTR | 系统调用被信号中断 | accept、read、write被信号打断 |
| EAGAIN/EWOULDBLOCK | 资源暂时不可用 | 非阻塞socket读无数据、写缓冲区满 |
| EADDRINUSE | 地址已在使用 | bind时端口已被占用 |
| ECONNREFUSED | 连接被拒绝 | connect时目标端口无服务 |
| ETIMEDOUT | 连接超时 | connect时SYN无响应 |
| EPIPE | 管道破裂 | 向已关闭的连接写数据 |
六、学习文档与读书笔记总结
6.1 核心概念辨析
字节序问题
网络字节序采用大端字节序,而系统使用的主机字节序可能是大端也可能是小端。需要使用转换函数:
| 函数 | 功能 |
|---|---|
| htons() | 主机字节序 → 网络字节序(16位) |
| htonl() | 主机字节序 → 网络字节序(32位) |
| ntohs() | 网络字节序 → 主机字节序(16位) |
| ntohl() | 网络字节序 → 主机字节序(32位) |
值-结果参数
套接字函数中经常出现“值-结果参数”,即参数既是输入值(告诉内核结构体的大小),又是输出结果(内核告诉应用程序实际存储了多少数据)。例如accept函数的第三个参数。
6.2 TCP协议栈架构图
┌─────────────────────────────────────────────────┐
│ 应用层 │
│ HTTP │ FTP │ SMTP │ Telnet │ ... │
├─────────────────────────────────────────────────┤
│ 传输层 │
│ TCP │ UDP │ SCTP │
├─────────────────────────────────────────────────┤
│ 网络层 │
│ IP │
├─────────────────────────────────────────────────┤
│ 链路层 │
│ Ethernet │ Wi-Fi │ PPP │
└─────────────────────────────────────────────────┘
6.3 完整的TCP回射服务器(多进程并发版)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BACKLOG 5
#define BUFFER_SIZE 1024
void sig_chld(int signo) {
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
void do_service(int conn_fd) {
char buffer[BUFFER_SIZE];
ssize_t n;
while ((n = read(conn_fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer[n] = '\0';
printf("Received: %s", buffer);
write(conn_fd, buffer, n);
}
close(conn_fd);
}
int main() {
int listen_fd, conn_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
exit(1);
}
if (listen(listen_fd, BACKLOG) < 0) {
perror("listen");
exit(1);
}
signal(SIGCHLD, sig_chld);
printf("Server listening on port %d\n", PORT);
while (1) {
conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd < 0) {
if (errno == EINTR)
continue;
perror("accept");
continue;
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
if (fork() == 0) {
close(listen_fd);
do_service(conn_fd);
close(conn_fd);
exit(0);
}
close(conn_fd);
}
close(listen_fd);
return 0;
}
6.4 epoll I/O复用服务器框架
#include <sys/epoll.h>
#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024
int epoll_fd, listen_fd, conn_fd;
struct epoll_event ev, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
// 1. 创建epoll实例
epoll_fd = epoll_create(1024);
// 2. 添加监听socket到epoll
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
// 3. 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
conn_fd = accept(listen_fd, NULL, NULL);
ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
ev.data.fd = conn_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev);
} else {
int fd = events[i].data.fd;
int n = read(fd, buffer, BUFFER_SIZE);
if (n == 0) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
} else if (n > 0) {
write(fd, buffer, n);
}
}
}
}
6.5 学习资源推荐
- 《UNIX网络编程 卷1:套接字联网API(第3版)》——导读(阿里云开发者社区):完整的目录导读和各章节链接
- Unix网络编程 3rd vol1 读书笔记(阿里云开发者社区):记录重要内容和经过深入理解的知识点
- 2021版!万字UNIX网络编程学习笔记(套接字篇)(腾讯云):涵盖环境搭建、套接字基础等
- UNIX网络编程之旅-配置unp.h头文件环境(阿里云开发者社区):详细的环境搭建指导
七、常见问题快速诊断
7.1 运行时常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
connect error: Connection refused |
daytime服务未开启 | 开启xinetd中的daytime服务 |
address already in use |
端口处于TIME_WAIT状态 | 设置SO_REUSEADDR套接字选项 |
fatal error: unp.h: No such file |
未正确配置头文件 | 将unp.h和config.h复制到/usr/include |
undefined reference to 'err_sys' |
未链接libunp.a | 编译时加上 -lunp 参数 |
inet_ntop.c: argument 'size' doesn't match prototype |
类型不匹配 | 将size_t改为socklen_t |
7.2 调试工具推荐
| 工具 | 用途 |
|---|---|
tcpdump / Wireshark |
抓包分析网络协议 |
netstat -ni / netstat -nr |
查看网络接口信息和路由表 |
strace |
跟踪系统调用 |
gdb |
调试程序 |
valgrind |
检查内存泄漏 |
八、第一章预告
📌 下一篇:《UNIX网络编程》读书笔记(一):第一章 简介
下一篇将详细讲解:
- 客户/服务器模型:深入理解网络编程的基本架构,Web浏览器与服务器之间的通信是如何组织的
- OSI模型与TCP/IP协议栈:理解各层职责,搞清楚网络数据包在每一层是如何封装的
- 协议无关性:如何编写同时支持IPv4和IPv6的网络程序,getaddrinfo函数族的妙用
- 包裹函数设计模式:学习Steven大神如何优雅地处理错误,以及err_sys系列函数的实现原理
- 时间获取客户/服务器程序详解:逐行解读
daytimetcpcli.c和daytimetcpsrv.c,分析每个系统调用的作用和潜在问题 - 测试环境配置:如何搭建和测试第一章的示例程序,验证TCP通信的基本流程
- 课后习题解析:第一章习题的解题思路和答案参考
学习目标:学完第一章后,你将能够:
- 理解网络编程的基本架构——客户/服务器模型
- 掌握包裹函数的错误处理设计模式
- 成功编译并运行第一个网络程序
- 为后续章节的深入学习打下坚实基础
敬请期待!
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
更多推荐


所有评论(0)