【基于 Swoole+Hyperf 的微服务实战】第一周·周二:正式踏入 HTTP 世界,并亲手揭开 Swoole 协程调度的面纱
这是第一周周二课程的详细展开,我们将在周一 TCP 服务器的基础上,正式踏入 HTTP 世界,并亲手揭开 Swoole 协程调度的面纱。

今日目标
- 在 Swoole 中创建高性能 HTTP 服务器,理解其处理流程。
- 掌握 Request/Response 对象,能够处理 GET/POST 参数并返回 JSON 数据。
- 实现静态资源(如图片)的托管,理解浏览器加载原理。
- 深刻理解
go()协程的创建与调度,在请求中使用协程异步写日志,并肉眼观察并发奇迹。 - 使用
ab压测工具,用数据证明协程并发能力。
一、环境准备与工具安装 (约 30 分钟)
今日的所有代码依然在 swoole-course 目录下编写,在容器内运行。
1. 进入容器
cd swoole-course
docker-compose exec swoole bash
2. 安装 ab (Apache Bench) 压测工具 (宿主机)
容器内专注于运行 PHP 服务,压测我们直接在宿主机发起,结果更清晰。
- MacOS:自带
ab,无需安装。 - Linux (Debian/Ubuntu):
sudo apt-get update sudo apt-get install apache2-utils - Windows:可以下载 Apache 发行包,或者使用 Git Bash 自带的
ab,或使用 WSL。
验证安装:
ab -V
# 应显示 ApacheBench 版本信息
3. 创建今日工作目录
mkdir -p day2
cd day2
二、知识核心:HTTP 服务器与协程调度 (约 1.5 小时)
1. Swoole HTTP 服务器原理
Swoole 的 HTTP 服务器不是传统的 PHP-FPM,它是常驻内存的。它内部是一个 TCP 服务器,但专门解析 HTTP 协议。
-
流程图:
- 客户端发起 HTTP 请求。
- 服务器
onRequest事件触发。 - 回调函数接收
$request(请求信息) 和$response(响应对象)。 - 开发者处理逻辑,调用
$response->end()发送数据并结束此次请求。 - 服务器继续监听下一个请求。
-
常驻内存的优势:框架初始化、数据库连接池等只需做一次,后续请求直接复用,因此速度极快。
-
静态资源托管:Swoole 可以直接读取文件系统,把图片、CSS、JS 返回给浏览器,无需 Nginx。
2. 协程调度初探
周一我们使用了 Swoole\Coroutine\System::sleep(),今天要理解更主动的协程创建:go()。
go(callable):创建一个新的协程,立即返回,不阻塞当前代码。- 协程是用户态的轻量级线程,由 Swoole 的调度器管理。
- 调度时机:当协程遇到 I/O 操作(网络请求、文件读写、
System::sleep())时,会自动让出 CPU 给其他协程。这就是协作式调度。 - 核心实验:我们将在主请求处理流程中,用
go()启动一个协程去写日志。这会发生什么?- 主流程会继续执行,立刻返回响应给浏览器。
- 写日志的协程在后台执行,即使耗时较长(比如我们模拟 sleep),也完全不影响用户感受到的响应速度。
这就是异步非阻塞的灵魂。
三、实战:HTTP 服务、静态资源与协程日志 (约 2.5 小时)
1. 基础 HTTP 服务:返回 JSON
创建文件 http_server.php:
<?php
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$http->on('Request', function ($request, $response) {
// 打印请求方法和路径
echo "收到请求: {$request->server['request_method']} {$request->server['request_uri']}\n";
// 设置响应头为 JSON
$response->header('Content-Type', 'application/json');
// 构建一个数据数组
$data = [
'code' => 200,
'message' => 'Hello Swoole HTTP',
'method' => $request->server['request_method'],
'time' => date('Y-m-d H:i:s'),
];
// 将数组转为 JSON 并发送,end 结束响应
$response->end(json_encode($data, JSON_UNESCAPED_UNICODE));
});
$http->start();
测试:
在容器内启动服务器:
php http_server.php
用浏览器访问 http://localhost:9501 或使用 curl:
curl http://localhost:9501
你会看到 JSON 返回。尝试加参数:curl "http://localhost:9501/user?id=1",但在代码中我们先不用 $request->get。
关键点:$response->end() 是必须调用的,否则请求会一直挂起。
2. 处理 GET/POST 参数
修改 http_server.php,增加参数处理:
$http->on('Request', function ($request, $response) {
$uri = $request->server['request_uri'];
// 路由简易分发
if ($uri == '/json') {
$response->header('Content-Type', 'application/json');
$name = $request->get['name'] ?? 'World';
$response->end(json_encode(['greeting' => "Hello, {$name}!"]));
} elseif ($uri == '/form' && $request->server['request_method'] == 'POST') {
$response->header('Content-Type', 'application/json');
$username = $request->post['username'] ?? 'Guest';
$response->end(json_encode(['received' => $username]));
} else {
$response->status(404);
$response->end('Not Found');
}
});
测试:
curl "http://localhost:9501/json?name=Swoole"
curl -X POST -d "username=Hyperf" http://localhost:9501/form
3. 静态资源托管
假设我们有一张图片放在 static/logo.png。创建目录并放一张图(你可以随便找一个图片文件放进去)。
mkdir -p static
# 用你自己的图片复制进来,比如 logo.png
修改 http_server.php,在 onRequest 开头添加静态文件处理:
$http->on('Request', function ($request, $response) {
$uri = $request->server['request_uri'];
// --- 静态资源托管 ---
// 定义静态资源扩展名与 MIME 类型
$staticExt = ['png', 'jpg', 'jpeg', 'gif', 'css', 'js', 'html'];
$ext = pathinfo($uri, PATHINFO_EXTENSION);
if (in_array($ext, $staticExt)) {
$filePath = __DIR__ . '/static' . $uri;
if (file_exists($filePath)) {
// 自动设置 Content-Type
$response->header('Content-Type', mime_content_type($filePath));
$response->sendfile($filePath);
return; // 注意要结束,不再往下执行
} else {
$response->status(404);
$response->end('File Not Found');
return;
}
}
// ... 后续动态路由 ...
});
测试:浏览器访问 http://localhost:9501/logo.png,应显示图片。注意图片路径。
4. 重磅:协程并发——异步写日志
这是今天的精华。我们将创建一个协程去执行一个耗时的“写日志”任务(用文件写入模拟,并故意 sleep 2 秒),同时主请求立即返回。
创建文件 http_server_async.php,完整代码如下:
<?php
$http = new Swoole\Http\Server('0.0.0.0', 9501);
$http->on('Request', function ($request, $response) {
$uri = $request->server['request_uri'];
// 路由:触发异步日志
if ($uri == '/async-log') {
// 获取客户端 IP
$ip = $request->server['remote_addr'];
$time = date('Y-m-d H:i:s');
// 使用 go() 创建一个新协程,执行耗时日志记录
go(function () use ($ip, $time) {
// 模拟耗时操作:写日志之前先 sleep 2 秒
echo "[协程] 开始记录日志,休眠2秒...\n";
Swoole\Coroutine\System::sleep(2);
// 写入日志文件(追加模式)
$logMsg = "[{$time}] 用户 {$ip} 访问了 /async-log\n";
file_put_contents('/var/www/day2/access.log', $logMsg, FILE_APPEND);
echo "[协程] 日志写入完成。\n";
});
// 主请求立即返回,不等待协程完成
$response->header('Content-Type', 'application/json');
$response->end(json_encode([
'code' => 200,
'msg' => '请求已接收,日志正在后台记录',
'time' => $time
]));
} else {
$response->status(404);
$response->end('Not Found');
}
});
$http->start();
启动并观察:
php http_server_async.php
打开两个终端:
- 终端 A:使用
curl连续快速发送请求:curl http://localhost:9501/async-log curl http://localhost:9501/async-log - 终端 B:实时查看日志文件变化:
tail -f /var/www/day2/access.log
预期现象与原理分析:
- 每次
curl都立刻返回 JSON 响应,不会等待 2 秒。 - 约 2 秒后,
access.log文件中会出现两条日志记录,且时间戳是发起请求的时间。 - 观察服务器控制台输出:
这说明多个协程并发执行了[协程] 开始记录日志,休眠2秒... [协程] 开始记录日志,休眠2秒... (约2秒后) [协程] 日志写入完成。 [协程] 日志写入完成。sleep,而不是顺序各等2秒。总耗时约2秒(而非4秒),证明协程是并发的。
这就是 go() 的神奇之处:它把同步的阻塞操作变成了并发非阻塞。在实际项目中,你可以用同样的方式异步发送邮件、推送消息、写入数据库日志。
四、成果测试与性能初探 (约 1 小时)
1. 使用 ab 压测,观察协程威力
我们依然使用 http_server_async.php,并为其增加一个不开启协程的对比路由。修改代码,增加:
// 同步阻塞版本(危险!仅用于对比)
else if ($uri == '/sync-log') {
$ip = $request->server['remote_addr'];
$time = date('Y-m-d H:i:s');
// 直接阻塞 sleep
echo "同步处理开始,阻塞2秒...\n";
sleep(2);
file_put_contents('/var/www/day2/access.log', "[{$time}] 同步用户 {$ip}\n", FILE_APPEND);
$response->end(json_encode(['msg' => 'sync done']));
}
压测异步接口:
# 并发10,总请求100
ab -n 100 -c 10 http://localhost:9501/async-log
查看报告中的 Time per request(平均响应时间)和 Failed requests。由于异步立即返回,响应时间应该非常短,且没有失败。
压测同步接口(谨慎,可能会超时):
# 并发10,总请求20 就好,因为每个请求阻塞2秒,会很慢
ab -n 20 -c 10 http://localhost:9501/sync-log
你会发现平均响应时间非常长,而且很多请求可能因为超过 ab 的默认超时时间而失败。
思考:为什么同步接口在并发下如此糟糕?因为 Worker 进程被 sleep(2) 阻塞,事件循环无法处理新的请求,导致请求排队,总耗时成倍增加。
2. 进阶自测任务
- 任务一:改造代码,使用
go()同时发起 3 个不同的“耗时任务”(比如记录不同日志),观察它们是否并发执行,总耗时是多少。 - 任务二:使用
Swoole\Coroutine\WaitGroup,让主请求等待所有日志协程完成后再返回响应。(提示:$wg = new WaitGroup(); $wg->add(); go() ... $wg->done(); $wg->wait();) - 任务三:给静态资源托管增加缓存头(
Cache-Control),体验常驻内存下自定义响应头的便利。
3. 测试检验清单
| 检验项 | 方法 | 通过标准 |
|---|---|---|
| HTTP 服务返回 JSON | curl 访问,检查 Content-Type | 返回 application/json 且结构正确 |
| GET/POST 参数解析 | curl -X POST -d "k=v" | 正确接收并响应 |
| 静态图片访问 | 浏览器访问 /logo.png | 图片显示,状态码 200 |
| 异步日志并发写入 | 两次快速 curl,查看 access.log 及控制台输出 | 请求立即返回,2秒后日志出现且时间戳正确 |
| ab 压测异步接口 | ab -n 100 -c 10 | 失败请求为0,平均响应时间 < 50ms |
| ab 压测同步接口 | 相同参数 | 响应时间极大,或出现失败 |
今日作业与学习产出
- 提交代码:将
http_server.php、http_server_async.php和static目录提交至 Git。 - 压测截图:将异步接口和同步接口的
ab压测结果截图,并附上你的分析结论。 - 协程笔记:画出
go()协程的并发执行流程图,标注主请求和协程之间的关系,以及何时让出 CPU。 - 挑战:实现
WaitGroup版本,使得请求在等待所有日志协程完成后才返回,对比与立即返回的场景差异。
通过今天的实战,你已经掌握了 Swoole HTTP 服务的核心,并亲眼见证了协程如何用单进程实现高并发。明天我们将利用 Hyperf 框架再次实现这一切,感受框架的强大封装。
更多推荐
所有评论(0)