# 实验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 #include #include #include #include #include #include #include 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 #include #include #include #include #include #include #include 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 #include #include #include #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 代理服务器的实现方法