通过自定义配置文件直接增加 listen 端口并绑定原生回调

ThinkPHP 6.x / 8.x 结合 think-swoole 扩展的环境下,通过自定义配置文件直接增加 listen 端口并绑定原生回调是完全可行的。

think-swoole 的底层是基于 Swoole 的,它在框架初始化时会读取 config/swoole.php。我们可以利用 Swoole 的 server 或者是 think-swoole 提供的事件系统,在主服务启动前(custom_initServerManager)动态添加监听端口,并绑定标准的 JSON-RPC 原生回调。

以下是为您量身定制的无缝接入适配方案,无需修改框架源码,直接配置即可投入生产:


🛠️ 生产级落地步骤

步骤一:创建 JSON-RPC 协议处理器

我们在 app/rpc 目录下(若不存在可自行创建)新建一个处理器类,专门负责解析标准 JSON-RPC 2.0 协议、调用 ThinkPHP 内部控制器/服务,并返回标准格式。

创建文件:app/rpc/JsonRpcHandler.php

php

<?php
namespace app\rpc;

use Swoole\Server;
use think\Container;
use think\App;

class JsonRpcHandler
{
    protected $app;

    public function __construct(App $app)
    {
        $this->app = $app;
    }

    /**
     * 接管监听端口的 Receive 回调
     */
    public function onReceive(Server $server, int $fd, int $reactorId, string $data)
    {
        // 1. 去除包尾换行符(配合 EOF 拆包)
        $payload = rtrim($data, "\r\n");
        $request = json_decode($payload, true);

        // 2. 验证是否符合标准 JSON-RPC 2.0 规范
        if (!$request || !isset($request['jsonrpc']) || $request['jsonrpc'] !== '2.0' || !isset($request['method'])) {
            $this->sendError($server, $fd, -32700, "Parse error or invalid request");
            return;
        }

        $method = $request['method'];
        $params = $request['params'] ?? [];
        $id     = $request['id'] ?? null;

        try {
            // 3. 将 JSON-RPC 的 method(例如 "user.info" 或 "UserService@getInfo")映射到框架内部
            $result = $this->dispatchToContainer($method, $params);

            // 4. 返回标准成功的格式
            $response = [
                "jsonrpc" => "2.0",
                "result"  => $result,
                "id"      => $id
            ];
            $server->send($fd, json_encode($response) . "\r\n");

        } catch (\Throwable $e) {
            // 5. 捕获内部异常,转换为标准的 JSON-RPC 错误码
            $code = $e->getCode() ?: -32603; // 默认 Internal Error
            $this->sendError($server, $fd, $code, $e->getMessage(), $id);
        }
    }

    /**
     * 容器分发:将 RPC 请求路由到 ThinkPHP 的 Service、Controller 或应用容器中
     */
    protected function dispatchToContainer(string $method, array $params)
    {
        // 示例规则:将 "user.getInfo" 解析为调用 app\rpc\service\UserService 类的 getInfo 方法
        // 您可以根据自己的命名空间规则自行调整解析逻辑
        if (strpos($method, '.') !== false) {
            list($serviceName, $action) = explode('.', $method);
        } else {
            throw new \Exception("Method not found (Invalid format)", -32601);
        }

        $className = "app\\rpc\\service\\" . ucfirst($serviceName) . "Service";

        if (!class_exists($className)) {
            throw new \Exception("Service class not found", -32601);
        }

        // 从 ThinkPHP 容器中获取实例(保证依赖注入正常工作)
        $instance = Container::getInstance()->make($className);

        if (!method_exists($instance, $action)) {
            throw new \Exception("Method not found in service", -32601);
        }

        // 执行业务方法(利用 invokeMethod 自动实现参数依赖注入)
        return Container::getInstance()->invokeMethod([$instance, $action], $params);
    }

    /**
     * 错误回包封装
     */
    protected function sendError(Server $server, int $fd, int $code, string $message, $id = null)
    {
        $response = [
            "jsonrpc" => "2.0",
            "error"   => [
                "code"    => $code,
                "message" => $message
            ],
            "id"      => $id
        ];
        $server->send($fd, json_encode($response) . "\r\n");
    }
}

请谨慎使用此类代码。


步骤二:通过 think-swoole 事件动态绑定多端口

think-swoole 提供了 swoole.init 事件,该事件在 Swoole Server 创建后、启动(start)前触发,是注入多端口(listen)和原生回调的绝佳时机。

创建或修改文件:app/listener/SwooleInitListener.php

php

<?php
namespace app\listener;

use Swoole\Server;
use think\App;
use app\rpc\JsonRpcHandler;

class SwooleInitListener
{
    protected $app;

    public function __construct(App $app)
    {
        $this->app = $app;
    }

    /**
     * 监听 swoole.init 事件
     */
    public function handle(Server $server)
    {
        // 1. 通过主服务动态 listen 新的 TCP 端口(例如 9502)
        // 可以根据需要将配置提到 config/swoole.php 中
        $jsonRpcPort = $server->listen("0.0.0.0", 9502, SWOOLE_SOCK_TCP);

        if ($jsonRpcPort === false) {
            // 端口监听失败处理
            return;
        }

        // 2. 为该端口设置独立的协议分包参数(开启 EOF 检测防粘包)
        $jsonRpcPort->set([
            'open_eof_check'     => true,
            'package_eof'        => "\r\n", // 约定每条 JSON 数据以换行符结尾
            'package_max_length' => 1024 * 1024 * 2, // 最大 2MB 包
        ]);

        // 3. 从 ThinkPHP 容器实例化刚才创建的处理器
        /** @var JsonRpcHandler $handler */
        $handler = $this->app->make(JsonRpcHandler::class);

        // 4. 绑定该端口专用的原生回调
        $jsonRpcPort->on('Receive', [$handler, 'onReceive']);
    }
}

请谨慎使用此类代码。


步骤三:注册事件监听

打开 ThinkPHP 全局或应用的事件配置文件 app/event.php,将编写好的监听器注册到 think-swoole 的初始化事件中:

php

return [
    'bind'      => [],
    'listen'    => [
        // 注册到 think-swoole 的服务器初始化事件
        'swoole.init' => [
            \app\listener\SwooleInitListener::class,
        ],
    ],
    'subscribe' => [],
];

请谨慎使用此类代码。


步骤四:编写您的内部业务 RPC 服务

现在可以按照在 JsonRpcHandler 里面定义的路由规则,编写实际承载业务的服务类了。

创建文件:app/rpc/service/UserService.php

php

<?php
namespace app\rpc\service;

class UserService
{
    /**
     * 测试 RPC 方法
     * 请求中的 params 数组若包含键名(如 ['uid' => 1001]),
     * ThinkPHP 的容器会自动将其赋给同名形参 $uid
     */
    public function getInfo(int $uid)
    {
        if ($uid <= 0) {
            // 自定义业务异常,会被 handler 捕获并转为 JSON-RPC 格式
            throw new \Exception("Invalid user ID", -32602); // -32602 Invalid params
        }

        return [
            'uid'      => $uid,
            'nickname' => 'ThinkSwoole开发者',
            'role'     => 'Admin',
            'time'     => date('Y-m-d H:i:s')
        ];
    }
}

请谨慎使用此类代码。


🚀 启动与联调测试

  1. 使用命令行正常启动你的 think-swoole 服务:

    bash

    php think swoole start
    

    请谨慎使用此类代码。

  2. 原有的 HTTP 端口(如 9501)和官方原生功能依然不受影响。

  3. 打开任意 TCP 调试工具(如 ncTelnet)或使用客户端向 9502 端口发送标准的 JSON-RPC 2.0 数据包。记得在数据包末尾加上回车换行(\r\n请求报文(Payload):

    json

    {"jsonrpc": "2.0", "method": "user.getInfo", "params": {"uid": 888}, "id": "req-001"}\r\n
    

    请谨慎使用此类代码。

    标准响应报文:

    json

    {"jsonrpc":"2.0","result":{"uid":888,"nickname":"ThinkSwoole\u5f00\u53d1\u8005","role":"Admin","time":"2026-06-12 18:42:00"},"id":"req-001"}\r\n
    

    请谨慎使用此类代码。

💡 针对 Think-Swoole 的架构优化建议

  • 协程上下文清理:由于是在多端口的原生 Receive 回调里直接调用了容器逻辑,如果业务中使用了 think\facade\Context 或协程相关的单例,为了防止多连接间的数据串户,建议在 onReceivetry...catch...finally 块中显式执行一下上下文的 clear() 动作。

  • 自定义路由字典:当前 Demo 采用的是传统的 Class.Method 字符串切割。如果项目庞大,可以在 JsonRpcHandler 中维护一个完全自定义的 Map 字典,将前端暴露的 method 字符串与内部真实的控制器进行硬编码或注解映射,从而保障内部代码结构的安全性。