13 KiB
13 KiB
实验06 并发网络应用编程
实验目的
- 理解迭代式服务器与并发式服务器的区别
- 掌握多进程并发服务器的实现
- 掌握多线程并发服务器的实现
- 学会使用预线程化(prethreading)技术提高服务器性能
- 了解 Web 代理服务器的工作原理
- 掌握 I/O 多路复用(select)的基本使用
涉及知识点
- 迭代式 vs 并发式服务器模型
fork实现多进程并发pthread_create实现多线程并发- 预线程化线程池(生产者-消费者模型)
selectI/O 多路复用- 临界区保护与线程安全
- 代理服务器的工作原理
任务一:测试 togglesp / togglest / togglest_pre
任务要求
分别测试三种并发服务器模型:
| 模型 | 文件 | 说明 |
|---|---|---|
| 多进程 | togglesp.c |
每个连接 fork 一个子进程 |
| 多线程 | togglest.c |
每个连接创建一个线程 |
| 预线程化 | togglest_pre.c |
固定线程池 + 任务队列 |
操作步骤
# 分别编译三种服务器
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
多线程服务器核心代码
#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;
}
预线程化服务器核心代码
#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 一个子进程处理。
关键代码提示
#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 服务器改造为多线程并发模型:每个客户端连接创建一个线程处理。
关键代码提示
#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 服务器,使用固定线程池处理请求。支持根据负载动态增减线程数量。
关键代码提示
#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 代理服务器:
- 客户端连接代理,发送 HTTP 请求
- 代理解析请求中的目标 URL
- 代理向目标服务器发起请求
- 将目标服务器的响应转发给客户端
关键代码提示
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://、端口号、路径等 |
| 性能差 | 每次请求都新建连接 | 可实现连接池缓存 |
实验总结
通过本实验,应掌握以下能力:
- 区分迭代式和并发式服务器模型
- 使用
fork实现多进程并发服务器 - 使用
pthread实现多线程并发服务器 - 使用预线程化技术构建高性能服务器
- 理解线程池的工作原理和动态管理策略
- 了解 Web 代理服务器的实现方法