vault backup: 2026-06-13 23:46:22
This commit is contained in:
526
操作系统/实验/实验06_并发服务器.md
Normal file
526
操作系统/实验/实验06_并发服务器.md
Normal file
@@ -0,0 +1,526 @@
|
||||
# 实验06 并发网络应用编程
|
||||
|
||||
## 实验目的
|
||||
|
||||
1. 理解迭代式服务器与并发式服务器的区别
|
||||
2. 掌握多进程并发服务器的实现
|
||||
3. 掌握多线程并发服务器的实现
|
||||
4. 学会使用预线程化(prethreading)技术提高服务器性能
|
||||
5. 了解 Web 代理服务器的工作原理
|
||||
6. 掌握 I/O 多路复用(select)的基本使用
|
||||
|
||||
## 涉及知识点
|
||||
|
||||
- 迭代式 vs 并发式服务器模型
|
||||
- `fork` 实现多进程并发
|
||||
- `pthread_create` 实现多线程并发
|
||||
- 预线程化线程池(生产者-消费者模型)
|
||||
- `select` I/O 多路复用
|
||||
- 临界区保护与线程安全
|
||||
- 代理服务器的工作原理
|
||||
|
||||
---
|
||||
|
||||
## 任务一:测试 togglesp / togglest / togglest_pre
|
||||
|
||||
### 任务要求
|
||||
|
||||
分别测试三种并发服务器模型:
|
||||
|
||||
| 模型 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 多进程 | `togglesp.c` | 每个连接 fork 一个子进程 |
|
||||
| 多线程 | `togglest.c` | 每个连接创建一个线程 |
|
||||
| 预线程化 | `togglest_pre.c` | 固定线程池 + 任务队列 |
|
||||
|
||||
### 操作步骤
|
||||
|
||||
```bash
|
||||
# 分别编译三种服务器
|
||||
gcc -o togglesp togglesp.c -L. -lwrapper
|
||||
gcc -o togglest togglest.c -L. -lwrapper -lpthread
|
||||
gcc -o togglest_pre togglest_pre.c -L. -lwrapper -lpthread
|
||||
|
||||
# 测试多进程服务器
|
||||
./togglesp 8080 &
|
||||
./togglec localhost 8080
|
||||
|
||||
# 测试多线程服务器
|
||||
./togglest 8081 &
|
||||
./togglec localhost 8081
|
||||
|
||||
# 测试预线程化服务器
|
||||
./togglest_pre 8082 &
|
||||
./togglec localhost 8082
|
||||
```
|
||||
|
||||
### 多线程服务器核心代码
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_fd);
|
||||
void *serve_client(void *vargp);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_fd, conn_fd, *conn_fd_p;
|
||||
struct sockaddr_in clientaddr;
|
||||
socklen_t clientlen = sizeof(clientaddr);
|
||||
pthread_t tid;
|
||||
|
||||
listen_fd = open_listen_sock(atoi(argv[1]));
|
||||
|
||||
while (1) {
|
||||
conn_fd_p = malloc(sizeof(int));
|
||||
*conn_fd_p = Accept(listen_fd,
|
||||
(SA *)&clientaddr, &clientlen);
|
||||
Pthread_create(&tid, NULL, serve_client, conn_fd_p);
|
||||
}
|
||||
}
|
||||
|
||||
void *serve_client(void *vargp) {
|
||||
int conn_fd = *((int *)vargp);
|
||||
Pthread_detach(pthread_self());
|
||||
Free(vargp);
|
||||
toggle(conn_fd);
|
||||
Close(conn_fd);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
### 预线程化服务器核心代码
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
|
||||
#define NTHREADS 4
|
||||
#define SBUFSIZE 16
|
||||
|
||||
// Sbuf 结构(生产者-消费者缓冲区)
|
||||
typedef struct {
|
||||
int *buf;
|
||||
int n;
|
||||
int front;
|
||||
int rear;
|
||||
sem_t mutex;
|
||||
sem_t slots;
|
||||
sem_t items;
|
||||
} sbuf_t;
|
||||
|
||||
sbuf_t sbuf;
|
||||
|
||||
void sbuf_init(sbuf_t *sp, int n) {
|
||||
sp->buf = Calloc(n, sizeof(int));
|
||||
sp->n = n;
|
||||
sp->front = sp->rear = 0;
|
||||
Sem_init(&sp->mutex, 0, 1);
|
||||
Sem_init(&sp->slots, 0, n);
|
||||
Sem_init(&sp->items, 0, 0);
|
||||
}
|
||||
|
||||
void sbuf_insert(sbuf_t *sp, int item) {
|
||||
P(&sp->slots);
|
||||
P(&sp->mutex);
|
||||
sp->buf[(sp->rear++) % sp->n] = item;
|
||||
V(&sp->mutex);
|
||||
V(&sp->items);
|
||||
}
|
||||
|
||||
int sbuf_remove(sbuf_t *sp) {
|
||||
P(&sp->items);
|
||||
P(&sp->mutex);
|
||||
int item = sp->buf[(sp->front++) % sp->n];
|
||||
V(&sp->mutex);
|
||||
V(&sp->slots);
|
||||
return item;
|
||||
}
|
||||
|
||||
void *thread(void *vargp) {
|
||||
Pthread_detach(pthread_self());
|
||||
while (1) {
|
||||
int conn_fd = sbuf_remove(&sbuf);
|
||||
toggle(conn_fd);
|
||||
Close(conn_fd);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_fd = open_listen_sock(atoi(argv[1]));
|
||||
sbuf_init(&sbuf, SBUFSIZE);
|
||||
|
||||
for (int i = 0; i < NTHREADS; i++)
|
||||
Pthread_create(NULL, NULL, thread, NULL);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in clientaddr;
|
||||
socklen_t clientlen = sizeof(clientaddr);
|
||||
int conn_fd = Accept(listen_fd,
|
||||
(SA *)&clientaddr, &clientlen);
|
||||
sbuf_insert(&sbuf, conn_fd);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三种模型对比
|
||||
|
||||
| 特性 | 多进程 | 多线程 | 预线程化 |
|
||||
|------|--------|--------|----------|
|
||||
| 并发方式 | fork 子进程 | pthread_create | 固定线程池 |
|
||||
| 进程/线程数 | 动态增长 | 动态增长 | 固定 |
|
||||
| 创建开销 | 高 | 中 | 无(已预创建) |
|
||||
| 资源消耗 | 高(独立地址空间) | 低(共享地址空间) | 低 |
|
||||
| 编程复杂度 | 简单 | 中等 | 较高 |
|
||||
| 适用场景 | 连接数少 | 通用 | 高并发 |
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方法 |
|
||||
|------|------|----------|
|
||||
| 多进程服务器僵尸进程 | 子进程未回收 | 注册 `SIGCHLD` handler |
|
||||
| 多线程服务器段错误 | 线程间共享变量竞争 | 使用互斥锁保护共享数据 |
|
||||
| 预线程化服务器阻塞 | 缓冲区满 | 增大 SBUFSIZE 或增加线程数 |
|
||||
|
||||
---
|
||||
|
||||
## 任务二:task92.c —— 多进程 weblet
|
||||
|
||||
### 任务要求
|
||||
|
||||
将 weblet 服务器改造为多进程并发模型:每个客户端连接 fork 一个子进程处理。
|
||||
|
||||
### 关键代码提示
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
void sigchld_handler(int sig) {
|
||||
while (waitpid(-1, NULL, WNOHANG) > 0);
|
||||
}
|
||||
|
||||
void handle_request(int conn_fd) {
|
||||
// 读取 HTTP 请求
|
||||
char buf[8192];
|
||||
int n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) { close(conn_fd); return; }
|
||||
buf[n] = '\0';
|
||||
|
||||
// 解析请求行(GET /path HTTP/1.1)
|
||||
char method[16], path[256], version[16];
|
||||
sscanf(buf, "%s %s %s", method, path, version);
|
||||
|
||||
// 处理静态文件请求
|
||||
// ... 打开文件,发送 HTTP 响应头和文件内容 ...
|
||||
|
||||
close(conn_fd);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) { exit(1); }
|
||||
|
||||
signal(SIGCHLD, sigchld_handler);
|
||||
|
||||
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int optval = 1;
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
|
||||
&optval, sizeof(optval));
|
||||
|
||||
struct sockaddr_in servaddr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||
.sin_port = htons(atoi(argv[1]))
|
||||
};
|
||||
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||
listen(listen_fd, 1024);
|
||||
|
||||
printf("多进程 weblet 启动,端口 %s\n", argv[1]);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in cliaddr;
|
||||
socklen_t clien = sizeof(cliaddr);
|
||||
int conn_fd = accept(listen_fd,
|
||||
(struct sockaddr *)&cliaddr, &clien);
|
||||
|
||||
if (fork() == 0) {
|
||||
close(listen_fd);
|
||||
handle_request(conn_fd);
|
||||
exit(0);
|
||||
}
|
||||
close(conn_fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方法 |
|
||||
|------|------|----------|
|
||||
| 服务器响应慢 | fork 开销大 | 改用多线程或预线程化 |
|
||||
| 文件描述符泄漏 | 子进程继承了 listen_fd | 子进程中 `close(listen_fd)` |
|
||||
|
||||
---
|
||||
|
||||
## 任务三:task93.c —— 多线程 weblet
|
||||
|
||||
### 任务要求
|
||||
|
||||
将 weblet 服务器改造为多线程并发模型:每个客户端连接创建一个线程处理。
|
||||
|
||||
### 关键代码提示
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
void *handle_request(void *arg) {
|
||||
int conn_fd = *((int *)arg);
|
||||
free(arg);
|
||||
pthread_detach(pthread_self());
|
||||
|
||||
char buf[8192];
|
||||
int n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) { close(conn_fd); return NULL; }
|
||||
buf[n] = '\0';
|
||||
|
||||
// 解析并处理 HTTP 请求
|
||||
char method[16], path[256], version[16];
|
||||
sscanf(buf, "%s %s %s", method, path, version);
|
||||
|
||||
// 发送响应...
|
||||
// 注意:多个线程共享 listen_fd,但 conn_fd 是各线程独有的
|
||||
|
||||
close(conn_fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) { exit(1); }
|
||||
|
||||
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int optval = 1;
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
|
||||
&optval, sizeof(optval));
|
||||
|
||||
struct sockaddr_in servaddr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||
.sin_port = htons(atoi(argv[1]))
|
||||
};
|
||||
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||
listen(listen_fd, 1024);
|
||||
|
||||
printf("多线程 weblet 启动,端口 %s\n", argv[1]);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in cliaddr;
|
||||
socklen_t clien = sizeof(cliaddr);
|
||||
int *conn_fd = malloc(sizeof(int));
|
||||
*conn_fd = accept(listen_fd,
|
||||
(struct sockaddr *)&cliaddr, &clien);
|
||||
|
||||
pthread_t tid;
|
||||
pthread_create(&tid, NULL, handle_request, conn_fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方法 |
|
||||
|------|------|----------|
|
||||
| 线程数爆炸 | 每个请求创建新线程 | 改用预线程化或限制最大线程数 |
|
||||
| 线程不安全的函数 | `strtok`、`ctime` 等非线程安全 | 使用 `_r` 后缀的可重入版本 |
|
||||
| 内存泄漏 | 未 `free(arg)` 或未 `pthread_detach` | 确保线程退出前释放资源 |
|
||||
|
||||
---
|
||||
|
||||
## 任务四:task94.c —— 预线程化 weblet(动态增减线程)
|
||||
|
||||
### 任务要求
|
||||
|
||||
实现预线程化 weblet 服务器,使用固定线程池处理请求。支持根据负载动态增减线程数量。
|
||||
|
||||
### 关键代码提示
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
#include <semaphore.h>
|
||||
|
||||
#define MIN_THREADS 2
|
||||
#define MAX_THREADS 16
|
||||
#define SBUFSIZE 32
|
||||
|
||||
typedef struct {
|
||||
int *buf;
|
||||
int n, front, rear;
|
||||
sem_t mutex, slots, items;
|
||||
} sbuf_t;
|
||||
|
||||
sbuf_t sbuf;
|
||||
int current_threads = 0;
|
||||
pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void *worker(void *arg) {
|
||||
pthread_detach(pthread_self());
|
||||
pthread_mutex_lock(&count_mutex);
|
||||
current_threads++;
|
||||
pthread_mutex_unlock(&count_mutex);
|
||||
|
||||
while (1) {
|
||||
int conn_fd = sbuf_remove(&sbuf);
|
||||
|
||||
// 处理 HTTP 请求
|
||||
handle_http_request(conn_fd);
|
||||
close(conn_fd);
|
||||
|
||||
// 动态缩减:如果缓冲区长时间为空且线程数过多
|
||||
// 可在此处实现缩减逻辑
|
||||
}
|
||||
}
|
||||
|
||||
void *manager(void *arg) {
|
||||
// 监控线程:根据负载动态增减工作线程
|
||||
while (1) {
|
||||
sleep(5);
|
||||
|
||||
int pending = ...; // 获取待处理请求数
|
||||
pthread_mutex_lock(&count_mutex);
|
||||
|
||||
if (pending > current_threads && current_threads < MAX_THREADS) {
|
||||
// 扩容
|
||||
pthread_t tid;
|
||||
pthread_create(&tid, NULL, worker, NULL);
|
||||
} else if (pending == 0 && current_threads > MIN_THREADS) {
|
||||
// 缩减(通过向缓冲区插入特殊值 -1 实现)
|
||||
sbuf_insert(&sbuf, -1);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&count_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
sbuf_init(&sbuf, SBUFSIZE);
|
||||
|
||||
// 创建初始线程池
|
||||
for (int i = 0; i < MIN_THREADS; i++) {
|
||||
pthread_t tid;
|
||||
pthread_create(&tid, NULL, worker, NULL);
|
||||
}
|
||||
|
||||
// 创建管理线程
|
||||
pthread_t mgr_tid;
|
||||
pthread_create(&mgr_tid, NULL, manager, NULL);
|
||||
|
||||
// 主线程接受连接
|
||||
int listen_fd = open_listen_sock(atoi(argv[1]));
|
||||
while (1) {
|
||||
struct sockaddr_in cliaddr;
|
||||
socklen_t clien = sizeof(cliaddr);
|
||||
int conn_fd = accept(listen_fd,
|
||||
(struct sockaddr *)&cliaddr, &clien);
|
||||
sbuf_insert(&sbuf, conn_fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 动态增减策略
|
||||
|
||||
| 条件 | 操作 |
|
||||
|------|------|
|
||||
| 待处理请求数 > 当前线程数 且 < 最大线程数 | 创建新线程 |
|
||||
| 待处理请求数 = 0 且 当前线程数 > 最小线程数 | 终止一个线程 |
|
||||
| 线程数已达上限 | 等待(请求在缓冲区排队) |
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方法 |
|
||||
|------|------|----------|
|
||||
| 线程缩减无效 | 线程阻塞在 `sbuf_remove` | 发送特殊值唤醒线程 |
|
||||
| 线程数波动过大 | 扩缩策略过于敏感 | 设置冷却时间和阈值 |
|
||||
| 队列溢出 | SBUFSIZE 太小 | 增大缓冲区或动态扩容 |
|
||||
|
||||
---
|
||||
|
||||
## 任务五:task95.c —— Web 代理服务器(选做)
|
||||
|
||||
### 任务要求
|
||||
|
||||
实现一个 Web 代理服务器:
|
||||
|
||||
1. 客户端连接代理,发送 HTTP 请求
|
||||
2. 代理解析请求中的目标 URL
|
||||
3. 代理向目标服务器发起请求
|
||||
4. 将目标服务器的响应转发给客户端
|
||||
|
||||
### 关键代码提示
|
||||
|
||||
```c
|
||||
void handle_proxy(int client_fd) {
|
||||
char buf[8192];
|
||||
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) { close(client_fd); return; }
|
||||
buf[n] = '\0';
|
||||
|
||||
// 解析 HTTP 请求中的 URL
|
||||
char method[16], url[512], version[16];
|
||||
sscanf(buf, "%s %s %s", method, url, version);
|
||||
|
||||
// 解析主机名和端口
|
||||
char host[256];
|
||||
int port = 80;
|
||||
// url 格式: http://host:port/path
|
||||
// 解析 host 和 port ...
|
||||
|
||||
// 连接目标服务器
|
||||
int server_fd = open_client_sock(host, port);
|
||||
|
||||
// 转发请求
|
||||
send(server_fd, buf, n, 0);
|
||||
|
||||
// 转发响应
|
||||
while ((n = recv(server_fd, buf, sizeof(buf), 0)) > 0)
|
||||
send(client_fd, buf, n, 0);
|
||||
|
||||
close(server_fd);
|
||||
close(client_fd);
|
||||
}
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方法 |
|
||||
|------|------|----------|
|
||||
| HTTPS 站点无法代理 | 代理不支持 CONNECT 方法 | 仅支持 HTTP |
|
||||
| URL 解析错误 | 格式多样 | 仔细处理 `http://`、端口号、路径等 |
|
||||
| 性能差 | 每次请求都新建连接 | 可实现连接池缓存 |
|
||||
|
||||
---
|
||||
|
||||
## 实验总结
|
||||
|
||||
通过本实验,应掌握以下能力:
|
||||
|
||||
1. 区分迭代式和并发式服务器模型
|
||||
2. 使用 `fork` 实现多进程并发服务器
|
||||
3. 使用 `pthread` 实现多线程并发服务器
|
||||
4. 使用预线程化技术构建高性能服务器
|
||||
5. 理解线程池的工作原理和动态管理策略
|
||||
6. 了解 Web 代理服务器的实现方法
|
||||
Reference in New Issue
Block a user