【基于 Swoole+Hyperf 的微服务实战】 第一周·周一 理解 Swoole 的核心——Reactor 事件循环
2026/8/30 5:05:26 网站建设 项目流程

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


今日目标

  1. 用 Docker 一键搭建 Swoole 5.x 开发环境,无需折腾编译。
  2. 吃透 Reactor 模型,理解 Swoole 如何用单进程处理海量并发连接。
  3. 动手编写一个纯 TCP 回声服务器,并观察同步阻塞与协程化的巨大差异。
  4. 能使用telnet和多个终端窗口对自己的服务器进行并发交互测试。

一、环境搭建:Docker 与 Swoole 5.x (约 1 小时)

我们使用官方镜像,省去安装 PHP 和 Swoole 的繁琐过程。

1. 准备项目目录

在你的工作目录下新建文件夹,例如swoole-course,后续所有代码都放在这里。

mkdirswoole-course&&cdswoole-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.2container_name:swoole-labvolumes:# 把当前目录映射到容器的 /var/www-.:/var/wwwworking_dir:/var/wwwports:# 将容器的 9501 端口映射到宿主机的 9501,用于后续 TCP 连接-"9501:9501"# 让容器持续运行,不至于立刻退出tty:true
3. 启动环境

swoole-course目录下执行:

docker-composeup-d
4. 进入容器并验证 Swoole
# 进入容器内的 Bashdocker-composeexecswoolebash# 检查 PHP 版本php-v# 检查 Swoole 扩展及版本php-m|grepswoole php--riswoole

当你看到Swoole => enabledVersion => 5.1.x时,环境就 OK 了。退出容器用exit即可,但我们下面的操作都在容器内进行。


二、知识核心:彻底搞懂 Reactor 事件循环 (约 1.5 小时)

类比理解(餐厅服务员):
一个传统 PHP-FPM 餐厅里,每来一个客人(请求),老板就临时雇一个服务员(进程/线程),服务完就解雇。1000 个客人同时来,就得雇 1000 个服务员,系统负担极重。

而 Swoole 的 Reactor 模型,就像一个超级服务员。他手里有一个记录板(事件循环),时刻关注所有客人的桌子(连接)。客人招手(有数据到达)时,他才过去处理一下,然后立刻回来继续看记录板。一个人就能同时服务成百上千个客人。

技术原理:

  1. 同步阻塞 (Blocking I/O):传统的socket_read,如果没有数据,整个进程就卡住傻等,什么也做不了。
  2. I/O 多路复用 (epoll):操作系统提供一个机制,能同时监听成百上千个 socket。Swoole 的事件循环底层就是基于epoll,它不断询问操作系统:“哪个 socket 有新数据?”,然后只去处理那些有事件的 socket。
  3. Reactor 模式
    • 事件循环 (EventLoop):一个无限循环,不停地等待事件发生。
    • 事件分发:事件发生(新连接、数据到达、连接关闭),循环根据注册好的回调函数去执行相应逻辑。
    • 非阻塞:回调函数必须快速执行,绝对不能有sleep或阻塞的数据库查询,否则这个超级服务员就会被卡在那个客人的桌前,其他所有客人都被晾着了。

这就是今天我们要在代码里亲手验证的道理。


三、实战:纯 TCP 回声服务器与协程初体验 (约 2 小时)

所有代码都在宿主机swoole-course目录下编写,然后在容器内执行。

1. 基础版:单连接阻塞回声服务器

创建文件tcp_echo_blocking.php

<?php// 创建一个 TCP 服务器,监听 0.0.0.0:9501$server=newSwoole\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-composeexecswoolebashphp 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=newSwoole\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.phpCtrl+C),运行新的:

php tcp_echo_coroutine.php

同样打开两个终端窗口,都用telnet localhost 9501连接。

  • 在窗口 1 输入hello1并回车。
  • 立刻切换到窗口 2,输入hello2并回车。
  • 现象:你会看到两个窗口几乎同时收到了回声,而且都是在 5 秒后。

结论:通过协程,我们让这个单进程的超级服务员同时处理了两个耗时任务,完全没有任何阻塞。这就是 Swoole 高性能的核心秘密。


四、成果测试与方法检验 (约 1 小时)

1. 基础端口连通性测试

服务器运行时,在宿主机或容器内执行:

# 检查 9501 端口是否在监听netstat-tulnp|grep9501# 应该看到一条 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.phpReceive回调中,即使不加Swoole\Coroutine\System::sleep,直接写sleep(5),它还会阻塞吗?会的,因为原生函数如果不做运行时协程化,就是阻塞的。我们将在后面学习Runtime::enableCoroutine()来一键让原生函数协程化。

今日作业与学习产出

  1. 绘制流程图:手绘或使用工具画出 Reactor 模型的事件循环流程图,包括ConnectReceiveClose事件。
  2. 提交代码:将今天编写的两个tcp_echo_*.php文件提交到 Git 仓库。
  3. 学习笔记:用自己的话解释同步阻塞和协程非阻塞的差异,并记录下你观察到的实验现象。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询