PHP的rpc客戶端和服務端
[TOC]
PHP的rpc客戶端和服務端
PHP的rpc有很多解決方案, composer上可以看得到很多開遠的基於PHP程式碼的rpc框架.
也有很多基於C的PHP擴充套件的rpc框架,比如: yar,grpc.都很不錯.
這裡比較推薦的是grpc,grpc同時支援多種語言包括但不限於,go,php,c++, grpc官方php文件
現成框架的用起來都很簡單, 直接安裝呼叫即可,我就不說了.這裡直說一下,使用PHP程式碼寫的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