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


在这里插入图片描述

今日目标

  1. 在 Swoole 中创建高性能 HTTP 服务器,理解其处理流程。
  2. 掌握 Request/Response 对象,能够处理 GET/POST 参数并返回 JSON 数据。
  3. 实现静态资源(如图片)的托管,理解浏览器加载原理。
  4. 深刻理解 go() 协程的创建与调度,在请求中使用协程异步写日志,并肉眼观察并发奇迹。
  5. 使用 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 协议。

  • 流程图

    1. 客户端发起 HTTP 请求。
    2. 服务器 onRequest 事件触发。
    3. 回调函数接收 $request (请求信息) 和 $response (响应对象)。
    4. 开发者处理逻辑,调用 $response->end() 发送数据并结束此次请求。
    5. 服务器继续监听下一个请求。
  • 常驻内存的优势:框架初始化、数据库连接池等只需做一次,后续请求直接复用,因此速度极快。

  • 静态资源托管: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
    

预期现象与原理分析

  1. 每次 curl立刻返回 JSON 响应,不会等待 2 秒。
  2. 约 2 秒后,access.log 文件中会出现两条日志记录,且时间戳是发起请求的时间。
  3. 观察服务器控制台输出:
    [协程] 开始记录日志,休眠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 服务返回 JSONcurl 访问,检查 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 压测同步接口相同参数响应时间极大,或出现失败

今日作业与学习产出

  1. 提交代码:将 http_server.phphttp_server_async.phpstatic 目录提交至 Git。
  2. 压测截图:将异步接口和同步接口的 ab 压测结果截图,并附上你的分析结论。
  3. 协程笔记:画出 go() 协程的并发执行流程图,标注主请求和协程之间的关系,以及何时让出 CPU。
  4. 挑战:实现 WaitGroup 版本,使得请求在等待所有日志协程完成后才返回,对比与立即返回的场景差异。

通过今天的实战,你已经掌握了 Swoole HTTP 服务的核心,并亲眼见证了协程如何用单进程实现高并发。明天我们将利用 Hyperf 框架再次实现这一切,感受框架的强大封装。

更多推荐