uma/json-rpc

一个 JSON-RPC 2.0 服务器

v4.2.0 2024-02-22 16:40 UTC

This package is auto-updated.

Last update: 2024-09-22 17:52:22 UTC


README

Build Status Code Coverage

一个针对 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() 是一个包含两个整数的数组,或者是一个具有 minuendsubtrahend 属性的 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

middleware depiction

中间件示例

假设您想将传入的通知队列到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服务器应努力通过依赖例如BeanstalkdRabbitMQ这样的工作队列来推迟实际工作。然后,第二个独立于带宽的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}'