前言
RPC即跨專案呼叫。可以將一個大的專案分成多個子專案,然後子專案之間透過對外提供API從而實現了專案之間的解耦,再者便於各個子專案的單獨部署和管理。而子專案之間的呼叫除了提供API之外,還可以透過比如Yar
將專案的類對外提供服務,其他專案對類的呼叫就猶如呼叫自己的類一樣。
再此透過RabbitMQ實現的RPC佇列也可以完成跨專案呼叫,但是實現的方式和Yar不同。實現的原理圖如下:
1、client將request(包含corrid和reply_to引數)傳送到了事先定義好的佇列rpc_queue
2、啟動rpc_queue
佇列的消費者去處理佇列中的訊息,並且將響應結果投遞到reply_to指定的臨時佇列
3、等待臨時佇列的消費者返回rpc_queue
的結果並校驗,最終返回client。
4、client根據返回訊息的corrid和request的corrid對比(批次請求時用到)。
RPC佇列的配置和使用
配置和普通佇列的配置一樣簡單,在Yii的配置檔案中配置RPC佇列的元件屬性:
/** RPC佇列 */
'rpcQueue' => [
'class' => \pzr\amqp\queue\RpcQueue::class,
'host' => '127.0.0.1',
'port' => 5672,
'user' => 'guest',
'password' => 'guest',
'queueName' => 'rpc',
'exchangeName' => 'rpc',
'routingKey' => 'rpc',
'duplicate' => 2,
],
然後在控制器中呼叫元件物件:
public function actionRpc() {
Yii::$app->rpcQueue->on(AmqpBase::EVENT_BEFORE_PUSH, function(PushEvent $event) {
Yii::$app->rpcQueue->bind();
});
// 批次請求
for ($i=1; $i<=10; $i++) {
$jobs[] = new RequestJob([
'request' => 'request_' . $i,
]);
}
$response = Yii::$app->rpcQueue->publish($jobs);
return $response;
}
這裡RequestJob是消費體物件必須繼承AmqpJob,程式碼如:
class RequestJob extends AmqpJob
{
public $request;
public function execute()
{
$response = $this->request . ', corrid:' . $this->getUuid();
return $response;
}
}
不出意外得到:
request_1,corrid:xxxx
request_2,corrid:xxxx
... //省略
request_9,corrid:xxxx
這裡的corrid是請求的唯一標誌。因為支援批次請求,如果對結果的返回需要對號入座那麼就需要用到corrid。
RPC佇列的實現
竟然講到了RPC,那麼就以跨專案呼叫舉例(很細緻):
場景:A專案期望呼叫B專案的某個方法
step1:在A和B專案中都執行引入pzr/amqp包操作
composer require pzr/amqp
step2:引入包之後,可以看到在vendor目錄下有pzr/amqp目錄,目錄大致情況是:
pzr/amqp
forntend
src
如果只是為了測試功能大可以在frontend目錄下修改Yii2的配置和控制器,但是一旦執行composer update操作可能就會被覆蓋。因此建議copy frontend目錄。
frontend目錄是AMQP消費者程式web管理,所以很多和web相關的程式碼對於Rpc呼叫來說其實並無作用。frontend目錄大致情況是:
assets/
commands/
config/
controllers/
models/
vagrant/
views/
web/
widgets/
如果只是為了實現RPC功能只有以下目錄留著就行:
commands/
config/
models/
這裡假設copy的frontend重新命名為yii
step3:A呼叫B,所以可以理解為A為客戶端,B為服務端
A的使命就是發出請求並且等待響應,B的使命就是響應請求。因為A和B是兩個專案,所以專案環境的不同也將導致一個問題:B為了響應A的請求,佇列的消費者“必須”在B工作環境下啟動,否則可能導致A接收不到預期的結果。使用過Yar的同學肯定知道Yar的一個方便之處在於RPC呼叫的API也提供視覺化,對於Client呼叫方來說非常友好!但是這裡似乎做不到API的視覺化,呼叫的基礎是A已經瞭解B專案的呼叫方法。基於此去展開的RPC呼叫。
之前希望B可以返回A呼叫的序列化物件,但是發現物件能被序列化的只有非靜態化變數,方法和靜態屬性等都無法序列化。因此只能採取Client告知Server我需要呼叫哪個class、哪個method,傳入什麼引數。Server得到Client的請求之後就會按請求封裝物件,然後響應方法。如:
(new $class())->method($args)
。為了解決名稱空間的問題,往往Client在請求的時候需要帶上如:\namespace\class 去請求Server,於是Server才能夠正確找到需要例項化的物件。對於本身使用Composer管理的專案,那麼很輕鬆的就能載入並找到\namespace\class,而對於不是使用Composer管理的專案,那麼需要自己去引入名稱空間的autoload方法。
step4:準備工作完了之後,開始編寫程式碼
在A專案中yii/commands
下實現ClientController類發出請求(無關程式碼不體現):
class ClientController extends Controller
{
// qos消費者預處理數
// timeout 請求超時時間,如果超過時間還沒接收到完整資料則返回結果
public function actionRequest($jobs, $qos=1, $timeout=3) {
if (empty($jobs)) {
return null;
}
// 傳送訊息之前先繫結訊息,因為對於請求方來說如果傳送的請求丟失可以自己重新請求,所以關閉傳送方訊息確認機制
Yii::$app->on(AmqpBase::EVENT_BEFORE_PUSH, function(PushEvent $event) {
$event->noWait = true;
Yii::$app->rpcQueue->bind();
});
$response = Yii::$app->rpcQueue->setQos($qos)
->setTimeout($timeout)
->publish($jobs);
return $response;
}
}
A專案中的Client算是完成了,接下來需要在models編寫Job類。Job類可以寫成通用型,即A專案中所有的RPC呼叫都可以複用的那種。
class RpcJob extends AmqpJob
{
public $object; //請求的物件
public $action; //請求的方法
public $params = []; //方法可能需要的引數
// 這是執行請求響應的方法,只有在Server專案中啟動的消費者才能夠有效。如果在其他環境啟動消費者,那麼可能導致請求接收到不到響應資料(因為其他工作環境無法例項化並且響應請求)。
public function execute()
{
try {
$object = $this->object;
$obj = new $object();
if (!is_object($obj)) {
return false;
}
if (!method_exists($obj, $this->action)) {
return false;
}
return call_user_func_array([$obj, $this->action], $this->params);
} catch (Exception $e) {
} catch (Throwable $e) {
}
return false;
}
}
最後只需要在A專案中實際需要RPC呼叫的地方去呼叫Client/request即可,如在A專案中的IndexController(個人專案的控制器,和Yii2無關)中進行RPC呼叫。
class IndexController
{
public function init()
{
$yii = new MyYii();
$chargeStatusUuid = uniqid(true);
$jobs[] = new RpcJob([
'object' => 'PayConfig',
'action' => 'getChargeStatus',
'params' => [],
'uuid' => $chargeStatusUuid, //唯一標誌請求ID,當然如果你喜歡也可以自己隨便寫,只要能夠區分請求即可。
]);
$response = $yii->request([
'client/request', // 採用的Yii2的請求規則,類似:php yii client/request ,但是這裡我對yii指令碼改造了下,實現了MyYii
$jobs
]);
$chargeStatus = $response[$chargeStatusUuid];
}
}
上面的例項程式碼實現的是:A在init方法中想要呼叫B專案中的某個配置方法。然後B和A的實現步驟完全一致即可。
step5:在B專案中的實現和A完全一模一樣,不同的是在B專案中需要啟動消費者去響應
最後在A得到結果截圖如:
B中關於chargeStatus 的配置截圖如:
如果是多個請求同時傳送,那麼按corrid對應獲取。
後言
在後來對這個專案也升級過很多次,當然存在很多缺陷也一直最佳化。也在使用的過程當中經常碰到各種問題。
1、找不到對應的檔案vendor/bower-asset/jquery/dist
方案1:composer require “fxp/composer-asset-plugin” -vvv (這個成功了)
方案2:在composer.json新增如下(未嘗試):
"config": {
"fxp-asset": {
"installer-paths": {
"npm-asset-library": "vendor/npm",
"bower-asset-library": "vendor/bower"
}
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結