第一周·周一 内容可以照着这个步骤学完一天,并在当天结束时完成一个具备并发能力的 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,后续所有代码都放在这里。

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 => 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 = 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.phpCtrl+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.phpReceive 回调中,即使不加 Swoole\Coroutine\System::sleep,直接写 sleep(5),它还会阻塞吗?会的,因为原生函数如果不做运行时协程化,就是阻塞的。我们将在后面学习 Runtime::enableCoroutine() 来一键让原生函数协程化。

今日作业与学习产出

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

更多推荐