使用 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 与自主封装的优缺点对比
如果你的团队前后端全部使用 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.php的listen选项绑定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
由于原生回调非常开放,我们需要在调度器中做两件生产环境必须要做的事:
别名映射:客户端不想传长长的
app\rpc\service\UserService,让他们传简写的user即可。安全白名单:严禁客户端通过 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);
请谨慎使用此类代码。
🏆 这样设计的终极优势
绝对的跨语言:无论是 PHP 原生写的脚本、Go 语言的微服务、还是 Java 的后端,只要向服务端的
9502端口发送字符串{"jsonrpc":"2.0","method":"user@getUserInfo","params":[1001],"id":1}\r\n,它们都能完美互通。极高的并发吞吐:由于使用了 Swoole 的多端口独立监听(
listen),RPC 请求直接在底层的 Socket 回调中被RpcDispatcher拦截消化,绕过了 TP 框架昂贵的全局事件调度链路。