vault backup: 2026-06-13 23:46:22
This commit is contained in:
395
操作系统/10_并发服务器/10_并发服务器.md
Normal file
395
操作系统/10_并发服务器/10_并发服务器.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 第10讲:并发网络服务器
|
||||
|
||||
> 🎯 **本节目标**:掌握多进程、多线程、I/O 多路复用三种并发服务器模型
|
||||
|
||||
## 📋 前置知识
|
||||
- [[09_网络编程]] — Socket 编程基础
|
||||
- [[06_进程控制]] — 进程创建
|
||||
- [[07_多线程编程]] — 线程创建
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。
|
||||
|
||||
**并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。
|
||||
|
||||
**生活比喻**:
|
||||
- **迭代服务器** = 一个服务员一次只服务一桌客人
|
||||
- **多进程服务器** = 每来一桌客人就招一个新服务员
|
||||
- **多线程服务器** = 一个服务员同时照看多桌客人
|
||||
- **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 三种并发模型
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[并发服务器模型] --> B[多进程模型]
|
||||
A --> C[多线程模型]
|
||||
A --> D[I/O 多路复用]
|
||||
|
||||
B --> B1[每连接一个进程]
|
||||
B --> B2[进程间隔离]
|
||||
B --> B3[开销大]
|
||||
|
||||
C --> C1[每连接一个线程]
|
||||
C --> C2[共享内存]
|
||||
C --> C3[开销较小]
|
||||
|
||||
D --> D1[单线程处理多连接]
|
||||
D --> D2[select/poll/epoll]
|
||||
D --> D3[开销最小]
|
||||
|
||||
style B fill:#ffcdd2
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**对比**:
|
||||
| 模型 | 优点 | 缺点 | 适用场景 |
|
||||
|------|------|------|----------|
|
||||
| 多进程 | 隔离性好 | 开销大 | 连接数少 |
|
||||
| 多线程 | 开销较小 | 需要同步 | 连接数中等 |
|
||||
| I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 |
|
||||
|
||||
### 2. 多进程模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 主进程
|
||||
participant 子进程1
|
||||
participant 子进程2
|
||||
participant 客户端
|
||||
|
||||
主进程->>主进程: accept() 等待连接
|
||||
客户端->>主进程: 连接请求
|
||||
主进程->>子进程1: fork() 创建子进程
|
||||
子进程1->>客户端: 处理请求
|
||||
主进程->>主进程: 继续 accept()
|
||||
|
||||
客户端->>主进程: 新连接请求
|
||||
主进程->>子进程2: fork() 创建子进程
|
||||
子进程2->>客户端: 处理请求
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 每个连接一个独立进程
|
||||
- 进程间完全隔离
|
||||
- 进程创建和销毁开销大
|
||||
|
||||
### 3. 多线程模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 主线程
|
||||
participant 工作线程1
|
||||
participant 工作线程2
|
||||
participant 客户端
|
||||
|
||||
主线程->>主线程: accept() 等待连接
|
||||
客户端->>主线程: 连接请求
|
||||
主线程->>工作线程1: pthread_create()
|
||||
工作线程1->>客户端: 处理请求
|
||||
主线程->>主线程: 继续 accept()
|
||||
|
||||
客户端->>主线程: 新连接请求
|
||||
主线程->>工作线程2: pthread_create()
|
||||
工作线程2->>客户端: 处理请求
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 每个连接一个线程
|
||||
- 线程共享进程资源
|
||||
- 需要注意线程安全
|
||||
|
||||
### 4. I/O 多路复用(select)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主循环] --> B[select 监听所有 fd]
|
||||
B --> C{哪个 fd 就绪?}
|
||||
C -->|监听套接字| D[accept 新连接]
|
||||
C -->|客户端套接字| E[处理请求]
|
||||
D --> A
|
||||
E --> A
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**select 的工作原理**:
|
||||
1. 将所有需要监听的文件描述符放入集合
|
||||
2. 调用 `select()` 等待任意一个就绪
|
||||
3. 遍历集合,处理就绪的描述符
|
||||
4. 重复步骤 1
|
||||
|
||||
**fd_set 操作**:
|
||||
```c
|
||||
fd_set read_set;
|
||||
FD_ZERO(&read_set); // 清空集合
|
||||
FD_SET(fd1, &read_set); // 添加 fd1
|
||||
FD_SET(fd2, &read_set); // 添加 fd2
|
||||
select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待
|
||||
if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪
|
||||
// 处理 fd1
|
||||
```
|
||||
|
||||
### 5. 线程池模型
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主线程] -->|accept| B[任务队列]
|
||||
B --> C[工作线程1]
|
||||
B --> D[工作线程2]
|
||||
B --> E[工作线程3]
|
||||
C -->|处理完毕| B
|
||||
D -->|处理完毕| B
|
||||
E -->|处理完毕| B
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 避免频繁创建销毁线程
|
||||
- 控制并发数量
|
||||
- 提高资源利用率
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:多线程并发服务器
|
||||
|
||||
```c
|
||||
// togglest.c - 多线程并发服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock);
|
||||
void *serve_client(void *vargp);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port, *conn_sock_p;
|
||||
struct sockaddr_in clientaddr;
|
||||
struct hostent *hp;
|
||||
char *haddrp;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
pthread_t tid;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
|
||||
listen_sock = open_listen_sock(port);
|
||||
|
||||
while (1) {
|
||||
conn_sock_p = malloc(sizeof(int));
|
||||
*conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
|
||||
// 获取客户端信息
|
||||
hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
|
||||
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
|
||||
haddrp = inet_ntoa(clientaddr.sin_addr);
|
||||
printf("server connected to %s (%s)\n", hp->h_name, haddrp);
|
||||
|
||||
// 创建新线程处理客户端
|
||||
pthread_create(&tid, NULL, serve_client, conn_sock_p);
|
||||
}
|
||||
}
|
||||
|
||||
void *serve_client(void *vargp) {
|
||||
int conn_sock = *((int *)vargp);
|
||||
pthread_detach(pthread_self()); // 分离线程
|
||||
free(vargp);
|
||||
toggle(conn_sock);
|
||||
close(conn_sock);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o togglest togglest.c -L. -lwrapper -lpthread
|
||||
|
||||
# 终端1:启动服务器
|
||||
./togglest 8080
|
||||
|
||||
# 终端2-4:启动多个客户端
|
||||
./togglec localhost 8080
|
||||
```
|
||||
|
||||
### 示例2:I/O 多路复用服务器
|
||||
|
||||
```c
|
||||
// toggless1.c - select 多路复用服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock);
|
||||
void read_input(void);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
struct sockaddr_in clientaddr;
|
||||
fd_set read_set, ready_set;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
listen_sock = open_listen_sock(port);
|
||||
|
||||
// 初始化 fd_set
|
||||
FD_ZERO(&read_set);
|
||||
FD_SET(STDIN_FILENO, &read_set); // 监听标准输入
|
||||
FD_SET(listen_sock, &read_set); // 监听套接字
|
||||
|
||||
while (1) {
|
||||
ready_set = read_set;
|
||||
select(listen_sock + 1, &ready_set, NULL, NULL, NULL);
|
||||
|
||||
// 检查标准输入
|
||||
if (FD_ISSET(STDIN_FILENO, &ready_set))
|
||||
read_input();
|
||||
|
||||
// 检查新连接
|
||||
if (FD_ISSET(listen_sock, &ready_set)) {
|
||||
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
toggle(conn_sock);
|
||||
close(conn_sock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void read_input(void) {
|
||||
char buf[MAXLINE];
|
||||
if (!fgets(buf, MAXLINE, stdin))
|
||||
exit(0);
|
||||
printf("%s", buf);
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:连接池服务器
|
||||
|
||||
```c
|
||||
// toggless2.c - 连接池服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct {
|
||||
int maxfd;
|
||||
fd_set read_set;
|
||||
fd_set ready_set;
|
||||
int nready;
|
||||
int maxi;
|
||||
int client_sock[FD_SETSIZE];
|
||||
} sock_pool;
|
||||
|
||||
void init_sock_pool(int listen_sock, sock_pool *pool);
|
||||
void add_sock(int conn_sock, sock_pool *pool);
|
||||
void serve_clients(sock_pool *pool);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
struct sockaddr_in clientaddr;
|
||||
static sock_pool pool;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
|
||||
listen_sock = open_listen_sock(port);
|
||||
init_sock_pool(listen_sock, &pool);
|
||||
|
||||
while (1) {
|
||||
pool.ready_set = pool.read_set;
|
||||
pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL);
|
||||
|
||||
// 新连接
|
||||
if (FD_ISSET(listen_sock, &pool.ready_set)) {
|
||||
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
add_sock(conn_sock, &pool);
|
||||
}
|
||||
|
||||
// 处理客户端请求
|
||||
serve_clients(&pool);
|
||||
}
|
||||
}
|
||||
|
||||
void init_sock_pool(int listen_sock, sock_pool *p) {
|
||||
int i;
|
||||
p->maxi = -1;
|
||||
for (i = 0; i < FD_SETSIZE; i++)
|
||||
p->client_sock[i] = -1;
|
||||
p->maxfd = listen_sock;
|
||||
FD_ZERO(&p->read_set);
|
||||
FD_SET(listen_sock, &p->read_set);
|
||||
}
|
||||
|
||||
void add_sock(int conn_sock, sock_pool *p) {
|
||||
int i;
|
||||
p->nready--;
|
||||
for (i = 0; i < FD_SETSIZE; i++)
|
||||
if (p->client_sock[i] < 0) {
|
||||
p->client_sock[i] = conn_sock;
|
||||
FD_SET(conn_sock, &p->read_set);
|
||||
if (conn_sock > p->maxfd)
|
||||
p->maxfd = conn_sock;
|
||||
if (i > p->maxi)
|
||||
p->maxi = i;
|
||||
break;
|
||||
}
|
||||
if (i == FD_SETSIZE)
|
||||
perror("add_sock error: Too many clients");
|
||||
}
|
||||
|
||||
void serve_clients(sock_pool *p) {
|
||||
int i, conn_sock, n;
|
||||
char buf[MAXLINE];
|
||||
|
||||
for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {
|
||||
conn_sock = p->client_sock[i];
|
||||
if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) {
|
||||
p->nready--;
|
||||
if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) {
|
||||
printf("Server received %d bytes on fd %d\n", n, conn_sock);
|
||||
send(conn_sock, buf, n, 0);
|
||||
} else {
|
||||
close(conn_sock);
|
||||
FD_CLR(conn_sock, &p->read_set);
|
||||
p->client_sock[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 多进程模型在 [[06_进程控制]] 中有详细讲解
|
||||
- 多线程模型在 [[07_多线程编程]] 中有详细讲解
|
||||
- I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制?
|
||||
2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适?
|
||||
3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX网络编程》第1卷:第6章、第16章
|
||||
- [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html)
|
||||
- [高性能网络编程](https://www.zhihu.com/question/28594409)
|
||||
Reference in New Issue
Block a user