1. 程式人生 > >PHP的rpc客戶端和服務端

PHP的rpc客戶端和服務端

[TOC]

PHP的rpc客戶端和服務端

PHP的rpc有很多解決方案, composer上可以看得到很多開遠的基於PHP程式碼的rpc框架.

也有很多基於C的PHP擴充套件的rpc框架,比如: yar,grpc.都很不錯.

這裡比較推薦的是grpc,grpc同時支援多種語言包括但不限於,go,php,c++, grpc官方php文件

現成框架的用起來都很簡單, 直接安裝呼叫即可,我就不說了.這裡直說一下,使用PHP程式碼寫的rpc服務.

swoole官方提供的rpc服務端和客戶端程式碼

下面這個簡單的案例很簡單,就是這麼個結構.

~/Desktop/rpcDemo ⌚ 18:45:43
$ tree
.
├── rpcClient.php
├── rpcServer.php
└── service
    └── test.php

1 directory, 3 files

rpc協議簡單的說明

RPC(Remote Procedure Call)—遠端過程呼叫

RPC採用客戶機/伺服器模式。請求程式就是一個客戶機,而服務提供程式就是一個伺服器。

RPC框架與具體的協議無關。RPC 可基於 HTTP 或 TCP 協議,Web Service 就是基於 HTTP 協議的 RPC,它具有良好的跨平臺性,但其效能卻不如基於 TCP 協議的 RPC。

用的最多的時,各種虛擬貨幣錢包中的呼叫都是通過RPC來實現的.

簡而言之, 一般RPC呼叫都用在跨語言呼叫或者分部署呼叫中.

比較簡單的方式是資料包中每一行一個引數,冒號前為欄位名,冒號後為值.

rpc-module:moduleName;\n
rpc-class:className;\n
rpc-action:className;\n
rpc-params:paramsData;\n

比如要呼叫https://www.baidu.com/hospital/putian?patient=liyanhong&disease=eatbloodbread

但是,如果百度用其他語言寫的,或者說為了便於呼叫,不想用一般的那種resetApi介面的方式拿介面返回值.

假定百度的服務是用PHP寫的,沒有做路由.那他的檔案路徑應該是: /index/hospital/putian/傳遞了兩個引數是:patient=liyanhong和disease=eatbloodbread

這時候就要用RPC了,兩邊可以協商通訊方式和引數傳遞的方式,那他的傳參方式應該是這樣的.

rpc-module:index;\n
rpc-class:hospital;\n
rpc-action:putian;\n
rpc-params:{"patient":"liyanhong","isease":"eatbloodbread"};\n

簡單的純PHP程式碼的rpc服務端

<?php

/**
 * rpc服務
 */
class rpcServer
{
    protected $server;

    /**
     * 返回資料 function.
     *
     * @param [type] $data
     *
     * @return mixed
     */
    public function response($data)
    {
        if (is_array($data)) {
            $data = json_encode($data);
        }
        echo $data.PHP_EOL;

        return;
    }

    /**
     * 建立服務 function.
     *
     * @param [string] $host 連結
     * @param [int]    $port 埠號
     */
    public function instance($host, $port)
    {
        //建立一個 Socket 服務
        //AF_INET IPv4 網路協議
        //SOCK_STREAM 提供一個順序化的、可靠的、全雙工的、基於連線的位元組流。支援資料傳送流量控制機制。TCP 協議即基於這種流式套接字。
        //SOL_TCP   TCP和UDP對應SOL_TCP 和 SOL_UDP
        if (($this->server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0) {
            $this->response('socket_create() 執行失敗:'.socket_strerror(socket_last_error()));
        }

        //設定埠重用
        if (!socket_set_option($this->server, SOL_SOCKET, SO_REUSEADDR, 1)) {
            $this->response('socket_set_option() 執行失敗:'.socket_strerror(socket_last_error()));
        }
        //繫結埠
        if (socket_bind($this->server, $host, $port) < 0) {
            $this->response('socket_set_option() 執行失敗:'.socket_strerror(socket_last_error()));
        }
        //監聽埠
        if ((socket_listen($this->server, 3)) < 0) {
            $this->response('socket_listen() 執行失敗:'.socket_strerror(socket_last_error()));
        }
    }

    /**
     * 建構函式 function.
     *
     * @param [string] $host 連結
     * @param [int]    $port 埠號
     * @param [string] $path 請求的方法路徑
     */
    public function __construct($host, $port, $path)
    {
        // 判斷 RPC 服務目錄是否存在
        $realPath = realpath(__DIR__.$path);
        if (!is_dir($realPath)) {
            $this->response('path引數錯誤,目錄不存在:'.$path);
        }
        //啟用服務端
        $this->instance($host, $port);
        //執行操作
        $this->processing($realPath);
    }

    /**
     * processing function.
     *
     * @param [string] $realPath 服務程式碼路徑
     */
    public function processing($realPath)
    {
        do {
            //開始執行操作
            $client = socket_accept($this->server);
            if ($client) {
                // 一次性讀取
                $buffer = socket_read($client, 1024);
                echo '接收到的客戶端1024長度內的資料: '.PHP_EOL.$buffer.PHP_EOL;

                //正則判斷客戶端提交來的資料
                $classData = preg_match('/Rpc-Class:\s(.*);\n/i', $buffer, $class);
                $methodData = preg_match('/Rpc-Method:\s(.*);\n/i', $buffer, $method);
                $paramsRet = preg_match('/Rpc-Params:\s(.*);\n/i', $buffer, $params);
                if ($classData && $methodData) {
                    $class = $class[1];
                    $method = $method[1];
                    $params = $params[1];
                    if (!empty($params)) {
                        $params = json_decode($params, true);
                        if (is_array($params)) {
                            $params = implode(',', $params);
                        }
                    }

                    $file = $realPath.'/'.$class.'.php';  // 類檔案需要和類名一致
                    $data = ''; // 執行結果,作為返回值
                    // 判斷類檔案是否存在
                    if (file_exists($file)) {
                        // 引入類檔案
                        include $file;
                        // 例項化類, ReflectionClass它是用來匯出或提取出關於類、方法、屬性、引數等的詳細資訊,包括註釋。
                        $refObj = new ReflectionClass($class);
                        // 判斷指定方法是否存在該物件中
                        if ($refObj->hasMethod($method)) {
                            // 執行該物件方法
                            $refMethod = $refObj->getMethod($method);
                            if (!empty($params)) {
                                //為方法傳遞引數
                                $data = $refMethod->invokeArgs($refObj->newInstance(), [$params]);
                            }
                        } else {
                            socket_write($client, '方法不存在');
                        }
                        //把執行後的結果返回給客戶端
                        socket_write($client, $data);
                    }
                } else {
                    socket_write($client, '物件或方法不存在');
                }

                // 關閉客戶端
                socket_close($client);
            }
        } while (true);
    }

    /**
     * 解構函式 function.
     */
    public function __destruct()
    {
        socket_close($this->server);
    }
}
//這裡Mac的直接用這埠最好
new rpcServer('127.0.0.1', 8080, '/service');

簡單的純PHP程式碼的rpc客戶端

<?php

/**
 * rpc客戶端類.
 */
class rpcClient
{

    protected $client = null;
    protected $urlInfo = [];   // 遠端呼叫 URL 組成部分

    /**
     * rpcClient constructor.
     *
     * @param $url
     */
    public function __construct($url)
    {

        // 解析 URL
        $this->urlInfo = parse_url($url);
    }

    /**
     * Method  __call
     *
     * @desc    ......
     *
     * @param $name
     * @param $arguments
     *
     * @return  void
     */
    public function __call($name, $arguments)
    {
        $socketHandler = fsockopen($this->urlInfo['host'], $this->urlInfo['port']);
        // 傳遞呼叫的類名
        $class = basename($this->urlInfo['path']);
        // 傳遞呼叫的引數
        $args = '';
        if (isset($arguments[0])) {
            $args = json_encode($arguments[0]);
        }
        // 向服務端傳送我們自定義的協議資料
        $data = "Rpc-Class: {$class};" . PHP_EOL
            . "Rpc-Method: {$name};" . PHP_EOL
            . "Rpc-Params: {$args};" . PHP_EOL;
        fputs($socketHandler, $data);
        $start_time   = time();
        $responseData = '';
        while (!feof($socketHandler)) {
            $responseData .= fread($socketHandler, 1024);
            $diff         = time() - $start_time;
            if ($diff > 24) {
                die('Timeout!n');
            }
            $status = stream_get_meta_data($socketHandler);
            if ($status['timed_out']) {
                $this->response('Stream Timeout!n');
            }
        }
        var_dump($responseData);
        fclose($socketHandler);
    }

    /**
     * 返回資料 function.
     *
     * @param [type] $data
     *
     * @return mixed
     */
    public function response($data)
    {
        if (is_array($data)) {
            $data = json_encode($data);
        }
        echo $data . PHP_EOL;

        return;
    }
}

//這裡Mac的直接用這埠最好
$rpcClient = new RpcClient('http://127.0.0.1:8080/test');
echo $rpcClient->demo1(['title' => '標題', 'content' => '內容']);

service目錄下的檔案的test.php

<?php

class test
{
    public function __construct()
    {
    }

    public function demo1($data = '')
    {
        return '這是test::demo1 返回的請求引數: '.$data;
    }

    public function demo2($data = '')
    {
        return '這是test::demo2 返回的請求引數: '.$da