579 lines
15 KiB
Markdown
579 lines
15 KiB
Markdown
|
|
# 实验一: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)`
|