Hyperf微服务——五、JsonRpc远程调用
- 一、JsonRpc请求的实现、
- 二、定义服务消费者
- 1.配置服务消费者文件
- 2.配置服务消费者绑定关系
- 3. 注入接口文件
- 4.封装rpc调用方法
一、JsonRpc请求的实现、
调用链从上到下
namespace App\JsonRpc;
//请求入口 通过容器获取接口对象
class IndexController extends AbstractController
{
public function index()
{
$user_info = ApplicationContext::getContainer()->get(UserInfoInterface::class);
return $user_info->getUserInfoById(1);
}
}
namespace App\JsonRpc;
//服务启动,自动生成服务提供者的接口代理文件
class UserInfoInterface_7be0c5bab4e9d0b2671b5482e5fa29f4 extends \Hyperf\RpcClient\Proxy\AbstractProxyService implements UserInfoInterface
{
public function getUserInfoById($id)
{
//通过抽象代理文件的__call方法进行调用
return $this->client->__call(__FUNCTION__, func_get_args());
}
}
namespace Hyperf\RpcClient;
//rpc客户端的底层实现
class ServiceClient extends AbstractServiceClient
{
/**
* @var MethodDefinitionCollectorInterface
*/
protected $methodDefinitionCollector;
/**
* @var string
*/
protected $serviceInterface;
/**
* @var NormalizerInterface
*/
private $normalizer;
public function __construct(ContainerInterface $container, string $serviceName, string $protocol = 'jsonrpc-http', array $options = [])
{
$this->serviceName = $serviceName;
$this->protocol = $protocol;
$this->setOptions($options);
parent::__construct($container);
$this->normalizer = $container->get(NormalizerInterface::class);
$this->methodDefinitionCollector = $container->get(MethodDefinitionCollectorInterface::class);
}
//真正的rpc调用方法
protected function __request(string $method, array $params, ?string $id = null)
{
if ($this->idGenerator instanceof IdGeneratorInterface && ! $id) {
$id = $this->idGenerator->generate();
}
//生成调用地址并发送消息体到服务提供者
$response = $this->client->send($this->__generateData($method, $params, $id));
if (! is_array($response)) {
throw new RequestException('Invalid response.');
}
$response = $this->checkRequestIdAndTryAgain($response, $id);
if (array_key_exists('result', $response)) {
$type = $this->methodDefinitionCollector->getReturnType($this->serviceInterface, $method);
if ($type->allowsNull() && $response['result'] === null) {
return null;
}
//消息体解码
return $this->normalizer->denormalize($response['result'], $type->getName());
}
if ($code = $response['error']['code'] ?? null) {
$error = $response['error'];
// Denormalize exception.
$class = Arr::get($error, 'data.class');
$attributes = Arr::get($error, 'data.attributes', []);
if (isset($class) && class_exists($class) && $e = $this->normalizer->denormalize($attributes, $class)) {
if ($e instanceof \Throwable) {
throw $e;
}
}
// Throw RequestException when denormalize exception failed.
throw new RequestException($error['message'] ?? '', $code, $error['data'] ?? []);
}
throw new RequestException('Invalid response.');
}
//对外部开放的调用方法
public function __call(string $method, array $params)
{
return $this->__request($method, $params);
}
protected function setOptions(array $options): void
{
$this->serviceInterface = $options['service_interface'] ?? $this->serviceName;
if (isset($options['load_balancer'])) {
$this->loadBalancer = $options['load_balancer'];
}
}
}
namespace Hyperf\RpcClient;
//打包消息为json格式并请求服务端
class Client
{
/**
* @var null|PackerInterface
*/
private $packer;
/**
* @var null|TransporterInterface
*/
private $transporter;
//消息体发送到服务提供者
public function send($data)
{
if (! $this->packer) {
throw new InvalidArgumentException('Packer missing.');
}
if (! $this->transporter) {
throw new InvalidArgumentException('Transporter missing.');
}
//进行消息体编码并发送
$packer = $this->getPacker();
$packedData = $packer->pack($data);
$response = $this->getTransporter()->send($packedData);
return $packer->unpack((string) $response);
}
//接收服务提供者返回的消息体
public function recv()
{
$packer = $this->getPacker();
$response = $this->getTransporter()->recv();
return $packer->unpack((string) $response);
}
public function getPacker(): PackerInterface
{
return $this->packer;
}
public function setPacker(PackerInterface $packer): self
{
$this->packer = $packer;
return $this;
}
public function getTransporter(): TransporterInterface
{
return $this->transporter;
}
public function setTransporter(TransporterInterface $transporter): self
{
$this->transporter = $transporter;
return $this;
}
}
namespace Hyperf\JsonRpc;
//jsosrpc发送消息到服务提供者并接收返回
class JsonRpcTransporter implements TransporterInterface
{
use RecvTrait;
/**
* @var null|LoadBalancerInterface
*/
private $loadBalancer;
/**
* If $loadBalancer is null, will select a node in $nodes to request,
* otherwise, use the nodes in $loadBalancer.
*
* @var Node[]
*/
private $nodes = [];
/**
* @var float
*/
private $connectTimeout = 5;
/**
* @var float
*/
private $recvTimeout = 5;
/**
* @var array
*/
private $config = [];
public function __construct(array $config = [])
{
$this->config = array_replace_recursive($this->config, $config);
$this->recvTimeout = $this->config['recv_timeout'] ?? 5.0;
$this->connectTimeout = $this->config['connect_timeout'] ?? 5.0;
}
/**
* 基于swoole协程客户端发送方法实现消息体发送
* swoole文档 https://wiki.swoole.com/#/coroutine_client/client?id=send
**/
public function send(string $data)
{
$client = retry(2, function () use ($data) {
$client = $this->getClient();
if ($client->send($data) === false) {
if ($client->errCode == 104) {
throw new RuntimeException('Connect to server failed.');
}
}
return $client;
});
return $this->recvAndCheck($client, $this->recvTimeout);
}
/**
* 基于swoole协程客户端接收方法实现消息体发送
* swoole文档 https://wiki.swoole.com/#/coroutine_client/client?id=recv
**/
public function recv()
{
$client = $this->getClient();
return $this->recvAndCheck($client, $this->recvTimeout);
}
/**
* 基于swoole协程客户端连接方法实现远程服务器连接
* swoole文档 https://wiki.swoole.com/#/coroutine_client/client?id=connect
**/
public function getClient(): SwooleClient
{
$class = spl_object_hash($this) . '.Connection';
if (Context::has($class)) {
return Context::get($class);
}
return Context::set($class, retry(2, function () {
$client = new SwooleClient(SWOOLE_SOCK_TCP);
$client->set($this->config['settings'] ?? []);
$node = $this->getNode();
$result = $client->connect($node->host, $node->port, $this->connectTimeout);
if ($result === false && ($client->errCode == 114 or $client->errCode == 115)) {
// Force close and reconnect to server.
$client->close();
throw new RuntimeException('Connect to server failed.');
}
return $client;
}));
}
public function getLoadBalancer(): ?LoadBalancerInterface
{
return $this->loadBalancer;
}
public function setLoadBalancer(LoadBalancerInterface $loadBalancer): TransporterInterface
{
$this->loadBalancer = $loadBalancer;
return $this;
}
/**
* @param \Hyperf\LoadBalancer\Node[] $nodes
*/
public function setNodes(array $nodes): self
{
$this->nodes = $nodes;
return $this;
}
public function getNodes(): array
{
return $this->nodes;
}
/**
* If the load balancer is exists, then the node will select by the load balancer,
* otherwise will get a random node.
*/
private function getNode(): Node
{
if ($this->loadBalancer instanceof LoadBalancerInterface) {
return $this->loadBalancer->select();
}
return $this->nodes[array_rand($this->nodes)];
}
}
//接收服务提供者返回的消息体并检查远程连接可用
trait RecvTrait
{
/**
* @param Client|RpcConnection $client
* @param float $timeout
*/
public function recvAndCheck($client, $timeout)
{
$data = $client->recv((float) $timeout);
if ($data === '') {
// RpcConnection: When the next time the connection is taken out of the connection pool, it will reconnecting to the target service.
// Client: It will reconnecting to the target service in the next request.
$client->close();
throw new RecvException('Connection is closed. ' . $client->errMsg, $client->errCode);
}
if ($data === false) {
$client->close();
throw new RecvException('Error receiving data, errno=' . $client->errCode . ' errmsg=' . swoole_strerror($client->errCode), $client->errCode);
}
return $data;
}
}
二、定义服务消费者
如果我们的调用场景是 用户服务 调用 订单服务 ,那么首先需要按照上一篇文章 Hyperf微服务——四、第一个微服务的搭建 创建一个订单服务, 然后在用户服务按照如下步骤定义订单服务的服务消费者配置。
1.配置服务消费者文件
通过在 config/autoload/services.php
配置文件内进行一些简单的配置,即可通过动态代理自动创建消费者类。
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
return [
'enable' => [
'discovery' => true,
'register' => true,
],
//通过闭包函数完成多个服务消费者的定义
'consumers' => value(function () {
$consumers = [];
//统一包含定义服务消费者的关系文件
$services = include __DIR__ . '/service_consumers.php';
//循环处理服务消费者的配置信息
foreach ($services as $name => $interface) {
$consumers[] = [
'name' => $name,
'service' => $interface,
'protocol' => 'jsonrpc',
'load_balancer' => 'random',
'registry' => [
'protocol' => env('PROTOCOL', 'consul'),
'address' => env('CONSUL_URI', 'http://127.0.0.1:8500'),
],
'options' => [
'connect_timeout' => 5.0,
'recv_timeout' => 5.0,
'settings' => [
// 根据协议不同,区分配置
'open_eof_split' => true,
'package_eof' => "\r\n",
// 'open_length_check' => true,
// 'package_length_type' => 'N',
// 'package_length_offset' => 0,
// 'package_body_offset' => 4,
],
// 重试次数,默认值为 2,收包超时不进行重试。暂只支持 JsonRpcPoolTransporter
'retry_count' => 2,
// 重试间隔,毫秒
'retry_interval' => 100,
// 使用多路复用 RPC 时的心跳间隔,null 为不触发心跳
'heartbeat' => 30,
// 当使用 JsonRpcPoolTransporter 时会用到以下配置
'pool' => [
'min_connections' => 1,
'max_connections' => 32,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
],
],
];
}
return $consumers;
}),
'providers' => [],
'drivers' => [
'consul' => [
'uri' => env('CONSUL_URI', 'http://127.0.0.1:8500'),
'token' => '',
'check' => [
'deregister_critical_service_after' => '90m',
'interval' => '1s',
],
],
],
];
2.配置服务消费者绑定关系
创建配置文件 config/autoload/service_consumers.php
定义服务消费者的关系,如果所有服务统一使用consul为注册中心,配置文件 config/autoload/services.php
的其他项不需要修改。
<?php
//定义服务消费者配置文件,用来快速注册服务消费者
return [
//用户服务
'OrderService' => App\JsonRpc\OrderInterface::class,
//...
];
3. 注入接口文件
这样便可以通过注入 OrderInterface 接口来使用客户端了
复制服务提供者订单服务的OrderInterface文件到用户服务的JsonRpc目录,如下如所示,启动用户服务即可进行RPC远程调用。
4.封装rpc调用方法
可以通过封装简易调用方法,以方便业务开发者使用,不需要关注调用逻辑。
//封装简易rpc调用,
public function rpc($interface, $function, $args)
{
$interface = (new \ReflectionClass($interface))->getName();
$data = ApplicationContext::getContainer()->get($interface)->$function($args);
return $data;
}
//调用demo
$this->rpc('\App\JsonRpc\OrderInterface','listOrder', []);
至此,一个简单的微服务RPC调用已经实现了。下面就可以根据实际的业务场景进行深度开发了。