o-perator / json-rpc
一个 JSON-RPC 2.0 服务器
Requires
- php: ~8.0.0 || ~8.1.0 || ~8.2.0
- ext-json: *
- opis/json-schema: ^2.0
- psr/container: ^1.0 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.16
- phpmetrics/phpmetrics: ^2.8
- phpunit/phpunit: ^9.5
- scrutinizer/ocular: ^1.9
- uma/dic: ^2.0 || ^3.0
This package is auto-updated.
Last update: 2024-09-26 10:33:43 UTC
README
一个针对 PHP 8.0+ 的现代、面向对象的 JSON-RPC 2.0 服务器,具有 JSON Schema 集成和中间件功能。
目录
安装
$ composer require uma/json-rpc
基本用法
设置 JsonRpc\Server
涉及三个独立的步骤:编写所需的进程、将它们注册为服务以及最后配置和运行服务器。
创建过程
进程类似于 MVC 模式中的 HTTP 控制器,并且必须实现 UMA\JsonRpc\Procedure
接口。
以下示例展示了 JSON-RPC 2.0 规范示例中找到的 subtract
进程的可能实现
declare(strict_types=1); namespace Demo; use stdClass; use UMA\JsonRpc; class Subtractor implements JsonRpc\Procedure { /** * {@inheritdoc} */ public function __invoke(JsonRpc\Request $request): JsonRpc\Response { $params = $request->params(); if ($params instanceof stdClass) { $minuend = $params->minuend; $subtrahend = $params->subtrahend; } else { [$minuend, $subtrahend] = $params; } return new JsonRpc\Success($request->id(), $minuend - $subtrahend); } /** * {@inheritdoc} */ public function getSpec(): ?stdClass { return \json_decode(<<<'JSON' { "$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#", "type": ["array", "object"], "minItems": 2, "maxItems": 2, "items": { "type": "integer" }, "required": ["minuend", "subtrahend"], "additionalProperties": false, "properties": { "minuend": { "type": "integer" }, "subtrahend": { "type": "integer" } } } JSON ); } }
逻辑假设 $request->params()
是两个整数的数组,或者是一个具有 minuend
和 subtrahend
属性的 stdClass
,这两个属性都是整数。
这完全安全,因为服务器在调用 __invoke()
之前,会将定义在上面的 JSON 模式与 $request->params()
进行匹配。如果输入不符合规范,则返回一个 -32602 (Invalid params)
错误,并且进程不会运行。
注册服务
下一步是在一个 PSR-11 兼容的容器中定义进程并在配置服务器。在这个例子中,我使用了 uma/dic
declare(strict_types=1); use Demo\Subtractor; use UMA\DIC\Container; use UMA\JsonRpc\Server; $c = new Container(); $c->set(Subtractor::class, function(): Subtractor { return new Subtractor(); }); $c->set(Server::class, function(Container $c): Server { $server = new Server($c); $server->set('subtract', Subtractor::class); return $server; });
此时,我们有一个具有一个单一方法(subtract
)的 JSON-RPC 服务器,该方法映射到 Subtractor::class
服务。此外,进程定义是懒加载的,所以除非服务器中实际调用了 subtract
,否则不会实际实例化 Subtractor
。
这在示例中可能不是很重要。但在有数十个进程定义,每个都有其依赖树的情况下,它就很重要了。
运行服务器
一旦设置完成,相同的服务器可以运行任意次数,并代表库的用户处理 JSON-RPC 规范中定义的大多数错误。
declare(strict_types=1); use UMA\JsonRpc\Server; $server = $c->get(Server::class); // RPC call with positional parameters $response = $server->run('{"jsonrpc":"2.0","method":"subtract","params":[2,3],"id":1}'); // $response is '{"jsonrpc":"2.0","result":-1,"id":1}' // RPC call with named parameters $response = $server->run('{"jsonrpc":"2.0","method":"subtract","params":{"minuend":2,"subtrahend":3},"id":1}'); // $response is '{"jsonrpc":"2.0","result":-1,"id":1}' // Notification (request with no id) $response = $server->run('{"jsonrpc":"2.0","method":"subtract","params":[2,3]}'); // $response is NULL // RPC call with invalid params $response = $server->run('{"jsonrpc":"2.0","method":"subtract","params":{"foo":"bar"},"id":1}'); // $response is '{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params"},"id":1}' // RPC call with invalid JSON $response = $server->run('invalid input {?<derp'); // $response is '{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}' // RPC call on non-existent method $response = $server->run('{"jsonrpc":"2.0","method":"add","params":[2,3],"id":1}'); // $response is '{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":1}'
自定义验证
从版本 4.0.0 开始,您可以通过容器覆盖 Opis 验证器。这允许您使用自定义 Opis 过滤器、格式 和 媒体类型。
为此,只需在 PSR-11 容器中定义一个 Opis\JsonSchema\Validator::class
服务并将其设置为验证器类的自定义实例。
以下示例定义了一个新的“prime”格式,您可以将其用于您的 json 模式。(PrimeNumberFormat 实现为简洁起见而省略)
$validator = new Opis\JsonSchema\Validator(); $formats = $validator->parser()->getFormatResolver(); $formats->register('integer', 'prime', new PrimeNumberFormat()); $psr11Container->set(Opis\JsonSchema\Validator::class, $validator); $jsonServer = new UMA\JsonRpc\Server($psr11Container); // ...
中间件
中间件是一个实现了 UMA\JsonRPC\Middleware
接口的类,其唯一方法接受一个 UMA\JsonRPC\Request
、一个 UMA\JsonRPC\Procedure
并返回一个 UMA\JsonRPC\Response
。在其主体中的某个点,此方法必须调用 $next($request)
,否则请求将无法到达后续中间件或最终进程。中间件是在需要在每个请求之前或之后运行代码块时的首选选项,无论方法如何。
以下是中间件的最小框架
declare(strict_types=1); namespace Demo; use UMA\JsonRpc; class SampleMiddleware implements JsonRpc\Middleware { public function __invoke(JsonRpc\Request $request, JsonRPC\Procedure $next): JsonRpc\Response { // Code run before procedure $response = $next($request); // Code run after procedure finished return $response; } }
为了激活一个中间件,您需要将其注册为依赖注入容器中的服务,就像程序一样。
declare(strict_types=1); use Demo\SampleMiddleware; use UMA\DIC\Container; use UMA\JsonRpc\Server; $c = new Container(); $c->set(SampleMiddleware::class, function(): SampleMiddleware { return new SampleMiddleware(); }); $c->set(Server::class, function(Container $c): Server { $server = new Server($c); // method definitions would go here... $server->attach(SampleMiddleware::class); return $server; });
中间件保证
每当执行流程进入用户定义的中间件的 __invoke
方法时,以下关于请求可以假设
-
原始负载是一个有效的 JSON-RPC 2.0 请求。
-
它的
method
属性指向服务器中实际注册的过程。 -
它的
params
属性符合该过程getSchema()
中定义的 Json Schema。
简而言之,它们是可以在程序内部做出的相同保证。
中间件排序
从某种意义上说,中间件可以被视为服务器的装饰器,每个中间件都将其包裹在一个新的层中。因此,最后附加的层将是第一个运行的(以及最后一个,当退出过程时)。Slim 框架文档通过以下图片描述了他们自己的中间件系统。相同的原理适用于 uma\json-rpc
。
中间件示例
假设您想将传入的通知排队到 Beanstalk 管道中,并在单独的进程中执行这些操作,而不在 HTTP 上下文中。记住,通知是一个没有 ID 属性的 JSON-RPC 请求。根据 JSON-RPC 2.0 规范,当服务器接收到此类请求时,它必须正常运行方法,但不能返回任何输出。
您不需要在每个程序的开始或在尴尬的基类中放置该逻辑,可以使用类似此处的中间件,利用 Request
对象可以被重新编码回原始负载的事实。
declare(strict_types=1); namespace Demo; use Pheanstalk\Pheanstalk; use UMA\JsonRpc; /** * A middleware that enqueues all incoming notifications to a Beanstalkd tube, * thus avoiding their execution overhead. */ class AsyncNotificationsMiddleware implements JsonRpc\Middleware { /** * @var Pheanstalk */ private $producer; public function __construct(Pheanstalk $producer) { $this->producer = $producer; } public function __invoke(JsonRpc\Request $request, JsonRPC\Procedure $next): JsonRpc\Response { if (null === $request->id()) { $this->producer->put(\json_encode($request)); return new JsonRpc\Success(null); } return $next($request); } }
常见问题解答
JSON-RPC 2.0 与 REST 相比有什么优势?
是的,有一些!最重要的是,规范是在 JSON 之上构建的,没有其他(即,其中不涉及 HTTP 动词、标题或身份验证方案)。因此,JSON-RPC 2.0 完全与传输层解耦,它可以在 HTTP、WebSockets、控制台 REPL 或甚至通过 avian carriers 或纸张运行。这实际上是为什么 Server::run()
的接口可以使用纯字符串的原因。
此外,规范简短且明确,支持“发射和遗忘”调用和批量处理。
我如何将中间件附加到特定过程而不是整个服务器?
我做出了不包含此功能的明确决定,因为它大大增加了服务器的复杂性。因此,中间件总是为所有请求运行。
然而,作为用户,您可以手动跳过它们,当方法不是您想要的那个时。
use UMA\JsonRpc; class PickyMiddleware implements JsonRpc\Middleware { /** * @var string[] */ private $targetMethods; public function __construct(array $targetMethods) { $this->targetMethods = $targetMethods; } public function __invoke(JsonRpc\Request $request, JsonRPC\Procedure $next): JsonRpc\Response { if (!in_array($request->method(), $this->targetMethods)) { return $next($request); } // Actual logic goes here } }
如何将 uma/json-rpc
集成到其他框架中?
我正在准备一个包含一些示例的存储库。它将涵盖 HTTP、TCP、WebSockets 和命令行界面上的 JSON-RPC 2.0。
最佳实践
依靠 JSON 模式来验证参数
虽然不需要从您的程序返回 JSON Schema,但强烈建议这样做,因为这样您的程序可以假设它接收到的输入参数是有效的,这将大大简化它们的逻辑。
如果您不熟悉 JSON Schema,可以在 Understanding JSON Schema 找到非常好的介绍。
尽可能推迟实际工作
由于 PHP 是一种没有“主流”支持并发编程的语言,因此每当服务器接收到批量请求时,它都必须顺序处理每个子请求,这可能会增加总响应时间。
因此,通过 HTTP 服务的 JSON-RPC 服务器应努力通过依赖例如 Beanstalkd
或 RabbitMQ
的工作队列来推迟任何实际工作。然后,第二个、出站的 JSON-RPC 服务器可以消费队列并执行实际工作。
实际上,该协议支持这种用法:当传入的请求没有 id
时,服务器必须不发送响应回(这类请求在规范中称为 Notifications
)。
限制批处理请求的数量
当服务器通过HTTP暴露时,批处理请求是一个拒绝服务向量(即使PHP能够并发处理它们)。恶意客户端可能发送包含数千个子请求的批处理请求,从而有效地阻塞服务器的资源。
为了降低这种风险,Server
有一个可选的 batchLimit
参数,该参数指定服务器可以处理的最大批处理请求数量。将其设置为1将有效地禁用批处理处理,如果您不需要该功能的话。
PS. 攻击者也可以发送数百或数千个单独的请求,同样会阻塞服务器。但是,由于这些是所有单独的HTTP请求,它们可以在web服务器级别进行速率限制。
$server = new \UMA\JsonRpc\Server($container, 2); $server->set('add', Adder::class); $response = $server->run('[ {"jsonrpc": "2.0", "method": "add", "params": [], "id": 1}, {"jsonrpc": "2.0", "method": "add", "params": [1,2], "id": 2}, {"jsonrpc": "2.0", "method": "add", "params": [1,2,3,4], "id": 3} ]'); // $response is '{"jsonrpc":"2.0","error":{"code":-32000,"message":"Too many batch requests sent to server","data":{"limit":2}},"id":null}'