通过网关代理类动态分发客户端的调用方法

针对方案一(通过网关代理类动态分发),客户端如果同样是 TP 项目,由于服务端在 config/swoole.php 中只向官方扩展注册了一个固定的服务标识 gateway,客户端的调用方式也需要做出相应的配合。

以下是客户端对应的具体调用写法,分为快捷调用优雅的代理类调用(推荐)两种:

写法 A:通过 RPC 门面直接快捷调用

这是最直接的写法,客户端直接连接服务端的 gateway 标识,并将实际的目标服务名方法名作为参数打包传过去:

php

namespace app\controller;

use think\swoole\facade\RPC;

class OrderController
{
    public function index()
    {
        // 1. 连接服务端配置的 'gateway' 服务
        // 2. 调用服务端的 __call 魔术方法,依次传入:[目标服务标识, 目标方法名, 实际参数]
        $userInfo = RPC::user('gateway')->execute('user', 'getInfo', ['uid' => 99]);

        return json($userInfo);
    }
}

请谨慎使用此类代码。

注: 这里的 execute 是在服务端 RpcGateway 类中可以自由定义的方法名(如果服务端用的是 __call,客户端这里写任何方法名如 run()do() 都可以,因为服务端都会通过魔术方法接收)。


写法 B:高级代理写法(完美复刻本地调用感,推荐)

写法 A 虽然能用,但代码看起来不够优雅,且失去了 IDE 的代码提示。在实际开发中,更推荐在客户端编写一个本地动态代理类,让调用端维持 UserService->getInfo() 这种完全面向对象的原生体验。

1. 客户端创建动态代理基类

在客户端创建一个公共的代理基类 app\rpc\client\BaseProxy.php,用来封装向服务端 gateway 发送请求的底层逻辑:

php

namespace app\rpc\client;

use think\swoole\facade\RPC;

abstract class BaseProxy
{
    // 子类需要定义自己对应服务端的哪个“动态服务标识”
    protected $serviceAlias = '';

    /**
     * 利用魔术方法,自动拦截客户端调用的所有方法
     */
    public function __call($methodName, $arguments)
    {
        // 自动打包:[ 动态服务标识, 调用的方法名, 传入的参数 ]
        return RPC::user('gateway')->execute(
            $this->serviceAlias, 
            $methodName, 
            $arguments
        );
    }
}

请谨慎使用此类代码。

2. 客户端为每个远程服务定义一个空类

有了基类后,客户端如果要调用服务端的 user 服务,只需要继承基类并指定标识即可。
创建 app\rpc\client\UserService.php

php

namespace app\rpc\client;

/**
 * 用户远程服务代理
 * @method array getInfo(int $uid)  <-- 在这里写注解,IDE 就会有完美的代码提示!
 */
class UserService extends BaseProxy
{
    // 对应服务端动态映射表(如 Redis/数据库)里的键名
    protected $serviceAlias = 'user'; 
}

请谨慎使用此类代码。

3. 客户端控制器中的最终调用

通过上述代理封装后,你在控制器或者 Service 层调用时,完全感知不到“网关分发”的存在,代码极度干净:

php

namespace app\controller;

use app\rpc\client\UserService;

class Index
{
    protected $userService;

    // 通过 TP6 容器自动注入代理类
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function getUser()
    {
        // 像调用本地方法一样,IDE 还能正常提示方法和参数
        $result = $this->userService->getInfo(99);

        return json($result);
    }
}

请谨慎使用此类代码。


🛠️ 配合方案一的服务端微调提醒

为了配合客户端通过 execute 或者魔术方法调用,服务端 app\rpc\RpcGateway.php 的接收方法需要对应写成这样:

php

namespace app\rpc;

use think\Container;

class RpcGateway
{
    /**
     * 统一接收客户端的 execute 请求
     */
    public function execute(string $serviceAlias, string $methodName, array $realParams = [])
    {
        // 1. 从 Redis 或配置中动态获取类名
        $serviceClass = \think\facade\Cache::store('redis')->get("rpc:services:{$serviceAlias}");
        
        if (!$serviceClass || !class_exists($serviceClass)) {
            throw new \Exception("RPC服务 [{$serviceAlias}] 未注册");
        }

        // 2. 实例化并调用
        $serviceInstance = Container::getInstance()->make($serviceClass);
        
        return call_user_func_array([$serviceInstance, $methodName], $realParams);
    }
}

请谨慎使用此类代码。

这种设计下,如果未来你服务端新增了一个 goods 货品服务,你只需要在服务端的 Redis 里加一条映射,然后客户端直接新建一个继承 BaseProxyGoodsService 类即可直接调用,两端都不需要重启 Swoole 进程