This commit is contained in:
li
2026-01-28 23:48:20 +08:00
commit b8cd0c9418
1293 changed files with 184011 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace think\swoole\rpc;
class Error implements \JsonSerializable
{
/**
* @var int
*/
protected $code = 0;
/**
* @var string
*/
protected $message = '';
/**
* @var mixed
*/
protected $data;
/**
* @param int $code
* @param string $message
* @param mixed $data
*
* @return Error
*/
public static function make(int $code, string $message, $data = null): self
{
$instance = new static();
$instance->code = $code;
$instance->message = $message;
$instance->data = $data;
return $instance;
}
/**
* @return int
*/
public function getCode(): int
{
return $this->code;
}
/**
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
public function jsonSerialize()
{
return [
'code' => $this->code,
'message' => $this->message,
'data' => $this->data,
];
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace think\swoole\rpc;
use Throwable;
class File extends \think\File
{
public function __destruct()
{
//销毁时删除临时文件
try {
if (file_exists($this->getPathname())) {
unlink($this->getPathname());
}
} catch (Throwable $e) {
}
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace think\swoole\rpc;
use Exception;
use think\swoole\contract\rpc\ParserInterface;
class JsonParser implements ParserInterface
{
/**
* Json-rpc version
*/
const VERSION = '2.0';
const DELIMITER = "@";
/**
* @param Protocol $protocol
*
* @return string
*/
public function encode(Protocol $protocol): string
{
$interface = $protocol->getInterface();
$methodName = $protocol->getMethod();
$method = $interface . self::DELIMITER . $methodName;
$data = [
'jsonrpc' => self::VERSION,
'method' => $method,
'params' => $protocol->getParams(),
'id' => '',
];
$string = json_encode($data, JSON_UNESCAPED_UNICODE);
return $string;
}
/**
* @param string $string
*
* @return Protocol
*/
public function decode(string $string): Protocol
{
$data = json_decode($string, true);
$error = json_last_error();
if ($error != JSON_ERROR_NONE) {
throw new Exception(
sprintf('Data(%s) is not json format!', $string)
);
}
$method = $data['method'] ?? '';
$params = $data['params'] ?? [];
if (empty($method)) {
throw new Exception(
sprintf('Method(%s) cant not be empty!', $string)
);
}
$methodAry = explode(self::DELIMITER, $method);
if (count($methodAry) < 2) {
throw new Exception(
sprintf('Method(%s) is bad format!', $method)
);
}
[$interfaceClass, $methodName] = $methodAry;
if (empty($interfaceClass) || empty($methodName)) {
throw new Exception(
sprintf('Interface(%s) or Method(%s) can not be empty!', $interfaceClass, $method)
);
}
return Protocol::make($interfaceClass, $methodName, $params);
}
/**
* @param string $string
*
* @return mixed
*/
public function decodeResponse(string $string)
{
$data = json_decode($string, true);
if (array_key_exists('result', $data)) {
return $data['result'];
}
$code = $data['error']['code'] ?? 0;
$message = $data['error']['message'] ?? '';
$data = $data['error']['data'] ?? null;
return Error::make($code, $message, $data);
}
/**
* @param mixed $result
*
* @return string
*/
public function encodeResponse($result): string
{
$data = [
'jsonrpc' => self::VERSION,
];
if ($result instanceof Error) {
$data['error'] = $result;
} else {
$data['result'] = $result;
}
$string = json_encode($data);
return $string;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace think\swoole\rpc;
class Packer
{
const HEADER_SIZE = 8;
const HEADER_STRUCT = "Nlength/Ntype";
const HEADER_PACK = "NN";
const TYPE_BUFFER = 0;
const TYPE_FILE = 1;
public static function pack($data, $type = self::TYPE_BUFFER)
{
return pack(self::HEADER_PACK, strlen($data), $type) . $data;
}
public static function unpack($data)
{
$header = unpack(self::HEADER_STRUCT, substr($data, 0, self::HEADER_SIZE));
$data = substr($data, self::HEADER_SIZE);
return [$header, $data];
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace think\swoole\rpc;
class Protocol
{
const ACTION_INTERFACE = '@action_interface';
const FILE = '@file';
/**
* @var string
*/
private $interface = '';
/**
* @var string
*/
private $method = '';
/**
* @var array
*/
private $params = [];
/**
* Replace constructor
*
* @param string $interface
* @param string $method
* @param array $params
*
* @return Protocol
*/
public static function make(string $interface, string $method, array $params)
{
$instance = new static();
$instance->interface = $interface;
$instance->method = $method;
$instance->params = $params;
return $instance;
}
/**
* @return string
*/
public function getInterface(): string
{
return $this->interface;
}
/**
* @return string
*/
public function getMethod(): string
{
return $this->method;
}
/**
* @return array
*/
public function getParams(): array
{
return $this->params;
}
}
@@ -0,0 +1,14 @@
<?php
namespace think\swoole\rpc\client;
use Generator;
interface Connector
{
/**
* @param Generator|string $data
* @return string
*/
public function sendAndRecv($data);
}
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace think\swoole\rpc\client;
use RuntimeException;
use Swoole\Client;
use Swoole\Coroutine;
use think\File;
use think\helper\Arr;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\exception\RpcClientException;
use think\swoole\exception\RpcResponseException;
use think\swoole\rpc\Error;
use think\swoole\rpc\Packer;
use think\swoole\rpc\Protocol;
class Gateway
{
protected $connector;
/** @var ParserInterface */
protected $parser;
/**
* Gateway constructor.
* @param Connector|array $connector
* @param ParserInterface $parser
*/
public function __construct($connector, ParserInterface $parser)
{
if (is_array($connector)) {
$connector = $this->createDefaultConnector($connector);
}
$this->connector = $connector;
$this->parser = $parser;
}
protected function encodeData(Protocol $protocol)
{
$params = $protocol->getParams();
//有文件,先传输
foreach ($params as $index => $param) {
if ($param instanceof File) {
$handle = fopen($param->getPathname(), "rb");
yield pack(Packer::HEADER_PACK, $param->getSize(), Packer::TYPE_FILE);
while (!feof($handle)) {
yield fread($handle, 8192);
}
fclose($handle);
$params[$index] = Protocol::FILE;
}
}
$protocol = Protocol::make($protocol->getInterface(), $protocol->getMethod(), $params);
$data = $this->parser->encode($protocol);
yield Packer::pack($data);
}
protected function decodeResponse($response)
{
[, $response] = Packer::unpack($response);
$result = $this->parser->decodeResponse($response);
if ($result instanceof Error) {
throw new RpcResponseException($result);
}
return $result;
}
public function sendAndRecv(Protocol $protocol)
{
$response = $this->connector->sendAndRecv($this->encodeData($protocol));
return $this->decodeResponse($response);
}
public function getServices()
{
$response = $this->connector->sendAndRecv(Packer::pack(Protocol::ACTION_INTERFACE));
return $this->decodeResponse($response);
}
protected function createDefaultConnector($config)
{
$class = Coroutine::getCid() > -1 ? Coroutine\Client::class : Client::class;
/** @var Client|Coroutine\Client $client */
$client = new $class(SWOOLE_SOCK_TCP);
$host = Arr::pull($config, 'host');
$port = Arr::pull($config, 'port');
$timeout = Arr::pull($config, 'timeout');
$client->set(array_merge($config, [
'open_length_check' => true,
'package_length_type' => Packer::HEADER_PACK,
'package_length_offset' => 0,
'package_body_offset' => 8,
]));
if (!$client->connect($host, $port, $timeout)) {
throw new RuntimeException(
sprintf('Connect failed host=%s port=%d', $host, $port)
);
}
return new class($client) implements Connector {
protected $client;
/**
* constructor.
* @param Client|Coroutine\Client $client
*/
public function __construct($client)
{
$this->client = $client;
}
public function sendAndRecv($data)
{
if (!$data instanceof \Generator) {
$data = [$data];
}
foreach ($data as $string) {
if (!$this->client->send($string)) {
throw new RpcClientException(swoole_strerror($this->client->errCode), $this->client->errCode);
}
}
$response = $this->client->recv();
if ($response === false || empty($response)) {
throw new RpcClientException(swoole_strerror($this->client->errCode), $this->client->errCode);
}
return $response;
}
};
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace think\swoole\rpc\client;
use InvalidArgumentException;
use Nette\PhpGenerator\Factory;
use Nette\PhpGenerator\PhpNamespace;
use ReflectionClass;
use think\swoole\rpc\Protocol;
abstract class Proxy
{
protected $interface;
/** @var Gateway */
protected $gateway;
final public function __construct(Gateway $gateway)
{
$this->gateway = $gateway;
}
final protected function proxyCall($method, $params)
{
$protocol = Protocol::make($this->interface, $method, $params);
return $this->gateway->sendAndRecv($protocol);
}
final public static function getClassName($client, $interface)
{
if (!interface_exists($interface)) {
throw new InvalidArgumentException(
sprintf('%s must be exist interface!', $interface)
);
}
$proxyName = class_basename($interface) . "Service";
$className = "rpc\\service\\{$client}\\{$proxyName}";
if (!class_exists($className, false)) {
$namespace = new PhpNamespace("rpc\\service\\{$client}");
$namespace->addUse(Proxy::class);
$namespace->addUse($interface);
$class = $namespace->addClass($proxyName);
$class->setExtends(Proxy::class);
$class->addImplement($interface);
$class->addProperty('interface', class_basename($interface));
$reflection = new ReflectionClass($interface);
foreach ($reflection->getMethods() as $methodRef) {
$method = (new Factory)->fromMethodReflection($methodRef);
$body = "\$this->proxyCall('{$methodRef->getName()}', func_get_args());";
if ($method->getReturnType() != 'void') {
$body = "return {$body}";
}
$method->setBody($body);
$class->addMember($method);
}
eval($namespace);
}
return $className;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace think\swoole\rpc\server;
use Swoole\Coroutine;
use think\swoole\rpc\Packer;
use think\swoole\rpc\server\channel\Buffer;
use think\swoole\rpc\server\channel\File;
class Channel
{
protected $header;
protected $queue;
public function __construct($header)
{
$this->header = $header;
$this->queue = new Coroutine\Channel(1);
Coroutine::create(function () use ($header) {
switch ($header['type']) {
case Packer::TYPE_BUFFER:
$type = Buffer::class;
break;
case Packer::TYPE_FILE:
$type = File::class;
break;
default:
throw new \RuntimeException('not support data type');
}
$handle = new $type($header['length']);
$this->queue->push($handle);
});
}
/**
* @return File|Buffer
*/
public function pop()
{
return $this->queue->pop();
}
public function push($handle)
{
return $this->queue->push($handle);
}
public function close()
{
return $this->queue->close();
}
}
@@ -0,0 +1,186 @@
<?php
namespace think\swoole\rpc\server;
use Exception;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use Swoole\Server;
use think\App;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\rpc\Error;
use think\swoole\rpc\File;
use think\swoole\rpc\Packer;
use think\swoole\rpc\Protocol;
use Throwable;
class Dispatcher
{
/**
* Parser error
*/
const PARSER_ERROR = -32700;
/**
* Invalid Request
*/
const INVALID_REQUEST = -32600;
/**
* Method not found
*/
const METHOD_NOT_FOUND = -32601;
/**
* Invalid params
*/
const INVALID_PARAMS = -32602;
/**
* Internal error
*/
const INTERNAL_ERROR = -32603;
protected $app;
protected $parser;
protected $services = [];
protected $server;
protected $files = [];
public function __construct(App $app, ParserInterface $parser, Server $server, $services)
{
$this->app = $app;
$this->parser = $parser;
$this->server = $server;
$this->prepareServices($services);
}
/**
* 获取服务接口
* @param $services
* @throws \ReflectionException
*/
protected function prepareServices($services)
{
foreach ($services as $className) {
$reflectionClass = new ReflectionClass($className);
$interfaces = $reflectionClass->getInterfaceNames();
foreach ($interfaces as $interface) {
$this->services[class_basename($interface)] = [
'interface' => $interface,
'class' => $className,
];
}
}
}
/**
* 获取接口信息
* @return array
*/
protected function getInterfaces()
{
$interfaces = [];
foreach ($this->services as $key => ['interface' => $interface]) {
$interfaces[$key] = $this->getMethods($interface);
}
return $interfaces;
}
protected function getMethods($interface)
{
$methods = [];
$reflection = new ReflectionClass($interface);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$returnType = $method->getReturnType();
if ($returnType instanceof ReflectionNamedType) {
$returnType = $returnType->getName();
}
$methods[$method->getName()] = [
'parameters' => $this->getParameters($method),
'returnType' => $returnType,
'comment' => $method->getDocComment(),
];
}
return $methods;
}
protected function getParameters(ReflectionMethod $method)
{
$parameters = [];
foreach ($method->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType) {
$type = $type->getName();
}
$param = [
'name' => $parameter->getName(),
'type' => $type,
];
if ($parameter->isOptional()) {
$param['default'] = $parameter->getDefaultValue();
}
$parameters[] = $param;
}
return $parameters;
}
/**
* 调度
* @param int $fd
* @param string|File $data
*/
public function dispatch(int $fd, $data)
{
if ($data instanceof File) {
$this->files[$fd][] = $data;
} else {
try {
if ($data === Protocol::ACTION_INTERFACE) {
$result = $this->getInterfaces();
} else {
$protocol = $this->parser->decode($data);
$interface = $protocol->getInterface();
$method = $protocol->getMethod();
$params = $protocol->getParams();
//文件参数
foreach ($params as $index => $param) {
if ($param === Protocol::FILE) {
$params[$index] = array_shift($this->files[$fd]);
}
}
$service = $this->services[$interface] ?? null;
if (empty($service)) {
throw new Exception(
sprintf('Service %s is not founded!', $interface),
self::INVALID_REQUEST
);
}
$result = $this->app->invoke([$this->app->make($service['class']), $method], $params);
}
} catch (Throwable | Exception $e) {
$result = Error::make($e->getCode(), $e->getMessage());
}
$data = $this->parser->encodeResponse($result);
$this->server->send($fd, Packer::pack($data));
//清空文件缓存
unset($this->files[$fd]);
}
}
}
@@ -0,0 +1,30 @@
<?php
namespace think\swoole\rpc\server\channel;
class Buffer
{
protected $data = '';
protected $length;
public function __construct($length)
{
$this->length = $length;
}
public function write(&$data)
{
$size = strlen($this->data);
$string = substr($data, 0, $this->length - $size);
$this->data .= $string;
if (strlen($data) >= $this->length - $size) {
$data = substr($data, $this->length - $size);
return $this->data;
} else {
$data = '';
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace think\swoole\rpc\server\channel;
class File
{
protected $name;
protected $handle;
protected $length;
public function __construct($length)
{
$this->name = tempnam(sys_get_temp_dir(), "swoole_rpc_");
$this->handle = fopen($this->name, 'ab');
$this->length = $length;
}
public function write(&$data)
{
$size = fstat($this->handle)['size'];
$string = substr($data, 0, $this->length - $size);
fwrite($this->handle, $string);
if (strlen($data) >= $this->length - $size) {
fclose($this->handle);
$data = substr($data, $this->length - $size);
return new \think\swoole\rpc\File($this->name);
} else {
$data = '';
}
}
}