引言:从进程间通信到网络通信
作者: andylin02
学习章节: 第 11 章 网络编程
关键词: 客户端-服务器模型;套接字接口;socket/bind/listen/accept/connect;HTTP协议;Web服务器;Proxy Lab;并发服务器
“网络应用依赖于前面很多概念:进程、信号、字节顺序、内存映射以及动态内存分配。需要理解基本的客户端-服务器编程模型,以及如何编写使用因特网提供的服务的客户端-服务器程序。”——CSAPP作者
第 10 章我们学习了系统级 I/O,第 8 章学习了进程和信号,第 9 章学习了虚拟内存。第 11 章将这些知识融会贯通,深入探讨网络应用的构建方法。对主机而言,网络只是一种 I/O 设备,从网络上接收的数据从适配器经过 I/O 和内存总线复制到内存,通常是通过 DMA 传送。
本章将带你从零开始构建网络应用:先理解客户端-服务器模型和网络协议栈,再掌握套接字接口的核心函数,最终实现一个功能完整的 Web 服务器(Tiny Web Server),并为下一章第 12 章并发编程和 Proxy Lab 实验打下基础。
本章结构速览:
- 11.1 客户端-服务器编程模型:网络应用的基本架构与事务模型
- 11.2 网络:从以太网段到互联网络的层次结构
- 11.3 全球 IP 因特网:IP 地址、域名系统(DNS)、TCP 连接
- 11.4 套接字接口:socket、bind、listen、accept、connect 详解
- 11.5 Web 服务器:HTTP 协议、静态内容与动态内容(CGI)
- 11.6 Tiny Web 服务器实现:完整源码解析
- 11.7 配套实验:Proxy Lab——实现一个多线程带缓存的 Web 代理
💡 与第 10 章的关联:第 10 章的系统级 I/O(RIO 包)是本章网络编程的直接依赖——所有套接字读写都依赖 rio_readlineb 和 rio_writen 等函数。建议在阅读本章前先掌握第 10 章的内容。
一、客户端-服务器编程模型
1.1 基本概念
每个网络应用都是基于客户端-服务器模型的。采用这个模型,一个应用是由一个服务器进程和一个或多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。
💡 关键认知:客户端和服务器是进程,而不是机器或主机。一台主机可以同时运行许多不同的客户端和服务器,客户端和服务器的事务可以在同一台或不同的主机上。
1.2 客户端-服务器事务
一个客户端-服务器事务由以下四步组成:
┌─────────────────────────────────────────────────────────────────────┐
│ 客户端-服务器事务模型 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端进程 服务器进程 │
│ ┌─────────┐ ┌─────────┐ │
│ │ │ 1. 发送请求 │ │ │
│ │ │ ────────────────────→ │ │ │
│ │ │ │ 解释 │ │
│ │ │ 2. 操作资源 │ 请求 │ │
│ │ │ │ │ │
│ │ │ 3. 发送响应 │ │ │
│ │ │ ←──────────────────── │ │ │
│ │ 4. 处理 │ │ │ │
│ │ 响应 │ │ │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
- 步骤 1:客户端需要服务时,向服务器发送请求,发起一个事务(如 Web 浏览器请求一个文件)
- 步骤 2:服务器收到请求后解释它,并以适当方式操作其资源(如 Web 服务器读取磁盘文件)
- 步骤 3:服务器给客户端发送一个响应,并等待下一个请求
- 步骤 4:客户端收到响应并处理它(如浏览器将数据显示在屏幕上)
二、网络——从以太网到互联网络
2.1 网络层次结构
物理上,网络是一个按照地理远近组成的层次系统。最底层是局域网(LAN) ,最流行的局域网技术是以太网。
┌─────────────────────────────────────────────────────────────────────┐
│ 网络层次结构示意图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 层次 1: 以太网段(Ethernet Segment) │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │主机A│ │主机B│ │主机C│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ └──────────┼──────────┘ │
│ 集线器 │
│ │
│ 层次 2: 桥接以太网(Bridged Ethernet) │
│ [以太网段1] ──→ 网桥/交换机 ──→ [以太网段2] │
│ │
│ 层次 3: 互联网络(Internet) │
│ [局域网A] ──→ 路由器 ──→ [局域网B] │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 以太网段(Ethernet Segment)
每个以太网适配器都有一个全球唯一的 48 位地址(MAC 地址),存储在该适配器的非易失性存储器上。集线器不加分辨地将一个端口收到的每个位复制到其他所有端口,因此每个主机都能看到该帧,但只有目的主机实际读取它。
2.3 互联网络
多个不兼容的局域网可以通过叫做路由器的特殊计算机连接起来,组成一个互联网络(internet) 。互联网络的关键特性是,它能采用完全不同和不兼容技术的各种局域网和广域网组成。通过运行在每个主机和路由器上的协议软件,解决两大问题:
| 命名机制 | 定义一致的主机格式,唯一标识每台主机 |
| 传送机制 | 定义标准传送单元——包(packet),包含包头和有效载荷 |
三、全球 IP 因特网
3.1 TCP/IP 协议族
全球 IP 因特网是最著名和最成功的互联网络实现。每台因特网主机都运行实现 TCP/IP 协议的软件,客户端和服务器混合使用套接字接口函数和 Unix I/O 函数来进行通信。
┌─────────────────────────────────────────────────────────────────────┐
│ TCP/IP 协议族层次结构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 应用层 HTTP │ FTP │ SMTP │ … │
│ ───────────────────────────────────────────────────────────────── │
│ 传输层 TCP(可靠、全双工) │ UDP(进程间传送) │
│ ───────────────────────────────────────────────────────────────── │
│ 网络层 IP(主机间传送,不可靠) │
│ ───────────────────────────────────────────────────────────────── │
│ 链路层 以太网 / 令牌环 / … │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.2 IP 地址
一个 IP 地址就是一个 32 位的无符号整数。在 IP 地址结构中存放的地址总是以大端法(网络字节顺序)存放。通常用点分十进制来表示。
// IP 地址结构
struct in_addr {
uint32_t s_addr; // 网络字节顺序(大端)
};
// 点分十进制示例:0x8002c2f2 = 128.2.194.242
字节序转换函数:
| htonl | 32 位主机字节序 → 网络字节序 | 主机小,网络大 |
| htons | 16 位主机字节序 → 网络字节序 | |
| ntohl | 32 位网络字节序 → 主机字节序 | |
| ntohs | 16 位网络字节序 → 主机字节序 |
3.3 域名系统(DNS)
域名和 IP 地址的映射是通过分布世界范围内的数据库(DNS)来维护的。
# DNS 查询命令
hostname # 确定本地主机的实际域名
nslookup whleshark.ics.cs.cmu.edu # 查找域名对应的 IP 地址
💡 一个域名可能有多个 IP 地址(负载均衡、CDN 等场景)。
3.4 因特网连接
一个套接字是连接的一个端点,套接字地址为(IP 地址:端口号)。一个连接是由它两端的套接字地址唯一确定的。
端口类型:
| 临时端口 | 49152–65535 | 客户端发起连接时,由内核随机自动分配 |
| 知名端口 | 0–1023 | 服务器使用,如 Web 服务器使用 80,电子邮件使用 25 |
TCP 连接的特点:
- 点对点:连接两端是一对套接字
- 全双工:数据可以双向同时流动
- 可靠:保证数据按顺序、无丢失、无重复地到达
四、套接字接口
“套接字接口是一组函数,它们和 Unix I/O 函数结合起来,用以创建网络应用。”
4.1 套接字地址结构
// IP 套接字地址结构(具体地址)
struct sockaddr_in {
uint16_t sin_family; // 协议族(总是 AF_INET)
uint16_t sin_port; // 端口号(网络字节顺序)
struct in_addr sin_addr; // IP 地址(网络字节顺序)
unsigned char sin_zero[8]; // 填充,使总大小为 16 字节
};
// 通用套接字地址结构(用于函数参数)
struct sockaddr {
uint16_t sa_family; // 协议族
char sa_data[14]; // 地址数据
};
💡 所有套接字函数接受的地址参数类型都是 struct sockaddr *,因此在传递 sockaddr_in * 时需要强制类型转换。
4.2 核心套接字函数
┌─────────────────────────────────────────────────────────────────────┐
│ 客户端-服务器套接字调用流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 服务器 客户端 │
│ ┌─────────┐ ┌─────────┐ │
│ │ socket │ │ socket │ │
│ │ 创建 │ │ 创建 │ │
│ └────┬────┘ └────┬────┘ │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ bind │ │connect │ │
│ │ 绑定 │ │ 连接 │ │
│ └────┬────┘ └────┬────┘ │
│ ↓ │ │
│ ┌─────────┐ │ │
│ │ listen │ │ │
│ │ 监听 │ │ │
│ └────┬────┘ │ │
│ ↓ │ │
│ ┌─────────┐ 连接请求 ┌─────────┐ │ │
│ │ accept │←────────────────│connect │ │ │
│ │ 接受 │ │ │ │ │
│ └────┬────┘ └─────────┘ │ │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ rio_ │ 请求/响应(RIO I/O) │ rio_ │ │
│ │ read/wr │←──────────────────────────→│ read/wr │ │
│ └────┬────┘ └────┬────┘ │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ close │ │ close │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
① socket
int socket(int domain, int type, int protocol);
// 返回:成功返回套接字描述符,失败返回 -1
客户端和服务器使用 socket 函数来创建一个套接字描述符。
- domain:协议族(通常为 AF_INET 表示 IPv4)
- type:套接字类型(SOCK_STREAM 表示 TCP)
- protocol:协议(通常设为 0,表示默认协议)
② bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回:成功返回 0,失败返回 -1
bind 函数告诉内核将 addr 中的服务器套接字地址和套接字描述符 sockfd 联系起来。服务器用它将监听套接字绑定到知名端口。
③ listen
int listen(int sockfd, int backlog);
// 返回:成功返回 0,失败返回 -1
listen 函数将 sockfd 从一个主动套接字转化为监听套接字,该套接字可以接受来自客户端的连接请求。
- backlog:内核在开始拒绝连接请求前,队列中要排队的未完成连接请求的数量
④ accept
int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen);
// 返回:成功返回已连接描述符,失败返回 -1
accept 函数等待来自客户端的连接请求到达监听描述符 listenfd,利用传出参数 addr 来返回客户端的相关信息,并返回对应该客户端连接的通信文件描述符。
💡 两种描述符的区别:
- 监听描述符 (listenfd):服务器生命周期内持续存在,用于接收新的连接请求
- 已连接描述符 (connfd):每个客户端连接对应一个,通信完成后关闭
⑤ connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回:成功返回 0,失败返回 -1
客户端通过调用 connect 函数来建立和服务器的连接。connect 函数会阻塞,一直到连接成功建立或者发生错误。
⑥ getaddrinfo 与 getnameinfo
现代网络编程应优先使用这些函数,它们与协议无关,可同时处理 IPv4 和 IPv6。
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
// 返回:成功返回 0,失败返回非零错误码
getaddrinfo 将主机名、主机地址、服务名、端口号的字符串表示转换成套接字地址结构 addrinfo 链表。
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen, int flags);
// 返回:成功返回 0,失败返回非零错误码
getnameinfo 将套接字地址结构反向转换成主机名和服务名的字符串表示。
4.3 CSAPP 提供的封装函数
// 简化版封装(在 csapp.h 中定义)
int open_clientfd(char *hostname, char *port);
int open_listenfd(char *port);
open_clientfd 建立与运行在 hostname 主机上、监听 port 端口的服务器的连接。open_listenfd 打开并返回一个监听描述符,准备接受连接。
五、Web 服务器
5.1 HTTP 协议基础
Web 客户端和服务器之间的交互使用的是一个基于文本的应用级协议,叫做 HTTP(超文本传输协议) 。
HTTP 请求的格式:
<method> <uri> <version>
<headers>
<body>
示例:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Connection: close
HTTP 响应的格式:
<version> <status-code> <status-message>
<headers>
<body>
常见状态码:
| 200 | OK——请求成功 |
| 301 | Moved Permanently——资源已永久移动 |
| 302 | Found——资源临时移动 |
| 400 | Bad Request——请求格式错误 |
| 403 | Forbidden——禁止访问 |
| 404 | Not Found——资源不存在 |
| 500 | Internal Server Error——服务器内部错误 |
5.2 静态内容 vs 动态内容
Web 内容分静态内容和动态内容。
| 静态内容 | 客户端访问服务器,服务器返回指定的静态资源 | HTML 文件、图像、CSS |
| 动态内容 | 客户端请求页面,服务端根据请求参数动态生成页面 | CGI 程序、PHP、JSP |
静态内容:服务器读取磁盘文件,通过 HTTP 响应返回。
动态内容:服务器通过 fork + execve 执行 CGI(Common Gateway Interface)程序,将输出作为响应返回给客户端。
5.3 CGI(通用网关接口)
服务器使用 fork 和 execve 在子进程的环境中运行 CGI 程序。服务器通过环境变量向 CGI 程序传递请求参数,CGI 程序的输出(标准输出)被服务器捕获并返回给客户端。
关键环境变量:
| QUERY_STRING | GET 请求的参数 |
| REQUEST_METHOD | 请求方法(GET 或 POST) |
| CONTENT_LENGTH | POST 请求的内容长度 |
| REMOTE_ADDR | 客户端的 IP 地址 |
六、Tiny Web 服务器完整实现
Tiny 是一个功能齐全的 Web 服务器,能够为真实的 Web 浏览器提供静态的和动态的文本和图形内容。
6.1 主函数
#include "csapp.h"
void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);
int main(int argc, char **argv) {
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\\n", argv[0]);
exit(1);
}
listenfd = open_listenfd(argv[1]); // 创建监听套接字
while (1) {
clientlen = sizeof(clientaddr);
connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\\n", hostname, port);
doit(connfd); // 处理请求
close(connfd); // 关闭连接
}
}
6.2 doit 函数:处理 HTTP 请求
void doit(int fd) {
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
rio_readinitb(&rio, fd);
rio_readlineb(&rio, buf, MAXLINE);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio);
is_static = parse_uri(uri, filename, cgiargs);
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}
if (is_static) {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size);
} else {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs);
}
}
6.3 parse_uri:解析 URI
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr;
if (!strstr(uri, "cgi-bin")) { // 静态内容
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if (uri[strlen(uri) – 1] == '/')
strcat(filename, "home.html");
return 1;
} else { // 动态内容
ptr = index(uri, '?');
if (ptr) {
strcpy(cgiargs, ptr + 1);
*ptr = '\\0';
} else
strcpy(cgiargs, "");
sprintf(filename, ".%s", uri);
return 0;
}
}
6.4 serve_static:提供静态内容
void serve_static(int fd, char *filename, int filesize) {
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\\r\\n");
sprintf(buf, "%sServer: Tiny Web Server\\r\\n", buf);
sprintf(buf, "%sContent-length: %d\\r\\n", buf, filesize);
sprintf(buf, "%sContent-type: %s\\r\\n\\r\\n", buf, filetype);
rio_writen(fd, buf, strlen(buf));
srcfd = open(filename, O_RDONLY, 0);
srcp = mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
close(srcfd);
rio_writen(fd, srcp, filesize);
munmap(srcp, filesize);
}
6.5 serve_dynamic:提供动态内容(CGI)
void serve_dynamic(int fd, char *filename, char *cgiargs) {
char buf[MAXLINE], *emptylist[] = { NULL };
sprintf(buf, "HTTP/1.0 200 OK\\r\\n");
rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\\r\\n");
rio_writen(fd, buf, strlen(buf));
if (fork() == 0) { // 子进程执行 CGI 程序
setenv("QUERY_STRING", cgiargs, 1);
dup2(fd, STDOUT_FILENO); // 将标准输出重定向到套接字
execve(filename, emptylist, environ);
}
wait(NULL); // 父进程等待子进程结束
}
七、并发服务器
虽然第 12 章会详细展开,但本章的 Web 服务器实现为并发编程埋下了伏笔。在第 11 章中,我们实现的是迭代服务器(一次只处理一个客户端),而并发服务器可以同时处理多个客户端。
7.1 三种并发模型概览
| 基于进程 | accept 后 fork,每个子进程处理一个客户端 | 地址空间隔离,安全可靠 | 进程创建/切换开销大 |
| 基于 I/O 多路复用 | 单个进程使用 select/poll/epoll 监控多个描述符 | 开销小,共享数据容易 | 编程复杂度高 |
| 基于线程 | 创建多个线程共享进程地址空间 | 开销适中,共享数据容易 | 需要同步机制 |
基于进程的并发服务器核心模式:
while (1) {
connfd = accept(listenfd, ...);
if (fork() == 0) { // 子进程
close(listenfd); // 子进程关闭监听描述符
doit(connfd); // 处理客户端请求
close(connfd);
exit(0);
}
close(connfd); // 父进程关闭已连接描述符
}
💡 关键:子进程需要关闭从父进程继承的监听描述符,父进程需要关闭已连接描述符,以避免文件描述符泄漏。
八、配套实验:Proxy Lab
“Web Proxy 是 Web 浏览器和服务器之间的一个中间程序。本实验就是要实现一个支持多线程带缓存的 Web 代理。”
8.1 实验概览
Proxy Lab 是 CSAPP 的最后一个实验,分为三个部分:
| Part I | 实现一个最基本的顺序代理(转发请求和响应) | ★★☆ |
| Part II | 支持多线程并发处理请求(生产者-消费者模型) | ★★★ |
| Part III | 使用 LRU 策略缓存 Web 对象(读者-写者模型) | ★★★ |
8.2 Part I:顺序代理
主函数完全复制了 Tiny 服务器的框架,核心任务是在 doit 函数中实现代理逻辑:
8.3 Part II:多线程代理
使用生产者-消费者模型实现并发处理,每个客户端请求由一个独立的线程处理。使用互斥锁保护共享数据结构(如活跃客户端计数)。
8.4 Part III:LRU 缓存
使用 LRU(最近最少使用)策略缓存 Web 对象,减少重复请求的网络开销。需要处理读者-写者问题——多个线程可以同时读取缓存,但写入时必须独占。
8.5 关键技术点
// URL 解析(提取 host、端口、路径)
typedef struct URL {
char host[MAXLINE];
char port[MAXLINE];
char path[MAXLINE];
} URL;
void parseUrl(char *s, URL *url) {
char *ptr = strstr(s, "//");
if (ptr != NULL) s = ptr + 2;
ptr = strchr(s, '/');
if (ptr != NULL) {
strcpy(url->path, ptr);
*ptr = '\\0';
}
ptr = strchr(s, ':');
if (ptr != NULL) {
strcpy(url->port, ptr + 1);
*ptr = '\\0';
} else
strcpy(url->port, "80");
strcpy(url->host, s);
}
九、本章知识点思维导图
第 11 章 网络编程
│
├── 1. 客户端-服务器模型
│ ├── 服务器进程(管理资源)
│ ├── 客户端进程(请求服务)
│ └── 事务模型(请求→操作→响应→处理)
│
├── 2. 网络基础
│ ├── 以太网段(集线器、MAC 地址)
│ ├── 桥接以太网(网桥/交换机)
│ └── 互联网络(路由器、包)
│
├── 3. 全球 IP 因特网
│ ├── IP 地址(32 位整数,网络字节顺序)
│ ├── 域名系统(DNS 映射)
│ └── TCP 连接(点对点、全双工、可靠)
│
├── 4. 套接字接口
│ ├── socket(创建套接字)
│ ├── bind(绑定地址)
│ ├── listen(监听连接)
│ ├── accept(接受连接)
│ ├── connect(建立连接)
│ └── getaddrinfo / getnameinfo(地址转换)
│
├── 5. Web 服务器
│ ├── HTTP 协议(请求/响应格式)
│ ├── 静态内容 vs 动态内容(CGI)
│ └── Tiny Web 服务器实现
│
├── 6. 并发服务器
│ ├── 基于进程(fork)
│ ├── 基于 I/O 多路复用(select/poll/epoll)
│ └── 基于线程
│
└── 7. Proxy Lab
├── Part I:顺序代理
├── Part II:多线程代理
└── Part III:LRU 缓存
十、本章小结
第 11 章将网络通信的底层机制与应用层实现完美结合:
💡 本章最核心的三个洞察:
🔜 下一篇预告
下一章我们将进入 第 12 章:并发编程。
这一章将深入探讨构建高性能网络服务器的核心技术:
- 📌 并发编程基础:并发与并行的区别、并发编程的应用场景
- 📌 三种并发模型深度对比:
- 基于进程的并发编程(fork + 信号)
- 基于 I/O 多路复用的并发编程(select/poll/epoll)
- 基于线程的并发编程(Pthreads)
- 📌 线程同步:互斥锁、信号量、条件变量——避免竞争条件
- 📌 并发错误:死锁、数据竞争、饥饿的分析与防范
- 📌 配套实验:
- Malloc Lab(已在第 9 章介绍)
- Shell Lab(已在第 8 章介绍)
- Proxy Lab(已在第 11 章预告)
- Thread Safety 相关练习
第 12 章将帮助你掌握编写安全、高效并发程序的核心技术,为构建高性能服务器打下坚实基础。
敬请期待!
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!




