1. 程式人生 > >Swoole 是 PHP 中的 Node.js?

Swoole 是 PHP 中的 Node.js?

一想到那些可以使用 Node 的同事,一些 PHP 開發者的臉都嫉妒綠了。非同步 Node 系統可以在不同協議間共享程式碼庫,並在程式碼之外提供服務。這真的想讓一個人轉 Node 開發。實際上 PHP 中也有類似於 Node 的存在,並被列入了 PHP 拓展,叫做 Swoole。

 

PHP 中的 Node ?Swoole 到底是什麼?

我先從 官方文件 中引用下 Swoole 的定義:

Swoole:面向生產環境的 PHP 非同步網路通訊引擎。
使 PHP 開發人員可以編寫高效能、可拓展的非同步併發 TCP、UDP、Unix Socket、HTTP,WebSocket 服務,而無需深入瞭解非阻塞 I/O 程式設計和初級 Linux 核心。

Swoole 使用 C 語言編寫,作為 PHP 的 基本擴充套件 存在。聽起來可還行,是吧?用 PHP 來執行 HTTP 服務?用 PHP 實現 Websockets ?還有其他的可能性,是不是很風騷?而且所有的這些都會保持極高的效能,我們來看看吧!

 

如何讓它執行?

不同平臺的安裝方法有差異。

對於 Linux 來說,只需要執行一條 PECL 命令:

pecl install swoole

MacOS 使用者可以使用 brew 命令:

brew install swoole
brew install homebrew/php/php72-swoole
譯者注:截止翻譯時,Brew 官方已經移除了所有 PHP 擴充套件,請使用 PECL 安裝。

暫時不支援在 Windows 上的安裝,但是可以使用 Docker 的方式。

 

使用 Docker 執行 Swoole

毫無疑問,執行 PHP + Swoole 的最佳方案便是 Docker。讓我們來看看如何建立一個包含 Swoole 的容器。首先,我們需要建立一個 Dockerfile。

FROM php:latest\
RUN pecl install swoole\
ADD php.ini /usr/local/etc/php\
RUN usermod -u 1000 www-data

這看起來十分直接。基於 PHP 官方 Docker 映象,使用 PECL 安裝 Swoole,接著複製 php.ini 到映象內 —— 搞定。最後一行是 MacOS 的 Docker 一個常規的許可權修復命令。

至於被複制的 php.ini 配置檔案,它只需一行:

extension=swoole.so

 

Swoole 可以做什麼?

Swoole 有許多功能,大部分是非同步執行。以下是其中最讓人感興趣的部分(其他的可以在 Swoole 官方文件 中找到):

  • TCP/UDP 服務端與客戶端,
  • HTTP 服務端與客戶端,
  • Websocket 服務端與客戶端,
  • 基於 Redis 協議的服務端與客戶端,
  • MySQL 客戶端,
  • 原子性,
  • 檔案系統。

我們來看下其中的 HTTP 服務、Websocket 服務、檔案系統怎麼使用。在我看來這是最重要的幾個功能。

 

基於 Swoole 實現 HTTP 服務

基於 Swoole 僅需少量程式碼即可實現一個簡易的非同步 HTTP 服務。以下是一份示例程式碼,該例子使用非同步檔案系統來讀取 index.html 檔案並作為響應返回給它處理的每條請求。

<?php
chdir(__DIR__);
$http = new swoole_http_server('php', 8080);
$http->on('start', function ($server) {
    echo "Server has been started!\n";
});
$http->on('request', function ($request, $response) {
    swoole_async_readfile('index.html', function($filename, $content) use ($response) {
        $response->header('Content-Type', 'text/html');
        $response->end($content);
    });
});
$http->start();

 

如你所見,這段程式碼看起來有點像 Node.js 的風格。

首先,我們建立一個類似 HTTP 服務的 swoole_http_server 物件。接著,繫結兩個非同步回撥函式到以下事件:一個用於啟動,將會在服務啟動時被呼叫;另一個用於請求,將會在收到每次請求時被呼叫,它帶有 $request 和 $response 兩個引數。

$request 物件包含了所有與請求相關的資料:請求路徑(Path)、頭資訊(Headers)等等。而 $response 被用來提供輸出、設定響應頭等。值得一提的是,以上兩個物件都不符合 PSR 標準,而是 Swoole 自定義的。
在請求事件中,非同步請求檔案系統用於從檔案載入資料。 一旦資料可用,就會在資料載入完成後觸發回撥。然後將此資料載入到響應體並關閉比此次響應。 這將會把資料有效地傳送回瀏覽器。

這樣看起來很簡潔,最重要的是 --- 能執行起來。 來看下它的效能如何呢?

 

HTTP Server 標準

為了使用 Swoole 測試 HTTP 伺服器的效能,我在 Node 中建立了一個應用程式 --- 它可以與 Swoole 中的應用程式完全相同 - 還有一個 伺服器,它將提供 index.html 作為靜態檔案。 全部執行在 3 個獨立的容器中。

然後,我用 wrk 工具給這些容器進行壓力測試。 結果令人震驚。

 

 

 

 

Swoole 的工作效能要比預期的好很多!

這令人驚訝。 我沒想到 Swoole 會超越 Nginx ,但它確實做到了!這也遠遠超過了 Node 。 這個擴充套件的原始功能確實令人印象深刻,但它在請求中完成了更多工作後逐漸消失。 不幸的是, Swoole 有兩個小缺點,使這些缺點和原始標準有些偏差。 我們稍後會找到他們。

 

在 Websocket 服務中使用 Swoole

如前所述, Swoole 提供了一種建立 websocket 伺服器的方法。 它以非同步方式來進行工作,遵循與 HTTP 協議並和 Swoole 部分方法功能相同。 在我看來,它是最重要的 Swoole 元件之一。 來吧,在 PHP 執行中的 websockets 會是怎麼樣。讓我們看看它的結果。

<?php
$server = new swoole_websocket_server('php', 9501);
$server->on('start', function (swoole_websocket_server $server) {
    echo "Server has been started!\n";
});
$server->on('open', function (swoole_websocket_server $server, $request) {
    echo "websocket: new connection, id: {$request->fd}\n";
});
$server->on('message', function (swoole_websocket_server $server, $frame) {
    echo "websocket: {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    $server->push($frame->fd, "Replying, you sent " . $frame->data);
});
$server->on('close', function (swoole_websocket_server $server, $fd) {
    echo "websocket: connection with id {$fd} has been closed\n";
});
$server->start();

 

看起來類似於 HTTP 伺服器的示例。

首先,我們建立類似於 websocket 伺服器的 swoole_websocket_server 物件。 然後,我們將 4 個匿名函式繫結到 4 個事件。 第一個啟動事件,它將像 HTTP 伺服器的啟動事件一樣工作。 第二個執行事件,它會在連線另一個 websocket 後執行。 第三個訊息事件將在 websocket 向伺服器傳送訊息時執行。最後 --- 關閉時間會在 websocket 斷開連線時執行。

ID 是作為 Websocket 連線到伺服器的唯一標識,該 ID 隨每個新的 websockets 進行遞增。

 

使用 Swoole 時遇到的問題

到目前為止,這一切都執行良好,但在使用 Swoole 測試某些解決方案時遇到了兩個問題。 我將它列出來:

  • HTTP 伺服器中沒有真正的支援 HTTPS,
  • 指令碼中不支援全域性變數。

第一問題個很容易解決。 我們只需要使用 Nginx 或任何負載均衡裝置設定反向代理,就完成了。 但通過這樣做,我們就失去了 Swoole 提供的極端效能。

第二個問題更棘手。 Swoole 生成用於處理 HTTP 請求的工作程序,這意味著如果我們建立一個全域性變數,它的值線上程之間是獨立的,並且它不能工作。下面這段程式碼是顯示問題所在之處。

<?php
$server = new swoole_websocket_server('php', 9501);
$server->on('start', function (swoole_websocket_server $server) {
    echo "Server has been started!\n";
});
$server->on('open', function (swoole_websocket_server $server, $request) {
    echo "websocket: new connection, id: {$request->fd}\n";
});
$server->on('message', function (swoole_websocket_server $server, $frame) {
    echo "websocket: {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    $server->push($frame->fd, "Replying, you sent " . $frame->data);
});
$server->on('close', function (swoole_websocket_server $server, $fd) {
    echo "websocket: connection with id {$fd} has been closed\n";
});
$server->start();

 

預期中響應的資訊將返回 0 ,然後返回 1, 2 , 3 等等,但它總是返回 0 。

我找到了 Swoole 的作者來檢查它是否是一個 bug ,但事實並非如此。 為了獲得我們期望的行為,我們可以在配置中設定 worker_num = 1 ,但這會降低部分效能。

 

結論

總的來說,Swoole 有明亮的側面也有黑暗的角落。我認為將非同步程式設計引入 PHP 仍然是一個好主意。 它可用於各種情況,包括快速設計原型,簡潔且責任單一的微服務,低延遲遊戲伺服器以及作為大型框架的後端伺服器。 確實有前途。

 推薦閱讀:

PHP 當Swoole 遇上 ThinkPHP5