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