Files
obsidian/操作系统/操作系统实践/实践01_Web服务器初步实现.md

15 KiB
Raw Blame History

实验一Web服务器的初步实现

[!info] 实验信息


一、实验目的

  1. 掌握 Linux 系统下网络编程开发环境的搭建
  2. 实现简单的单进程 Web 服务器
  3. 进行性能测试,掌握网络编程的基本技术
  4. 初步掌握 Web 服务器的编程实现和测试技术

二、实验环境

项目 说明
操作系统 Ubuntu / CentOS (Linux)
编译器 GCC
测试工具 http_load、ab、wrk
监控工具 vmstat、iostat、gprof
服务器端口 8088

三、源代码文件清单

.
├── common.h          # 公共头文件、函数声明、常量定义由wrapper.h简化而来
├── common.c          # RIO函数库和打开网络连接函数源代码
├── webclient.c       # web客户端源代码
├── webserver.c       # web服务器源代码即weblet.c
├── togglec.c         # toggle客户端源代码
├── togglesi.c        # 迭代式toggle服务器源代码
├── cgi-bin/
│   └── add.c         # CGI程序
├── Makefile          # 构建脚本
├── index.html        # 测试主页
├── test.html         # 测试页面
└── urls              # http_load测试用URL列表

[!tip] 文件说明

  • common.h / common.c 封装了网络编程常用的包装函数Socket、Bind、Listen、Accept 等),是对 wrapper.h 的简化版本
  • togglec.c / togglesi.c 用于演示迭代式客户-服务器模型
  • weblet.c 是核心的单进程 Web 服务器实现

四、服务器架构

4.1 整体架构图


graph TD
    subgraph 客户端
        Browser["浏览器 / Web客户端"]
    end

    subgraph Web服务器 - weblet
        Main["main() 主循环"]
        Accept["accept() 接受连接"]
        PT["process_trans() 处理HTTP事务"]
        Close["close() 关闭连接"]
    end

    subgraph 请求处理流程
        RR["读取请求行"]
        Judge["判断静态/动态请求"]
        Parse["解析URI"]
        Send["发送响应"]
        Err["错误处理"]
    end

    subgraph 响应模块
        FS["feed_static() 静态内容"]
        FD["feed_dynamic() 动态内容"]
        ER["error_request() 错误响应"]
    end

    Browser -->|"HTTP请求"| Main
    Main --> Accept
    Accept --> PT
    PT --> RR
    RR --> Judge
    Judge -->|"静态请求"| Parse
    Judge -->|"动态请求"| Parse
    Parse --> Send
    Send --> FS
    Send --> FD
    Send --> ER
    FS -->|"HTTP响应"| Browser
    FD -->|"HTTP响应"| Browser
    ER -->|"错误页面"| Browser
    PT --> Close
    Close -->|"等待下一个连接"| Accept



4.2 HTTP请求处理流程


flowchart TD
    Start(["开始: accept() 获取连接"]) --> ReadReq["1.读取请求行
    read_requesthdrs()"]
    ReadReq --> ParseReq["2.解析请求方法、URI、版本"]
    ParseReq --> IsStatic{"3.is_static()
    判断请求类型"}

    IsStatic -->|"静态请求"| ParseStatic["4a.parse_static_uri()
    解析静态文件URI"]
    IsStatic -->|"动态请求"| ParseDynamic["4b.parse_dynamic_uri()
    解析CGI程序URI"]

    ParseStatic --> OpenFile["5a.打开请求的文件"]
    ParseDynamic --> ExecCGI["5b.执行CGI程序"]

    OpenFile --> FileExist{"文件是否存在?"}
    FileExist -->|"是"| FeedStatic["6a.feed_static()
    发送静态文件响应"]
    FileExist -->|"否"| ErrorReq["6c.error_request()
    返回404错误"]

    ExecCGI --> FeedDynamic["6b.feed_dynamic()
    发送CGI执行结果"]

    FeedStatic --> Done(["结束: close()"])
    FeedDynamic --> Done
    ErrorReq --> Done




五、核心源码分析

5.1 主函数结构 (weblet.c)

/*
 * weblet.c - 一个简单的单进程迭代式Web服务器
 * 服务端口: 8088
 */
int main(int argc, char **argv) {
    int listen_sock, conn_sock;
    int hit;

    /* 创建监听套接字 */
    listen_sock = open_listen_sock(port);

    /* 主循环:迭代式处理每个请求 */
    for (hit = 1; ; hit++) {
        /* 接受客户端连接 */
        conn_sock = accept(listen_sock,
                           (struct sockaddr *)&client_addr,
                           &client_len);

        /* 处理一个HTTP事务 */
        process_trans(conn_sock, hit);

        /* 关闭连接,等待下一个请求 */
        close(conn_sock);
    }
}

[!warning] 迭代式服务器的局限 该服务器是单进程迭代式的,一次只能处理一个客户端请求。当某个请求处理耗时较长时,后续请求会被阻塞等待。改进方案参见 10_并发服务器 中的多进程和多线程服务器。

5.2 HTTP事务处理函数 (process_trans)

/*
 * process_trans - 处理一个HTTP事务
 * @fd:  连接套接字描述符
 * @hit: 请求计数
 */
void process_trans(int fd, int hit) {
    int is_static;          /* 是否为静态请求 */
    struct stat sbuf;       /* 文件状态 */
    char buf[MAXLINE];      /* 读缓冲区 */
    char method[MAXLINE];   /* 请求方法: GET */
    char uri[MAXLINE];      /* 请求URI */
    char version[MAXLINE];  /* HTTP版本 */
    char filename[MAXLINE]; /* 文件路径 */
    char cgiargs[MAXLINE];  /* CGI参数 */

    /* 1. 读取并解析请求行 */
    read_requesthdrs(buf);

    /* 2. 解析请求行: GET /index.html HTTP/1.1 */
    sscanf(buf, "%s %s %s", method, uri, version);

    /* 3. 判断是否为静态请求 */
    is_static = is_static(uri);

    /* 4. 解析URI提取文件名和CGI参数 */
    if (is_static) {
        parse_static_uri(uri, filename, cgiargs);
    } else {
        parse_dynamic_uri(uri, filename, cgiargs);
    }

    /* 5. 检查文件是否存在 */
    if (stat(filename, &sbuf) < 0) {
        error_request(fd, filename, "404", "Not Found");
        return;
    }

    /* 6. 发送响应 */
    if (is_static) {
        feed_static(fd, filename, sbuf.st_size);
    } else {
        feed_dynamic(fd, filename, cgiargs);
    }
}

5.3 RIO函数库 (common.c)

/*
 * RIO (Robust I/O) 健壮的I/O函数库
 * 解决了Unix I/O的不足:
 *  - 短读(short read)问题
 *  - 被信号中断(interrupted read)问题
 */

/* 无缓冲的RIO读取 */
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);

/* 带缓冲的RIO读取 */
void rio_readinitb(rio_t *rp, int fd);
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);

/* 网络连接函数 */
int open_listen_sock(int port);   /* 创建监听套接字 */
int open_client_sock(char *host, int port);  /* 创建客户端连接 */

六、MIME类型支持

服务器根据文件扩展名返回对应的 Content-Type

扩展名 MIME类型
.gif image/gif
.jpg / .jpeg image/jpeg
.png image/png
.ico image/x-icon
.zip application/zip
.gz / .tar application/x-tar
.htm / .html text/html

七、实验任务

任务1运行验证迭代式服务器togglec / togglesi

[!example] 操作步骤

# 终端1: 启动迭代式toggle服务器
$ ./togglesi

# 终端2: 运行toggle客户端
$ ./togglec localhost

此任务演示了09_网络编程基础中的基本客户-服务器通信模型。togglesi 是一个迭代式服务器,一次只服务一个客户端。

任务2编写跨计算机文件传输程序

实现 ftps.c(服务器)和 ftpc.c(客户端),支持以下命令:

  • put <文件名> —— 客户端上传文件到服务器
  • get <文件名> —— 客户端从服务器下载文件

[!abstract] 设计要点

  1. 使用 Socket API 建立 TCP 连接
  2. 先传输文件名和文件大小(协议头),再传输文件内容
  3. 使用 RIO 函数处理短读问题
  4. 服务器端需处理文件不存在的情况

任务3运行验证 Web 服务器

# 编译
$ make

# 启动Web服务器 (端口8088)
$ ./weblet 8088

# 在浏览器中访问
# http://localhost:8088/index.html
# http://localhost:8088/test.html

# 使用curl测试
$ curl -v http://localhost:8088/index.html

任务4使用 http_load 进行性能测试

# 准备URL列表文件 (urls)
$ cat urls
http://localhost:8088/index.html
http://localhost:8088/test.html

# 并行度5总请求数50
$ http_load -parallel 5 -fetches 50 urls

# 并行度10总请求数50
$ http_load -parallel 10 -fetches 50 urls

# 并行度5持续20秒
$ http_load -parallel 5 -seconds 20 urls

# 并行度10持续20秒
$ http_load -parallel 10 -seconds 20 urls

[!note] http_load 输出指标

  • fetches/sec: 每秒完成的请求数(吞吐量)
  • bytes/sec: 每秒传输的字节数
  • msecs/connect: 平均连接建立时间
  • msecs/first-response: 首字节响应时间

任务5使用 vmstat、iostat、gprof 收集性能数据

# ---- vmstat: 监控系统资源 ----
# 每2秒采样一次共10次
$ vmstat 2 10

# 重点关注:
#   r    - 运行队列长度
#   us   - 用户态CPU占比
#   sy   - 内核态CPU占比
#   wa   - I/O等待占比
#   free - 空闲内存

# ---- iostat: 监控磁盘I/O ----
# 以KB为单位每2秒采样一次共10次
$ iostat -k 2 10

# 重点关注:
#   tps   - 每秒传输次数
#   kB_read/s  - 每秒读取量
#   kB_wrtn/s  - 每秒写入量

# ---- gprof: 函数级性能分析 ----
# 编译时加 -pg 选项
$ gcc -pg -o weblet weblet.c common.c

# 运行服务器并处理若干请求后终止
$ ./weblet 8088

# 生成性能分析报告
$ gprof ./weblet gmon.out > perf.txt
$ cat perf.txt

任务6优化 Web 服务器

[!tip] 优化方向 — 参见 18_程序代码优化

抑制调试输出:将 printf 等调试输出替换为条件编译或日志级别控制。

/* 优化前: 每个请求都输出调试信息 */
void process_trans(int fd, int hit) {
    printf("Request %d: processing...\n", hit);
    // ...
}

/* 优化后: 仅在DEBUG模式下输出 */
#ifdef DEBUG
#define DBG_LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#define DBG_LOG(fmt, ...) /* nothing */
#endif

void process_trans(int fd, int hit) {
    DBG_LOG("Request %d: processing...\n", hit);
    // ...
}
# 编译优化版本 (关闭调试输出)
$ gcc -O2 -o weblet_opt weblet.c common.c

# 重新进行性能测试,对比优化前后结果
$ http_load -parallel 10 -fetches 50 urls

任务7优化服务器部署跨节点测试

# 服务器端 (节点A: 192.168.1.100)
$ ./weblet 8088

# 客户端 (节点B: 192.168.1.200)
# 修改urls文件中的地址
$ cat urls
http://192.168.1.100:8088/index.html
http://192.168.1.100:8088/test.html

# 跨节点压测
$ http_load -parallel 10 -seconds 20 urls

# 使用ab进行压测
$ ab -n 1000 -c 100 http://192.168.1.100:8088/

# 使用wrk进行压测
$ wrk -t4 -c100 -d10s http://192.168.1.100:8088/

八、性能测试命令汇总

# ===== http_load =====
http_load -parallel 5  -fetches 50 urls          # 并行550次请求
http_load -parallel 10 -fetches 50 urls          # 并行1050次请求
http_load -parallel 5  -seconds 20 urls          # 并行5持续20秒
http_load -parallel 10 -seconds 20 urls          # 并行10持续20秒

# ===== vmstat =====
vmstat 2 10                                       # 每2秒采样共10次

# ===== iostat =====
iostat -k 2 10                                    # KB为单位每2秒采样

# ===== gprof =====
gprof ./weblet gmon.out > perf.txt               # 生成函数调用分析

# ===== ab (Apache Bench) =====
ab -n 1000 -c 100 http://127.0.0.1:8080/         # 1000请求并发100

# ===== wrk =====
wrk -t4 -c100 -d10s http://127.0.0.1:8088/       # 4线程并发100持续10秒

九、Makefile 示例

CC = gcc
CFLAGS = -Wall -O2
DEBUG_CFLAGS = -Wall -g -pg -DDEBUG

all: weblet togglec togglesi webclient

weblet: weblet.c common.c common.h
	$(CC) $(CFLAGS) -o weblet weblet.c common.c

togglec: togglec.c common.c common.h
	$(CC) $(CFLAGS) -o togglec togglec.c common.c

togglesi: togglesi.c common.c common.h
	$(CC) $(CFLAGS) -o togglesi togglesi.c common.c

webclient: webclient.c common.c common.h
	$(CC) $(CFLAGS) -o webclient webclient.c common.c

debug: weblet.c common.c common.h
	$(CC) $(DEBUG_CFLAGS) -o weblet_debug weblet.c common.c

clean:
	rm -f weblet togglec togglesi webclient weblet_debug gmon.out perf.txt

十、实验结果与分析

10.1 性能对比记录表

测试条件 并行度 fetches/sec bytes/sec 备注
原始版本 5 - -
原始版本 10 - -
优化版本(关闭调试) 5 - -
优化版本(关闭调试) 10 - -
跨节点测试 10 - -

10.2 gprof 分析要点

[!abstract] 关键发现

  1. 识别 CPU 时间占比最高的函数
  2. 分析系统调用(readwriteaccept)的耗时
  3. 对比优化前后函数调用次数和耗时变化

10.3 分析与思考

  1. 迭代式服务器的瓶颈:单进程模型下,请求串行处理,并发性能受限。改进方案可参考 10_并发服务器 中的多进程 fork 模型、多线程 pthread 模型。

  2. 调试输出对性能的影响:频繁的 printf 会触发系统调用和缓冲区刷新,在高并发场景下成为性能瓶颈。

  3. 网络传输 vs 本地测试:跨节点测试引入了网络延迟,更接近真实场景,但本地测试更能反映服务器本身的处理能力。


十一、知识点总结


mindmap
  root((Web服务器实现))
    网络编程基础
      Socket API
        socket/bind/listen/accept
        connect/read/write
      RIO函数库
        健壮的I/O操作
        带缓冲读取
    服务器架构
      迭代式服务器
        单进程串行处理
        简单但并发性差
      并发式服务器
        多进程fork模型
        多线程pthread模型
    HTTP协议
      请求行解析
        GET / URI / HTTP/1.1
      MIME类型
        Content-Type头部
      静态/动态内容
        文件服务 vs CGI
    性能测试
      压测工具
        http_load
        ab / wrk
      系统监控
        vmstat / iostat
      代码分析
        gprof 函数级分析




十二、课后思考

  1. 如何将迭代式 Web 服务器改造为10_并发服务器?比较 forkpthreadselect/epoll 等方案的优劣。
  2. RIO 函数库解决了标准 I/O 的哪些问题?为什么不能直接使用 read() / write()
  3. 如何通过 18_程序代码优化 进一步提升 Web 服务器性能?(减少系统调用、零拷贝、内存映射等)
  4. 试分析 gprof 输出中各函数的调用关系和时间占比,找出性能热点。

[!quote] 参考资料

  • 《深入理解计算机系统》(CSAPP) 第11章网络编程
  • 《Unix网络编程》(UNP) 卷1套接字联网API
  • man pages: socket(2), bind(2), listen(2), accept(2), connect(2)