针对方案一(通过网关代理类动态分发),客户端如果同样是 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 里加一条映射,然后客户端直接新建一个继承 BaseProxy 的 GoodsService 类即可直接调用,两端都不需要重启 Swoole 进程。