今天我們共同學習下使用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
```