基于 onRequest 事件挂载的标准 JsonRPC 2.0 协议

针对前文提到的 方案二(基于 onRequest 事件挂载的标准 JsonRPC 2.0 协议),客户端如果是 TP(ThinkPHP)项目,由于它走的是标准的 HTTP(S) 协议,有以下两种非常实用的调用实现方式。


写法 A:使用原生 GuzzleHTTP 客户端(高并发、支持连接池)

在 TP 项目中,推荐引入 guzzlehttp/guzzle 组件(TP6 默认通常已依赖,或通过 composer require guzzlehttp/guzzle 安装)。它完美支持 Swoole 环境下的协程并发展开。

1. 封装一个基础 JsonRpcClient 客户端类

在客户端项目创建 app\rpc\client\JsonRpcClient.php

php

namespace app\rpc\client;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

class JsonRpcClient
{
    protected $client;
    protected $url = 'http://127.0.0.1:8000'; // 服务端 think-swoole 运行的 HTTP 端口

    public function __construct()
    {
        // 初始化 Guzzle 客户端
        $this->client = new Client([
            'timeout'  => 5.0, // 设置超时时间
            'headers' => [
                'Content-Type' => 'application/json',
            ]
        ]);
    }

    /**
     * 发送标准的 JsonRPC 2.0 请求
     */
    public function call(string $method, array $params = [])
    {
        $id = time() . rand(1000, 9999); // 生成唯一的请求流水号
        
        $payload = [
            'jsonrpc' => '2.0',
            'method'  => $method, // 对应服务端的 "类名/方法名",例如 "user/getInfo"
            'params'  => $params,
            'id'      => $id,
        ];

        try {
            $response = $this->client->post($this->url, [
                'json' => $payload
            ]);

            $body = json_decode($response->getBody()->getContents(), true);

            // 1. 判断是否符合 JsonRPC 2.0 错误规范
            if (isset($body['error'])) {
                throw new \Exception("RPC Error [{$body['error']['code']}]: {$body['error']['message']}");
            }

            // 2. 返回正常结果
            return $body['result'] ?? null;

        } catch (GuzzleException $e) {
            throw new \Exception("RPC 网络请求失败: " . $e->getMessage());
        }
    }
}

请谨慎使用此类代码。

2. 在控制器中进行调用

php

namespace app\controller;

use app\rpc\client\JsonRpcClient;

class UserController
{
    public function info(JsonRpcClient $rpcClient)
    {
        // 传入 "服务类/方法名" 
        $userInfo = $rpcClient->call('user/getInfo', ['uid' => 99]);

        return json($userInfo);
    }
}

请谨慎使用此类代码。


写法 B:面向对象代理写法(优雅、带 IDE 代码提示)

为了达到前述“像调用本地方法一样”的流畅体验,我们同样可以在 JsonRPC 之上套一层代理,自动将方法名转换为 user/getInfo 格式。

1. 编写 JsonRPC 代理基类

创建 app\rpc\client\JsonRpcProxy.php

php

namespace app\rpc\client;

abstract class JsonRpcProxy
{
    protected $serviceName = ''; // 由子类指定,例如 'user'
    protected $rpcClient;

    public function __construct(JsonRpcClient $rpcClient)
    {
        $this->rpcClient = $rpcClient;
    }

    /**
     * 拦截本地调用,组装为标准的 JsonRPC method
     */
    public function __call($method, $arguments)
    {
        // 拼接成服务端所需的 "user/getInfo" 格式
        $rpcMethod = $this->serviceName . '/' . $method;
        
        // JsonRPC 参数通常为关联数组(键值对)
        // 如果客户端传参形如 ->getInfo(['uid' => 99]),直接取第一个参数
        $params = isset($arguments[0]) && is_array($arguments[0]) ? $arguments[0] : $arguments;

        return $this->rpcClient->call($rpcMethod, $params);
    }
}

请谨慎使用此类代码。

2. 派生具体的业务服务类

创建 app\rpc\client\UserService.php

php

namespace app\rpc\client;

/**
 * 用户中心 JsonRPC 远程客户端
 * @method array getInfo(array $params) <-- 编写提示注解
 */
class UserService extends JsonRpcProxy
{
    protected $serviceName = 'user'; // 对应服务端 app\rpc\User 类
}

请谨慎使用此类代码。

3. 最终在控制器中优雅调用

此时,团队其他开发人员在客户端写代码时,完全不需要关心底层的 JsonRPC 协议格式:

php

namespace app\controller;

use app\rpc\client\UserService;

class Index
{
    public function test(UserService $userService)
    {
        // 原生调用感,参数以键值对(数组)方式传递给服务端的 getInfo(int $uid)
        $result = $userService->getInfo(['uid' => 101]);

        return json($result);
    }
}

请谨慎使用此类代码。


💡 JsonRPC 模式的高级拓展提示

  1. 批量请求 (Batch Request):标准的 JsonRPC 2.0 支持一次性发送一个数组(包含多个请求包)。如果客户端需要同时获取用户信息、订单信息,可以扩展 JsonRpcClientcall 方法,使其支持发送二维数组,从而通过一次网络 I/O 拿到所有数据,最大化发挥 Swoole 的并发性能。

  2. 跨语言联通:由于此方案基于标准的 JsonRPC 协议,该客户端不仅能调 TP 服务端,还可以无缝调用 Python (jsonrpcserver)、Go (net/rpc) 或 Node.js 编写的微服务。