今天我们共同学习下使用MQ来构建一个RPC系统。包含一个客户端和一个RPC服务端。现在的情况是,我们没有一个值得被分发的足够耗时的任务,所以接下来,我们创建一个模拟RPC服务。
客户端的接口
为了展示rpc服务如何使用,我们创建了一个简单的客户端,
关于RPC的注意事项:
尽管RPC在计算机领域是一个常用模式,但它也有一些问题,当一个问题被抛出时,程序员往往意识不到这到底是由本地调用还是由较慢的RPC调用引起的。同样的困惑还来自系统的不可预见性,和给调试工作带来的不必要的复杂性。跟软件精简不同的是,滥用RPC会导致不可维护性。
回调队列
一般来说通过MQ来实现RPC是很容易的。一个客户端发送请求信息,服务端将其应用到一个回复信息中,为了接收到回复信息,客户端需要在发送请求的时候发送一个回调队列的地址,我们可以使用默认的队列,我们试试看:
消息属性
MQ协议给消息预定义了一系列的14个属性,大多数属性很少会用到
- delivery_mode 投递模式 : 将消息标记为持久的值为2 或暂存的除了2之外的任何值。
- content_type 内容类型 : 用来描述编码mime-type 列如在实际使用中常常使用application/json来描述JOSN编码类型。
- reply_to 回复目标 :通常命名回调队列
- correlation_id(关联标识):用来将RPC的响应和请求关联起来。
关联标示
我建议给每个rpc 请求建立一个回调队列,我们可以为每个客户端只建立一个独立的回调队列。
这就带来一个新问题,当此队列接收到一个响应的时候它无法辨别出这个响应属于哪个请求。correlation_id 就是为了解决这个问题。我们给每个请求设置一个独一无二的值,稍后,当我们从回调队列中接收到一个消息的时候,我们就可以查看这条属性从而将响应和请求匹配起来,如果我们接收到的消息correlation_id是未知的。那就直接销毁它吧!因为它不属于任何请求。
你也许会问,为什么我们接收到未知消息的时候不抛出一个错误,而是要将它忽略掉?这是为了解决服务端有可能发生的竞争情况,尽管可能性不大,但是RPC服务器还是有可能已将应答发送给我们但还未确认消息发送给请求的情况下死掉。如果这种情况发生,RPC在重启后会重新处理请求,这就是为什么我们必须在客户端优雅的处理重复响应,同时RPC也需要尽可能保持幂等性。
我们的RPC如此工作:
- 当客户端启动的时候,它创建一个匿名独享的回调队列。
- 在RPC请求中,客户端发送带有两个属性的消息:一个是设置回调队列的reply_to 属性,另个是设置唯一值得 correlation_id 属性。
- 将请求发送到一个rpc_queue队列中。
- RPC工作者 (服务器)等待请求发送到这个队列中来,当请求出现的时候,它执行它的工作并且将带有执行结果的消息发送给reply_to字段指定的队列。
- 客户端等待回调队列的数据。当有消息出现的时候,它会检测correlation_id属性。 如果此属性的值与请求匹配,将它发回给应用。
RPC服务端脚本
<?php
/**
* Created by DemoController.php.
* User: gongzhiyang
* Date: 19/6/18
* Time: 6:40 下午
*/
namespace console\controllers;
use yii;
use yii\console\Controller;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
/**
* demo
* Class DemoController
* @package console\controllers
*/
class RpcServerController extends Controller
{
private $channel;
private $connection;
public function init ()
{
$amqp = yii::$app->params['amqp'];
//建立一个到RabbitMQ服务器的连接
$this->connection = new AMQPStreamConnection($amqp["host"], $amqp["port"], $amqp["user"], $amqp["password"]);
$this->channel = $this->connection->channel();
}
/**
* RPC服务端
*/
public function actionRpcServer()
{
//建立一个到RabbitMQ服务器的连接
$connection = $this->connection;
$channel = $this->channel;
//接下来,我们创建一个通道
$channel->queue_declare('rpc_queue',false,false,false,false);
function fib($n) {
return $n;
}
//回调
$callback = function($req){
$n = intval($req->body);
echo " [.] fib(", $n, ")\n";
$msg = new AMQPMessage(
(string) fib($n),
array('correlation_id' => $req->get('correlation_id'))
);
$req->delivery_info['channel']->basic_publish(
$msg,'', $req->get('reply_to')
);
$req->delivery_info['channel']->basic_ack(
$req->delivery_info['delivery_tag']
);
};
$channel->basic_qos(null,1,null);
$channel->basic_consume('rpc_queue','',false,false,false,false,$callback);
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
}
}```
RPC 客户端代码
<?php /** * Created by DemoController.php. * User: gongzhiyang * Date: 19/6/18 * Time: 6:40 下午 */ namespace console\controllers; use yii; use yii\console\Controller; use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Message\AMQPMessage; /** * demo * Class DemoController * @package console\controllers */ class RpcClientController extends Controller { private $channel; private $connection; private $callback_queue; private $corr_id; private $response; public function init () { $amqp = yii::$app->params['amqp']; //建立一个到RabbitMQ服务器的连接 $this->connection = new AMQPStreamConnection($amqp["host"], $amqp["port"], $amqp["user"], $amqp["password"]); $this->channel = $this->connection->channel(); //建立信道 排他性的(Exclusive Queue)。 list($this->callback_queue, ,) = $this->channel->queue_declare("",false,false,true,false); //回调 $callback = function($rep){ echo $this->corr_id; if($rep->get('correlation_id') == $this->corr_id) { $this->response = $rep->body; } }; //接收回调信息 $this->channel->basic_consume( $this->callback_queue,'',false,false,false,false,$callback); } /** * RPC */ public function RpcClient($n) { if(empty($n)) 30; $this->response = null; $this->corr_id = uniqid(); $msg = new AMQPMessage( (string) $n, array('correlation_id' => $this->corr_id, 'reply_to' => $this->callback_queue) ); // var_dump( array('correlation_id' => $this->corr_id, // 'reply_to' => $this->callback_queue)); // echo $n; $this->channel->basic_publish($msg, '', 'rpc_queue'); //等待响应 while(!$this->response) { $this->channel->wait(); } //var_dump($this->response); // // $this->channel->close(); // $this->connection->close(); return intval($this->response); } public function actionAll(){ //$fibonacci_rpc = $this->RpcClient(); $response = $this->RpcClient(30); echo " [.] Got ", $response, "\n"; } }``` 我们的RPC服务已经准备就绪了,现在启动服务器端: ``` gongzgiyangdeMacBook-Air:yii2advanced gongzhiyang$ ./yii rpc-server/rpc-server [.] fib(30) [.] fib(30) [.] fib(30) [.] fib(30) [.] fib(30) [.] fib(30) [.] fib(30) ``` 运行客户端: ``` gongzgiyangdeMacBook-Air:yii2advanced gongzhiyang$ ./yii rpc-client/all [.] Got 30 ```