uma / json-rpc
一个 JSON-RPC 2.0 服务器
Requires
- php: ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.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
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
对象,这两个属性都是整数。
这是完全安全的,因为 Server
在调用 __invoke()
之前会将其与上面定义的 JSON 模式进行匹配。如果输入不符合规范,则会返回 -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; });
到这一点,我们有一个 JSON-RPC 服务器,其中只有一个方法(subtract
),它映射到 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 Validator。这允许您使用自定义 Opis 过滤器、格式 和 媒体类型。
为此,只需在 PSR-11 容器中定义一个 Opis\JsonSchema\Validator::class
服务并将其设置为 Validator 类的自定义实例。
以下示例定义了一个新的整数 "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
对象可以json编码回原始有效载荷
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甚至通过鸟类载体或纸张上运行。这实际上是为什么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,请参阅理解JSON Schema的非常好的介绍。
尽可能推迟实际工作
由于PHP是一种没有“主流”支持并发编程的语言,因此每当服务器收到批请求时,它必须按顺序处理每个子请求,这可能会增加总响应时间。
因此,通过HTTP提供的JSON-RPC服务器应努力通过依赖例如Beanstalkd
或RabbitMQ
这样的工作队列来推迟实际工作。然后,第二个独立于带宽的JSON-RPC服务器可以消费队列并执行实际工作。
实际上,该协议支持这种用法:每当一个传入请求没有id
时,服务器必须不发送响应回去(这种请求在规范中被称为通知
)。
限制批处理请求的数量
当服务器
通过HTTP公开时,批量请求是一个拒绝服务的向量(即使PHP能够并发处理它们)。恶意客户端可能会发送包含数千个子请求的批量请求,从而有效地阻塞服务器的资源。
为了最小化这种风险,服务器
有一个可选的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}'