远程过程调用——RPC(Remote Procedure Call)
,在《UNIX 网络编程》
一书中是这样描述的:被调用过程和调用过程处于不同的进程中,一个进程调用同一台主机上另一个进程的某个过程(函数)。RPC
通常允许一台主机上的某个客户调用另一台主机上的某个服务器过程,只要这两台主机以某种形式的网络连接着。
RPC
的实现方式有很多,如XML-RPC
、JSON-RPC
、SOAP
等,这里我们使用JSON
作为数据传输格式,UDP
作为网络传输协议,PHP
作为编程语言。具体传输格式如下:
{
"class": "Test",
"method": "say",
"params": ["william", "你好,世界"]
}
为了简化示例,代码仅能调用类方法,通过class
指定类,method
指定方法名,params
指定参数。
RPC 服务端
Test
是服务端中可被客户端调用的类;RPCServer
用于接收RPCClient
请求并解释、代替客户端调用类方法。
<?php
// RPCServer.php
class Test
{
public function say(string $name, string $message, mixed ...$extra): string
{
return json_encode([
'name' => $name,
'message' => $message,
'extra' => [...$extra],
], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
}
}
class RPCServer
{
private $socket;
private function _wrapQuote(string $str): string
{
return '"' . $str . '"';
}
/**
* @throws Exception
*/
public function __construct(string $host, int $port)
{
$this->socket = stream_socket_server("udp://$host:$port", $errno, $error, STREAM_SERVER_BIND);
if (!$this->socket) {
throw new Exception("$errno: $error");
}
while (true) {
$buf = stream_socket_recvfrom($this->socket, 1024, 0, $client);
if ($buf) {
$json = json_decode($buf, true);
if (
isset($json['class'], $json['method'], $json['params'])
&& is_string($json['class']) && is_string($json['method']) && is_array($json['params'])
) {
if (
class_exists($json['class'])
&& method_exists($obj = new $json['class'](), $json['method'])
) {
try {
$res = call_user_func([$obj, $json['method']], ...$json['params']);
stream_socket_sendto($this->socket, json_encode($res), 0, $client);
} catch (Throwable $e) {
stream_socket_sendto($this->socket, $this->_wrapQuote($e->getFile(). '(line: '. $e->getLine(). '): '. $e->getMessage()), 0, $client);
}
} else {
stream_socket_sendto($this->socket, $this->_wrapQuote('class or method not exist'), 0, $client);
}
} else {
stream_socket_sendto($this->socket, $this->_wrapQuote('invalid protocol'), 0, $client);
}
}
}
}
public function __destruct()
{
fclose($this->socket);
}
}
try {
new RPCServer('127.0.0.1', 10240);
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}
$ php RPCServer.php
RPC 客户端
Test
只是一个伪类,作用就是提供IDE
提示,其中的文档注释对开发友好,实际上去掉也不影响功能完整性;RPCClient
作为代理,发起远程过程调用;Factory
起到隐藏网络细节的作用,让用户以为这只是一个本地的调用。
<?php
// RPCClient.php
/**
* @method string say(string $param1, string $param2, mixed ...$extra)
*/
class Test {}
class RPCClient
{
private string $host;
private int $port;
private string $class;
function __construct(string $host, int $port, string $class)
{
$this->host = $host;
$this->port = $port;
$this->class = $class;
}
/**
* @throws Exception
*/
function __call(string $method, $params)
{
$client = stream_socket_client("udp://$this->host:$this->port", $errno, $error);
if (!$client) {
throw new Exception("$errno: $error");
}
$json = json_encode([
'class' => $this->class,
'method' => $method,
'params' => $params,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
fwrite($client, $json);
$data = fread($client, 1024);
fclose($client);
return $data;
}
}
class Factory
{
public static function create(string $class): RPCClient
{
return new RPCClient('127.0.0.1', 10240, $class);
}
}
/**
* @var Test $test
*/
$test = Factory::create('Test');
$res = $test->say('william', '你好,世界', ['数组', ['name' => 'william']], 'extra');
var_dump(json_decode($res, true));
$res = $test->say('william', '你好,世界', ['数组', ['name' => 'william']], 'extra1', 'extra2');
var_dump(json_decode($res, true));
$res = $test->say('william');
var_dump(json_decode($res, true));
$res = $test->say();
var_dump(json_decode($res, true));
$res = $test->hello();
var_dump(json_decode($res, true));
$ php RPCClient.php
string(94) "{"name":"william","message":"你好,世界","extra":[["数组",{"name":"william"}],"extra"]}"
string(104) "{"name":"william","message":"你好,世界","extra":[["数组",{"name":"william"}],"extra1","extra2"]}"
string(113) "/usr/share/php/RPCServer.php(line: 6): Too few arguments to function Test::say(), 1 passed and exactly 2 expected"
string(113) "/usr/share/php/RPCServer.php(line: 6): Too few arguments to function Test::say(), 0 passed and exactly 2 expected"
string(25) "class or method not exist"