Files
obsidian/操作系统/实验/实验06_并发服务器.md

13 KiB
Raw Blame History

实验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 固定线程池 + 任务队列

操作步骤

# 分别编译三种服务器
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;
}

常见问题

问题 原因 解决方法
线程数爆炸 每个请求创建新线程 改用预线程化或限制最大线程数
线程不安全的函数 strtokctime 等非线程安全 使用 _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 代理服务器:

  1. 客户端连接代理,发送 HTTP 请求
  2. 代理解析请求中的目标 URL
  3. 代理向目标服务器发起请求
  4. 将目标服务器的响应转发给客户端

关键代码提示

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