Files
obsidian/操作系统/10_并发服务器/10_并发服务器.md

396 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第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
```
### 示例2I/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)