iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Modern Web

30 天上手! PHP 微服務入門與開發系列 第 29

第二十九章、高效能PHP: Anser 與非阻塞常駐型 PHP Web 伺服器 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

經過了前兩章的分享,我們成功地使用 PHP 建立起了一個非阻塞的常駐型伺服器。本章我們將關注於如果將 Workerman 與 Swow 等技術與 Anser 進行整合。

整合 Action

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 伺服器取得的資料。

依據這兩章我們學習到的內容,應該可以預期這個伺服器有兩種可能的狀態:

  1. 進行普通的請求:AnserServer 迅速請求 nonBlockTestService 並迅速響應。
  2. 進行 sleep(10) 的請求:AnserServer 等待 nonBlockTestService 10 秒後響應。

因為 nonBlockTestServiceAnserServer 都是以 Coroutine 的模式執行的程式,所以可以預期就算等待 10 秒也並不會阻塞。

讓我們來進行測試吧,透過以下指令在兩個 Command Line 介面中將伺服器啟動。

php anser-server.php start
php non-blocking-server.php start

緊接著,讓我們對著 AnserServer 發出連線請求:

http://localhost:8085/

看起來還不錯?接著,我們讓伺服器進入 Sleep 狀態,再開啟新的 Tab 來測試我們預期的非阻塞模式有沒有正常執行:

http://localhost:8085/?is_sleep=yes

經過實際的測試,你應該會發現結果與我們所預期的不一樣,伺服器阻塞了。當 AnserServer 開始因為 nonBlockTestService 等待時,執行緒就被 Action 的 API 請求給卡住了。

置換 Guzzle 核心

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 再做一次上一小節的實驗,你會發現一切暢通。

延伸整合 Anser-Orchestration 協作器

再完成了 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 簡介以及實作,恭喜你一起走到了這裡。


上一篇
第二十八章、高效能PHP:SWOW & Workerman - 以 Coroutine 實現的非阻塞常駐型 PHP Web 伺服器 - PHP 微服務入門與開發
下一篇
第三十章、系列回顧 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言