[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 返回的请求参数: '.$data;
}
}