【基于 Swoole+Hyperf 的微服务实战】 第一周·周一 理解 Swoole 的核心——Reactor 事件循环
第一周·周一 内容可以照着这个步骤学完一天,并在当天结束时完成一个具备并发能力的 TCP 回声服务器,同时深刻理解 Swoole 的核心——Reactor 事件循环。

今日目标
- 用 Docker 一键搭建 Swoole 5.x 开发环境,无需折腾编译。
- 吃透 Reactor 模型,理解 Swoole 如何用单进程处理海量并发连接。
- 动手编写一个纯 TCP 回声服务器,并观察同步阻塞与协程化的巨大差异。
- 能使用
telnet和多个终端窗口对自己的服务器进行并发交互测试。
一、环境搭建:Docker 与 Swoole 5.x (约 1 小时)
我们使用官方镜像,省去安装 PHP 和 Swoole 的繁琐过程。
1. 准备项目目录
在你的工作目录下新建文件夹,例如 swoole-course,后续所有代码都放在这里。
mkdir swoole-course && cd swoole-course
2. 编写 docker-compose.yml
这个文件定义了我们的 PHP-Swoole 容器,它会挂载当前目录到容器内的 /var/www,方便我们直接在宿主机写代码,容器内运行。
version: "3.8"
services:
swoole:
# 使用官方 Swoole 5.1 + PHP 8.2 镜像
image: phpswoole/swoole:5.1-php8.2
container_name: swoole-lab
volumes:
# 把当前目录映射到容器的 /var/www
- .:/var/www
working_dir: /var/www
ports:
# 将容器的 9501 端口映射到宿主机的 9501,用于后续 TCP 连接
- "9501:9501"
# 让容器持续运行,不至于立刻退出
tty: true
3. 启动环境
在 swoole-course 目录下执行:
docker-compose up -d
4. 进入容器并验证 Swoole
# 进入容器内的 Bash
docker-compose exec swoole bash
# 检查 PHP 版本
php -v
# 检查 Swoole 扩展及版本
php -m | grep swoole
php --ri swoole
当你看到 Swoole => enabled 和 Version => 5.1.x 时,环境就 OK 了。退出容器用 exit 即可,但我们下面的操作都在容器内进行。
二、知识核心:彻底搞懂 Reactor 事件循环 (约 1.5 小时)
类比理解(餐厅服务员):
一个传统 PHP-FPM 餐厅里,每来一个客人(请求),老板就临时雇一个服务员(进程/线程),服务完就解雇。1000 个客人同时来,就得雇 1000 个服务员,系统负担极重。
而 Swoole 的 Reactor 模型,就像一个超级服务员。他手里有一个记录板(事件循环),时刻关注所有客人的桌子(连接)。客人招手(有数据到达)时,他才过去处理一下,然后立刻回来继续看记录板。一个人就能同时服务成百上千个客人。
技术原理:
- 同步阻塞 (Blocking I/O):传统的
socket_read,如果没有数据,整个进程就卡住傻等,什么也做不了。 - I/O 多路复用 (epoll):操作系统提供一个机制,能同时监听成百上千个 socket。Swoole 的事件循环底层就是基于
epoll,它不断询问操作系统:“哪个 socket 有新数据?”,然后只去处理那些有事件的 socket。 - Reactor 模式:
- 事件循环 (EventLoop):一个无限循环,不停地等待事件发生。
- 事件分发:事件发生(新连接、数据到达、连接关闭),循环根据注册好的回调函数去执行相应逻辑。
- 非阻塞:回调函数必须快速执行,绝对不能有
sleep或阻塞的数据库查询,否则这个超级服务员就会被卡在那个客人的桌前,其他所有客人都被晾着了。
这就是今天我们要在代码里亲手验证的道理。
三、实战:纯 TCP 回声服务器与协程初体验 (约 2 小时)
所有代码都在宿主机 swoole-course 目录下编写,然后在容器内执行。
1. 基础版:单连接阻塞回声服务器
创建文件 tcp_echo_blocking.php:
<?php
// 创建一个 TCP 服务器,监听 0.0.0.0:9501
$server = new Swoole\Server('0.0.0.0', 9501);
// 配置参数
$server->set([
'worker_num' => 1, // 为了清晰演示阻塞效果,我们只开一个 Worker 进程
]);
// 监听连接建立事件
$server->on('Connect', function ($server, $fd) {
echo "新客户端接入,连接ID: {$fd}\n";
});
// 监听数据接收事件
$server->on('Receive', function ($server, $fd, $reactorId, $data) {
echo "收到来自连接 [{$fd}] 的数据: " . trim($data) . "\n";
// 模拟一个耗时业务操作,比如查询数据库(危险操作!)
echo "开始处理耗时任务(阻塞5秒)...\n";
sleep(5); // 这是同步阻塞的睡眠,整个进程会卡死!
// 发送回声
$server->send($fd, "服务器回复: " . $data);
});
// 监听连接关闭事件
$server->on('Close', function ($server, $fd) {
echo "连接 [{$fd}] 已关闭\n";
});
// 启动服务器
$server->start();
2. 观察基础版的阻塞
在容器内启动服务器:
docker-compose exec swoole bash
php tcp_echo_blocking.php
现在打开两个终端窗口,都执行 telnet localhost 9501。
- 在窗口 1 输入
hello1并回车。 - 立刻切换到窗口 2,输入
hello2并回车。 - 现象:窗口 1 会在 5 秒后收到回复。而窗口 2 没有任何反应,直到窗口 1 的 5 秒结束后,窗口 2 才开始处理并等待 5 秒。
结论:sleep(5) 阻塞了整个 Worker 进程的事件循环,导致它无法响应窗口 2 的连接。这就是 “Reactor 模型必须搭配非阻塞代码” 的原因。
3. 进阶版:协程化的非阻塞回声服务器
Swoole 提供了 Swoole\Coroutine\System::sleep() 来替代 sleep(),它会让出当前协程的控制权,Worker 进程可以立即去处理其他连接的请求。
创建文件 tcp_echo_coroutine.php:
<?php
$server = new Swoole\Server('0.0.0.0', 9501);
$server->set([
'worker_num' => 1,
// Swoole 5 默认开启协程,我们也可以显式开启以确保无误
'enable_coroutine' => true,
]);
$server->on('Connect', function ($server, $fd) {
echo "新客户端接入,连接ID: {$fd}\n";
});
$server->on('Receive', function ($server, $fd, $reactorId, $data) {
echo "收到来自连接 [{$fd}] 的数据: " . trim($data) . "\n";
echo "开始处理耗时任务(协程休眠5秒,非阻塞)...\n";
// 使用协程版本的 sleep,让出 CPU,事件循环可处理其他请求
Swoole\Coroutine\System::sleep(5);
$server->send($fd, "协程服务器回复: " . $data);
echo "任务完成,已回复连接 [{$fd}]\n";
});
$server->on('Close', function ($server, $fd) {
echo "连接 [{$fd}] 已关闭\n";
});
$server->start();
4. 观察非阻塞与并发处理
停止上一个 tcp_echo_blocking.php(Ctrl+C),运行新的:
php tcp_echo_coroutine.php
同样打开两个终端窗口,都用 telnet localhost 9501 连接。
- 在窗口 1 输入
hello1并回车。 - 立刻切换到窗口 2,输入
hello2并回车。 - 现象:你会看到两个窗口几乎同时收到了回声,而且都是在 5 秒后。
结论:通过协程,我们让这个单进程的超级服务员同时处理了两个耗时任务,完全没有任何阻塞。这就是 Swoole 高性能的核心秘密。
四、成果测试与方法检验 (约 1 小时)
1. 基础端口连通性测试
服务器运行时,在宿主机或容器内执行:
# 检查 9501 端口是否在监听
netstat -tulnp | grep 9501
# 应该看到一条 LISTEN 状态,进程为 php
2. telnet 并发交互测试
这是最直接的测试。分别用几个终端连接,发送不同消息,检验以下几点:
- 每个连接是否能立即收到回声?
- 回复内容是否与发送的完全一致?
- 如果一个连接断开(
Ctrl+],然后输入quit),服务器日志是否打印“已关闭”,且不影响其他连接?
3. 进阶思考与自测
- 任务:修改
tcp_echo_coroutine.php,在收到time命令时,返回当前服务器时间(date('Y-m-d H:i:s'))。 - 任务:尝试把
worker_num改为 2,重启服务器。用 4 个 telnet 连接,观察服务器日志中的进程 ID,看看连接是如何被分配到不同 Worker 进程的。 - 思考:为什么在
tcp_echo_coroutine.php的Receive回调中,即使不加Swoole\Coroutine\System::sleep,直接写sleep(5),它还会阻塞吗?会的,因为原生函数如果不做运行时协程化,就是阻塞的。我们将在后面学习Runtime::enableCoroutine()来一键让原生函数协程化。
今日作业与学习产出
- 绘制流程图:手绘或使用工具画出 Reactor 模型的事件循环流程图,包括
Connect、Receive、Close事件。 - 提交代码:将今天编写的两个
tcp_echo_*.php文件提交到 Git 仓库。 - 学习笔记:用自己的话解释同步阻塞和协程非阻塞的差异,并记录下你观察到的实验现象。
更多推荐
所有评论(0)