think-swoole 官方自带的 RPC 功能

使用 think-swoole 官方自带的 RPC 功能,其底层的设计思路非常精妙:它是通过接口(Interface)进行契约绑定的。 [1]

官方自带 RPC 的核心优势在于,它不仅为你准备好了服务端的解析路由,还会自动在客户端项目里生成和映射代理类。你调用远程方法时,代码提示和本地调用一模一样。 [1]

以下是使用官方自带 RPC 的标准落地步骤:


1. 服务端配置与接口定义

假设我们有两个独立的项目:服务端(Provider)客户端(Consumer)

步骤一:在服务端定义接口契约

RPC 的核心是契约。在服务端的 app\interface 目录下(如目录不存在请新建)创建一个接口文件 UserInterface.php

php

<?php
namespace app\interface;

interface UserInterface
{
    public function getInfo(int $uid): array;
}

请谨慎使用此类代码。

步骤二:在服务端实现该接口

app\rpc\service 目录下创建实现类 UserService.php: [1]

php

<?php
namespace app\rpc\service;

use app\interface\UserInterface;

class UserService implements UserInterface
{
    public function getInfo(int $uid): array
    {
        return [
            'uid'  => $uid,
            'name' => 'Swoole内置RPC用户',
            'time' => date('Y-m-d H:i:s')
        ];
    }
}

请谨慎使用此类代码。

步骤三:修改服务端的 config/swoole.php

找到默认配置中的 'rpc' 区域,开启服务端并注册刚才写好的接口: [1]

php

'rpc' => [
    'server' => [
        'enable'   => true, // 🌟 开启官方内置 RPC 服务端
        'host'     => '0.0.0.0',
        'port'     => 9000, // 官方自带 RPC 默认端口通常为 9000
        'services' => [
            // 🌟 契约绑定:接口类名 => 实现的业务类名
            \app\interface\UserInterface::class => \app\rpc\service\UserService::class,
        ],
    ],
    'client' => [
        // 服务端不需要配置客户端
    ],
],

请谨慎使用此类代码。

启动服务端:在服务端项目根目录下执行 php think swoole


2. 客户端配置与调用

现在转到你的客户端项目中。

步骤一:复制接口文件

客户端项目必须拥有和服务端一模一样命名空间的接口文件。
直接在客户端项目创建 app\interface\UserInterface.php,内容保持不变:

php

<?php
namespace app\interface;

interface UserInterface
{
    public function getInfo(int $uid): array;
}

请谨慎使用此类代码。

步骤二:修改客户端的 config/swoole.php

在客户端项目中配置连接信息,指定哪个接口需要去连接哪台远程服务器:

php

'rpc' => [
    'server' => [
        'enable' => false, // 客户端不需要开启服务端
    ],
    'client' => [
        // 🌟 核心:配置远程微服务节点
        'user_center' => [
            'host'     => '127.0.0.1', // 远程服务端的 IP
            'port'     => 9000,        // 远程服务端的 RPC 端口
            'services' => [
                // 🌟 告诉框架:只要我调用这个接口,就去 user_center 这台机器拿数据
                \app\interface\UserInterface::class,
            ],
        ],
    ],
],

请谨慎使用此类代码。

步骤三:一键生成客户端远程代理类 🚀

配置好 client 之后,在客户端项目的根目录下,执行 ThinkPHP 官方提供的专属命令: [1]

bash

php think rpc:interface

请谨慎使用此类代码。

执行后发生了什么?
框架会自动在客户端项目的 rpc.php 映射文件中或者系统容器里,为 UserInterface 接口生成一个动态的 Proxy(代理)类。这个代理类会自动拦截你的方法调用,并通过 Swoole TCP 传输到服务端。

步骤四:业务层无缝调用

在控制中,你可以通过依赖注入直接调用该接口,就像调用本地代码一样,IDE 会有完美的语法提示: [1]

php

<?php
namespace app\controller;

use app\BaseController;
use app\interface\UserInterface; // 注入接口

class Member extends BaseController
{
    /**
     * 依赖注入框架自动生成的 RPC 代理类
     */
    public function index(UserInterface $userService)
    {
        try {
            // 像本地方法一样直接调用,底层自动走 Swoole TCP 发送给服务端
            $userInfo = $userService->getInfo(9527);

            return json([
                'code' => 200,
                'data' => $userInfo
            ]);
            
        } catch (\Throwable $e) {
            return json([
                'code' => 500,
                'msg'  => $e->getMessage()
            ]);
        }
    }
}

请谨慎使用此类代码。


💡 官方自带 RPC 与自主封装的优缺点对比

特性 [1, 2]

官方自带 RPC (think-swoole)

自主封装 JSON-RPC (之前写的)

调用体验

🌟 极佳。面向接口编程,有完美的代码提示。

一般。需要通过 withService('类名')->方法() 字符串形式调用。

跨语言能力

极差。使用的是 PHP 特有的序列化/反序列化。只能 TP6 连 TP6/TP8

🌟 极强。标准的 JSON 格式,Java、Go、Python 客户端均可轻松接入。

环境限制

客户端通常也需要运行在 Swoole 协程环境下才能发挥最大长连接威力。

我们可以用原生流编写,支持传统的 Nginx + PHP-FPM 直接作为客户端。

如果你的团队前后端全部使用 ThinkPHP(且装了 Swoole),首选官方自带的 RPC,开发效率极高;如果是为了和其他语言(如 Go/Java)交互,或者客户端是 FPM 模式,则建议采用我们之前封装的通用 JSON-RPC。 [1]

think-swoole 官方自带的 RPC 完全不支持标准的 JSON-RPC 协议

如果你有跨语言(如 Go/Java/Python)调用的需求,或者想用标准的 JSON 格式传输,千万不要用官方自带的 RPC

官方自带 RPC 的底层设计原因和限制主要如下:

1. 采用的是 PHP 原生序列化(Swoole Serializer)

官方自带的 RPC 为了实现极其丝滑的面向接口调用(即客户端不用写任何解析,代码提示和调用本地类完全一样),其底层传输的数据是通过 PHP 的 serialize()unserialize()(或者 Swoole 特有的内置二进制序列化格式)来打包的。

  • 致命缺点:这种格式只有 PHP 能够看懂。如果其他语言(比如 Go 或 Java)向系统发送一个标准的 JSON-RPC 字符串(如 {"jsonrpc":"2.0", "method":"..."}),官方的服务端根本无法反序列化,会直接报错或断开连接。

2. 属于闭环的私有协议

官方内置的 RPC 核心目的只是为了让 ThinkPHP 项目连另一个 ThinkPHP 项目。它从数据包头、方法路由、参数映射到异常抛出,全套都是针对 TP 框架定制的私有 TCP 协议。


💡 结论与落地建议

  • 如果你是纯 ThinkPHP 微服务生态:全员都用 TP6 + Swoole,并且不需要和其他语言通信,那么可以直接使用官方自带的 RPC,开发效率确实最高。

  • 如果你需要异构系统交互或标准 JSON 协议:需要支持跨语言调用、支持第三方客户端或者不想受到 Swoole 客户端环境绑定的限制,必须使用我们之前自主封装的方案(即通过 config/swoole.phplisten 选项绑定 callbacks 原生接收,交由自定义的 RpcDispatcher 去解析标准的 JSON 字符串)。

既然官方自带的 RPC 不支持标准的 JSON-RPC 协议,那么通过 listen 配置多端口并绑定原生回调就是最完美的跨语言商用级解决方案。

为了让你能够直接复制并投入生产环境,我为你整合了一套完整的、带安全白名单过滤、带错误容错listen监听方案。

1. 最终的服务端 config/swoole.php 配置

打开你现有的 config/swoole.php,找到 'server' 配置,把 listen 区域精准地追加进去。这样既不影响你原有的 Web 网页服务,又单独开辟了 9502 端口用来接收标准 JSON-RPC。

php

<?php
// config/swoole.php

return [
    'server'     => [
        'host'      => env('SWOOLE_HOST', '127.0.0.1'),
        'port'      => env('SWOOLE_PORT', 80),
        'mode'      => SWOOLE_PROCESS,
        'sock_type' => SWOOLE_SOCK_TCP,
        'options'   => [
            'pid_file'              => runtime_path() . 'swoole.pid',
            'log_file'              => runtime_path() . 'swoole.log',
            'daemonize'             => false,
            // ... 保留你原本生成的其他 options 配置 ...
        ],
        
        // 🌟 核心:通过 listen 监听 9502 端口
        'listen'    => [
            [
                'host'      => '0.0.0.0', // 允许任何外网/内网IP连接
                'port'      => 9502,      // 标准 JSON-RPC 端口
                'sock_type' => SWOOLE_SOCK_TCP,
                'options'   => [
                    'open_eof_check' => true,    // 开启 EOF 检测防止粘包
                    'package_eof'    => "\r\n",  // 约定以换行符为边界
                ],
                // 🚀 核心:不走 TP 复杂的事件系统,底层原生回调直达,性能最高
                'callbacks' => [
                    \Swoole\Constant::EVENT_RECEIVE => function ($server, $fd, $reactorId, $rawData) {
                        try {
                            // 1. 从 TP6 容器中获取解析调度器
                            /** @var \app\common\rpc\RpcDispatcher $dispatcher */
                            $dispatcher = app(\app\common\rpc\RpcDispatcher::class);
                            
                            // 2. 传入原始请求数据进行解析和业务调度
                            $responsePayload = $dispatcher->dispatch($rawData);
                            
                            // 3. 将符合 JSON-RPC 2.0 规范的响应发回客户端
                            $server->send($fd, $responsePayload);
                            
                        } catch (\Throwable $e) {
                            // 极端底层崩溃防御
                            $errorResponse = json_encode([
                                'jsonrpc' => '2.0',
                                'error'   => ['code' => -32603, 'message' => 'Internal server error'],
                                'id'      => null
                            ]) . "\r\n";
                            $server->send($fd, $errorResponse);
                        }
                    }
                ]
            ],
        ],
    ],
    
    // 下面官方自带的 rpc 开关保持 false 即可,互不干扰
    'rpc' => [
        'enable' => false,
    ],
    // ... 其他默认配置保持原样 ...
];

请谨慎使用此类代码。

2. 生产级协议调度器 RpcDispatcher.php

由于原生回调非常开放,我们需要在调度器中做两件生产环境必须要做的事

  1. 别名映射:客户端不想传长长的 app\rpc\service\UserService,让他们传简写的 user 即可。

  2. 安全白名单:严禁客户端通过 RPC 动态调用系统内部的敏感类(如数据库类、文件操作类)。

app\common\rpc 目录下创建 RpcDispatcher.php

php

<?php
namespace app\common\rpc;

use Throwable;

class RpcDispatcher
{
    /**
     * 🌟 生产级:服务类简称映射别名(既能隐藏核心代码目录,又方便客户端调用)
     */
    private array $serviceMaps = [
        'user'  => \app\rpc\service\UserService::class,
        'order' => \app\rpc\service\OrderService::class,
    ];

    public function dispatch(string $rawData): string
    {
        $responseId = null;
        try {
            // 1. 解析数据
            $request = json_decode(trim($rawData), true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                return $this->error(-32700, 'Parse error', $responseId);
            }

            // 2. 基础协议校验
            if (!isset($request['jsonrpc']) || $request['jsonrpc'] !== '2.0' || !isset($request['method'])) {
                return $this->error(-32600, 'Invalid Request', $responseId);
            }

            $responseId = $request['id'] ?? null;
            $methodStr  = $request['method']; // 客户端传入格式如: "user@getUserInfo"
            $params     = $request['params'] ?? [];

            if (strpos($methodStr, '@') === false) {
                return $this->error(-32601, 'Method format error. Use "service@method"', $responseId);
            }

            list($serviceAlias, $methodName) = explode('@', $methodStr);

            // 3. 安全检查与别名转换
            if (!isset($this->serviceMaps[$serviceAlias])) {
                return $this->error(-32601, "Service '{$serviceAlias}' not found or Access Denied.", $responseId);
            }

            $className = $this->serviceMaps[$serviceAlias];

            if (!class_exists($className)) {
                return $this->error(-32601, "Target class internal error.", $responseId);
            }

            // 4. 利用 TP6 容器实例化(支持依赖注入)
            $instance = app($className);

            if (!method_exists($instance, $methodName)) {
                return $this->error(-32601, "Method '{$methodName}' not found.", $responseId);
            }

            // 5. 执行业务逻辑
            $result = call_user_func_array([$instance, $methodName], $params);

            return $this->success($result, $responseId);

        } catch (Throwable $e) {
            // 捕获业务抛出的任何异常并转化为标准的 JSON-RPC 错误返回
            return $this->error($e->getCode() ?: -32603, $e->getMessage(), $responseId);
        }
    }

    private function success($result, $id): string
    {
        return json_encode([
            'jsonrpc' => '2.0',
            'result'  => $result,
            'id'      => $id
        ]) . "\r\n";
    }

    private function error(int $code, string $message, $id): string
    {
        return json_encode([
            'jsonrpc' => '2.0',
            'error'   => [
                'code'    => $code,
                'message' => $message
            ],
            'id'      => $id
        ]) . "\r\n";
    }
}

请谨慎使用此类代码。

3. 客户端对应的精简调用方式

既然我们在服务端做了 user 别名映射,之前的客户端代码在调用时,withService 传入的值就可以大幅度精简,变得非常优雅:

php

// 控制器或其他业务代码中调用
$res = $rpcClient
    ->withService('user') // 🌟 直接传别名,不再暴露服务端的全量命名空间
    ->getUserInfo(1001);

dump($res);

请谨慎使用此类代码。

🏆 这样设计的终极优势

  1. 绝对的跨语言:无论是 PHP 原生写的脚本、Go 语言的微服务、还是 Java 的后端,只要向服务端的 9502端口发送字符串 {"jsonrpc":"2.0","method":"user@getUserInfo","params":[1001],"id":1}\r\n,它们都能完美互通。

  2. 极高的并发吞吐:由于使用了 Swoole 的多端口独立监听(listen),RPC 请求直接在底层的 Socket 回调中被 RpcDispatcher 拦截消化,绕过了 TP 框架昂贵的全局事件调度链路。