在 Swoole 的 onRequest 事件中挂载 JsonRPC 服务

在 ThinkPHP6 的 think-swoole 框架下,如果我们直接使用 Swoole 的底层事件 onRequest 来挂载并解析标准的 JsonRPC 2.0 协议,通常是因为要兼容非 TP 项目的外部调用(比如通过纯 HTTP POST 请求发送 JSON 数据的第三方客户端)。

下面是完整的服务侧挂载、逻辑解析与客户端请求的开发流程:

1. 订阅 Swoole 的 onRequest 事件

TP6 通过内置的事件系统管理 Swoole 周期。我们需要在 app/event.php 中订阅 swoole.request 事件:

php

return [
    'bind'      => [],
    'listen'    => [
        // 绑定 Swoole 的全局 HTTP 请求事件
        'swoole.request' => [
            \app\listener\JsonRpcServerListener::class,
        ],
    ],
    'subscribe' => [],
];

请谨慎使用此类代码。

2. 编写 JsonRPC 解析器监听类

创建事件监听类 app\listener\JsonRpcServerListener.php。在该类中获取客户端发来的原生 POST 载荷,按照 JSON-RPC 2.0 规范标准进行解析、路由分发和结果组装:

php

namespace app\listener;

use Swoole\Http\Request;
use Swoole\Http\Response;

class JsonRpcServerListener
{
    /**
     * 处理 Swoole 请求事件
     */
    public function handle($event)
    {
        // $event 中包含 Swoole 的原始 Request 和 Response 对象
        /** @var Request $request */
        $request = $event->request;
        /** @var Response $response */
        $response = $event->response;

        // 1. 设置响应头为标准的 JSON
        $response->header('Content-Type', 'application/json; charset=utf-8');

        // 2. 获取请求的原始 JSON 文本并解析
        $rawContent = $request->rawContent();
        $payload = json_decode($rawContent, true);

        // 3. 基础规范校验
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($payload)) {
            $this->sendError($response, -32700, 'Parse error', null);
            return;
        }

        // 提取规范字段 (支持单条请求,如需支持批量请求可使用循环)
        $jsonrpc = $payload['jsonrpc'] ?? '';
        $method  = $payload['method'] ?? '';
        $params  = $payload['params'] ?? [];
        $id      = $payload['id'] ?? null; // 为 null 时视为通知行为,原则上无回执

        if ($jsonrpc !== '2.0' || empty($method)) {
            $this->sendError($response, -32600, 'Invalid Request', $id);
            return;
        }

        // 4. RPC 路由分发 (格式规范通常为:服务类/方法名,如 "user/getInfo")
        $parts = explode('/', $method);
        if (count($parts) !== 2) {
            $this->sendError($response, -32601, 'Method not found', $id);
            return;
        }

        [$serviceName, $actionName] = $parts;
        // 映射到内部的具体业务类,假设放在 app\rpc 目录下
        $className = "app\\rpc\\" . ucfirst($serviceName);

        if (!class_exists($className) || !method_exists($className, $actionName)) {
            $this->sendError($response, -32601, 'Method not found', $id);
            return;
        }

        // 5. 执行业务方法并捕获可能出现的异常
        try {
            $serviceInstance = container($className); // 使用 TP 的容器实例化以支持依赖注入
            
            // 执行业务类对应的方法 (由于 RPC 参数按数组传递,这里使用 call_user_func_array)
            $result = call_user_func_array([$serviceInstance, $actionName], is_array($params) ? $params : [$params]);

            // 6. 返回成功的规范响应
            $response->end(json_encode([
                'jsonrpc' => '2.0',
                'result'  => $result,
                'id'      => $id
            ]));

        } catch (\Throwable $e) {
            // 运行时业务代码引发的错误
            $this->sendError($response, -32603, 'Internal error: ' . $e->getMessage(), $id);
        }
    }

    /**
     * 统一的错误回执响应
     */
    private function sendError(Response $response, int $code, string $message, $id)
    {
        $response->end(json_encode([
            'jsonrpc' => '2.0',
            'error'   => [
                'code'    => $code,
                'message' => $message
            ],
            'id'      => $id
        ]));
    }
}

请谨慎使用此类代码。

3. 创建具体的 RPC 业务类

app/rpc/User.php 中创建响应的具体业务类。这里不再受限于 TP 内置 RPC 的规则限制,编写普通的 PHP 类即可:

php

namespace app\rpc;

class User
{
    /**
     * 远程获取用户信息方法
     * 客户端 params 传入的是键值对时,变量名对应参数名
     */
    public function getInfo(int $uid)
    {
        if ($uid <= 0) {
            throw new \Exception("UID错误");
        }

        return [
            'uid'  => $uid,
            'name' => 'SwooleRPC用户',
            'time' => date('Y-m-d H:i:s')
        ];
    }
}

请谨慎使用此类代码。

4. 客户端如何请求调用?

现在服务端的 onRequest 已经变为一个标准无差异的 JsonRPC 高性能网关,调用端(无论是另一个 TP 项目、Go、Node.js 还是 Python)只需要发送一个标准的 HTTP POST 请求即可。

客户端你可以直接用 curl 或 PHP 的 file_get_contents 发起调用验证:

php

$url = 'http://127.0.0.1:8000'; // 填写你服务端 think-swoole 运行的 HTTP 端口

$requestData = [
    'jsonrpc' => '2.0',
    'method'  => 'user/getInfo', // 对应服务端绑定的 user 类与 getInfo 方法
    'params'  => ['uid' => 99],  // 参数
    'id'      => time()          // 消息流水号
];

$options = [
    'http' => [
        'header'  => "Content-Type: application/json\r\n",
        'method'  => 'POST',
        'content' => json_encode($requestData),
    ],
];

$context  = stream_context_create($options);
$response = file_get_contents($url, false, $context);

echo $response;
// 输出结果类似: {"jsonrpc":"2.0","result":{"uid":99,"name":"SwooleRPC\u7528\u6237","time":"2026-06-09 17:31:00"},"id":1781016660}

请谨慎使用此类代码。