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

579 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实验一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 # 并行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 示例
```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)`