經過了前兩章的分享,我們成功地使用 PHP 建立起了一個非阻塞的常駐型伺服器。本章我們將關注於如果將 Workerman 與 Swow 等技術與 Anser 進行整合。
Anser 的核心在於給予發人員與微服務溝通的簡單方法,而溝通的則是交由 Anser-Action 負責。因此在進行框架或者是特別的系統環境整合時,我們首先需要確保的是 Anser-Action 元件能夠正常運作。
為了確定 Anser 是否執行正常,我們得先小小地修改一下上一章的 non-blocking-server.php
。
$httpWorker = new Worker('http://0.0.0.0:8081');
將 non-blocking-server.php
的監聽埠由 8080
改為 8081
。接著前往 init.php
:
ServiceList::addLocalService(
name: "nonBlockTestService",
address: "localhost",
port: 8081,
isHttps: false
);
將 nonBlockTestService
的相關設定給加入進去。
接下來,讓我們建立一個 anser-server.php
吧:
<?php
require_once './init.php';
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
use Swow\Coroutine;
use System\SwowDriver;
use SDPMlab\Anser\Service\Action;
use SDPMlab\Anser\Service\ActionInterface;
use Psr\Http\Message\ResponseInterface;
// #### http worker ####
$httpWorker = new Worker('http://0.0.0.0:8080');
$httpWorker->onMessage = static function (TcpConnection $connection, Request $request) {
Coroutine::run(static function () use ($connection, $request) : void {
$start = microtime(true);
$isSleep = $request->get(name: 'is_sleep', default: null);
$action = (new Action(
serviceName: 'nonBlockTestService',
method: 'get',
path: '/' . ($isSleep == null ? '' : '?is_sleep=yes')
))->setTimeout(15)
->doneHandler(static function (
ResponseInterface $response,
ActionInterface $action
) {
$body = $response->getBody()->getContents();
$action->setMeaningData($body);
})->do();
$connection->send($action->getMeaningData());
$end = microtime(true);
$time = $end - $start;
print_r(sprintf(
"[%s] %s%s, %.2fms\n",
$request->method(),
$request->host(),
$request->uri(),
$time * 1000
));
});
};
$httpWorker::$eventLoopClass = SwowDriver::class;
// Run all workers
Worker::runAll();
在上述的伺服器程式中,當 HTTP 請求到達,將透過一個 Action 實體連線至 nonBlockTestService
伺服器。依據請求中是否包含 is_sleep
參數,向 nonBlockTestService
伺服器發起對應的請求。最後將會回傳從 nonBlockTestService
伺服器取得的資料。
依據這兩章我們學習到的內容,應該可以預期這個伺服器有兩種可能的狀態:
AnserServer
迅速請求 nonBlockTestService
並迅速響應。sleep(10)
的請求:AnserServer
等待 nonBlockTestService
10 秒後響應。因為 nonBlockTestService
與 AnserServer
都是以 Coroutine 的模式執行的程式,所以可以預期就算等待 10 秒也並不會阻塞。
讓我們來進行測試吧,透過以下指令在兩個 Command Line 介面中將伺服器啟動。
php anser-server.php start
php non-blocking-server.php start
緊接著,讓我們對著 AnserServer
發出連線請求:
看起來還不錯?接著,我們讓伺服器進入 Sleep 狀態,再開啟新的 Tab 來測試我們預期的非阻塞模式有沒有正常執行:
經過實際的測試,你應該會發現結果與我們所預期的不一樣,伺服器阻塞了。當 AnserServer
開始因為 nonBlockTestService
等待時,執行緒就被 Action 的 API 請求給卡住了。
Anser-Action 基於 Guzzle 開發,而 Guzzle 的核心採用系統層級的 Curl 作為請求方式,並透過 Promise 模式建構而成的。在使用了 Workerman 與 Swow 的 Coroutine 環境中,Guzzle 的 HTTP 連線相關實作並不能很好地,在 API 陷入等待與執行緒處於閒置時,通知 Swow 釋放執行緒。
因此,我們需要將 Guzzle 核心的實作抽換成 Swow 提供的 HTTP Client,請將下列程式碼置入於你的 init.php
:
ServiceList::setGlobalHandlerStack(static function (
\GuzzleHttp\Psr7\Request $request,
array $options
): \GuzzleHttp\Promise\PromiseInterface {
if ($request->getUri()->getPort() === null) {
$prot = $request->getUri()->getScheme() == 'http' ? 80 : 443;
}
$client = new \Swow\Psr7\Client\Client();
$client->connect(
name: $request->getUri()->getHost(),
port: $prot ?? $request->getUri()->getPort()
);
$swowResponse = $client->setTimeout((int)$options['timeout']*1000)->sendRequest($request);
return \GuzzleHttp\Promise\Create::promiseFor($swowResponse);
});
Anser-Action 元件的 ServiceList::setGlobalHandlerStack()
方法提供了一種能夠讓你自訂 Guzzle 發送 HTTP 請求時的中介處理程序,我們可以透過這個方法抽換 Guzzle 內建的 HTTP 溝通實作。
因為 Swow 的 Request 物件與 Response 物件與 Guzzle 一樣皆採用 PSR-7 介面進行開發,因此能夠透過幾行程式碼就做到 HTTP 處理核心的抽換。
若使用了這種方式使 HTTP 請求支援 Coroutine 的特性,Guzzle 則可以繼續作為 Anser-Action 的 HTTP 客戶端,但背後的實際請求由 Swow 處理。這樣的組合利於整合現有的基於 Guzzle 的應用程式,無需大幅改動。
將上述改動至於 init.php
後,讓我們重啟 AnserServer
再做一次上一小節的實驗,你會發現一切暢通。
再完成了 Action 類別的 Coroutine 支援後,讓我們試試看將 Anser-Orchestration 給融入進伺服器內試試,讓我們建立起 anser_orch.php
:
<?php
require_once './init.php';
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
use Swow\Coroutine;
use System\SwowDriver;
use Orchestrators\UserLoginOrchestrator;
use Workerman\Protocols\Http\Response;
// #### http worker ####
$httpWorker = new Worker('http://0.0.0.0:8080');
$httpWorker->onMessage = static function (TcpConnection $connection, Request $request) {
Coroutine::run(static function () use ($connection, $request) : void {
$start = microtime(true);
$response = new Response(headers: [ 'Content-Type' => 'application/json']);
if($request->method() == 'POST' && $request->uri() == '/user/login'){
$userOrch = new UserLoginOrchestrator();
$result = $userOrch->build(
$request->post('email', ''),
$request->post('password', '')
);
$response->withBody(json_encode($result));
} else {
$response->withBody(json_encode([
'message' => 'Server is running',
]));
}
$connection->send($response);
$end = microtime(true);
$time = $end - $start;
print_r(sprintf(
"[%s] %s%s, %.2fms\n",
$request->method(),
$request->host(),
$request->uri(),
$time * 1000
));
});
};
$httpWorker::$eventLoopClass = SwowDriver::class;
// Run all workers
Worker::runAll();
在上述的 anser_orch.php
中依樣話葫蘆地實作了一個簡單的非阻塞 HTTP 伺服器。當使用者向 /user/login
路徑發送 POST
請求時,它會利用 UserLoginOrchestrator
來處理登入過程。若不是指定的路徑和方法,伺服器將響應 Server is running
。
在 Anser 的背景下,每一個與微服務的互動都會被包裝成一個 Action,然後這些 Actions 會被組合在一起形成一個完整的 Orchestration。這些 Action 的核心 Guzzle HTTP 被抽換成了 Swow 的非阻塞實作,因此無論協作器的執行是否因為微服務的處理速度而等待,伺服器也會在最合適的時間暫停當前協作器把持著的執行緒,讓別的協作器有機會快一些開始執行任務,從而提高了整體的效能和響應速度。
本章,我們了解到如何結合 Workerman、Swow 以及 Anser 進行 PHP 高效能的 Web 伺服器開發。從基礎的非阻塞伺服器設計到微服務的整合,再到更進階的協作器模式,我們初步開啟了 PHP 的其他潛能。
透過 Workerman 的支援,我們可以輕鬆地在 PHP 中建立節省資源的常駐型伺服器,並與 Swow 的協同工作,以非阻塞的任務管理進一步強化系統的同步處理能力。
本章,已完成了入門的高效能 PHP 簡介以及實作,恭喜你一起走到了這裡。