一个简单的 RPC 示例

2024/12/29 计算机网络

远程过程调用——RPC(Remote Procedure Call),在《UNIX 网络编程》一书中是这样描述的:被调用过程和调用过程处于不同的进程中,一个进程调用同一台主机上另一个进程的某个过程(函数)。RPC通常允许一台主机上的某个客户调用另一台主机上的某个服务器过程,只要这两台主机以某种形式的网络连接着。

RPC的实现方式有很多,如XML-RPCJSON-RPCSOAP等,这里我们使用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"

Search

    Table of Contents