# 实验一:Web服务器的初步实现 > [!info] 实验信息 > - **课程**: 操作系统实践 > - **实验编号**: 实验一 > - **实验类型**: 综合性实验 > - **关联理论**: [[09_网络编程基础]]、[[10_并发服务器]]、[[18_程序代码优化]] --- ## 一、实验目的 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 整体架构图 ```mermaid 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请求处理流程 ```mermaid 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`) ```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`) ```c /* * 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`) ```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] 操作步骤 ```bash # 终端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 服务器 ```bash # 编译 $ 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 进行性能测试 ```bash # 准备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 收集性能数据 ```bash # ---- 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` 等调试输出替换为条件编译或日志级别控制。 ```c /* 优化前: 每个请求都输出调试信息 */ 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); // ... } ``` ```bash # 编译优化版本 (关闭调试输出) $ gcc -O2 -o weblet_opt weblet.c common.c # 重新进行性能测试,对比优化前后结果 $ http_load -parallel 10 -fetches 50 urls ``` ### 任务7:优化服务器部署(跨节点测试) ```bash # 服务器端 (节点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/ ``` --- ## 八、性能测试命令汇总 ```bash # ===== http_load ===== http_load -parallel 5 -fetches 50 urls # 并行5,50次请求 http_load -parallel 10 -fetches 50 urls # 并行10,50次请求 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 示例 ```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. 分析系统调用(`read`、`write`、`accept`)的耗时 > 3. 对比优化前后函数调用次数和耗时变化 ### 10.3 分析与思考 1. **迭代式服务器的瓶颈**:单进程模型下,请求串行处理,并发性能受限。改进方案可参考 [[10_并发服务器]] 中的多进程 fork 模型、多线程 pthread 模型。 2. **调试输出对性能的影响**:频繁的 `printf` 会触发系统调用和缓冲区刷新,在高并发场景下成为性能瓶颈。 3. **网络传输 vs 本地测试**:跨节点测试引入了网络延迟,更接近真实场景,但本地测试更能反映服务器本身的处理能力。 --- ## 十一、知识点总结 ```mermaid 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_并发服务器|并发服务器]]?比较 `fork`、`pthread`、`select`/`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)`