深入剖析 Web 伺服器與 PHP 應用之間的通訊機制 – 掌握 CGI 和 FastCGI 協議的執行原理

柳公子發表於2019-02-16

本文首發於 深入剖析 Web 伺服器與 PHP 應用之間的通訊機制 – 掌握 CGI 和 FastCGI 協議的執行原理,轉載請註明出處!

身為一名使用 PHP 語言開發後端服務的程式猿,我們每天都和 PHP 以及 Web 伺服器產生無數次的親密接觸。得益於它們,我們才能夠如此快速的構建出令人陶醉的 Web 產品。

儘管我們已經和 Web 伺服器和 PHP 建立起深厚的友誼,但你知道它們之間為何能夠配合的如此默契麼?

這一切都需要從 CGI(Common Gateway Interface:通用閘道器介面)協議說起。但是請不要對 CGI 協議產生任何的恐懼心理,它並非什麼特別複雜的協議,如果你對它不甚瞭解,可能的原因或許是你還有花一點小心思來學習它。

所以,你應該明白,現在你應該抽出 20 多分鐘仔細的研究一下: Web 伺服器與 PHP 應用之間是如何進行通訊的這個問題。

介紹

我們知道 PHP 自 5.4 起為我們內建的 Web 伺服器。不過在此之前的版本(或者不使用這個內建伺服器時),我們就需要使用其他的 Web 伺服器,通常是 Nginx 或者 Apache 這兩塊 Web 伺服器,來部署我們的 PHP 應用。

這就涉及一個問題,當使用者發起一個 HTTP 請求後,我們的 PHP 應用程式在處理這個請求時並沒有直接的解析這個 HTTP 協議,而是可以直接從 $_GET$_POST$_SERVER等全域性變數中,獲取到使用者請求資料和其它系統環境。這究竟又是為何呢?

要想整明白這個問題,我們就不得不需要整明白一個問題:CGI 協議

CGI 協議同 HTTP 協議一樣是一個「應用層」協議,它的 功能 是為了解決 Web 伺服器與 PHP 應用(或其他 Web 應用)之間的通訊問題。

既然它是一個「協議」,換言之它與語言無關,即只要是實現類 CGI 協議的應用就能夠實現相互的通訊。

深入 CGI 協議

我們已經知道了 CGI 協議是為了完成 Web 伺服器和應用之間進行資料通訊這個問題。那麼,這一節我們就來看看究竟它們之間是如何進行通訊的。

簡單來講 CGI 協議它描述了 Web 伺服器和應用程式之間進行資料傳輸的格式,並且只要我們的程式語言支援標準輸入(STDIN)、標準輸出(STDOUT)以及環境變數等處理,你就可以使用它來編寫一個 CGI 程式。

CGI 的執行原理

  • 當使用者訪問我們的 Web 應用時,會發起一個 HTTP 請求。最終 Web 伺服器接收到這個請求。
  • Web 伺服器建立一個新的 CGI 程式。在這個程式中,將 HTTP 請求資料已一定格式解析出來,並通過標準輸入和環境變數傳入到 URL 指定的 CGI 程式(PHP 應用 $_SERVER)。
  • Web 應用程式處理完成後將返回資料寫入到標準輸出中,Web 伺服器程式則從標準輸出流中讀取到響應,並採用 HTTP 協議返回給使用者響應。

一句話就是 Web 伺服器中的 CGI 程式將接收到的 HTTP 請求資料讀取到環境變數中,通過標準輸入轉發給 PHP 的 CGI 程式;當 PHP 程式處理完成後,Web 伺服器中的 CGI 程式從標準輸出中讀取返回資料,並轉換回 HTTP 響應訊息格式,最終將頁面呈獻給使用者。然後 Web 伺服器關閉掉這個 CGI 程式。

可以說 CGI 協議特別擅長處理 Web 伺服器和 Web 應用的通訊問題。然而,它有一個嚴重缺陷,對於每個請求都需要重新 fork 出一個 CGI 程式,處理完成後立即關閉。

CGI 協議的缺陷

  • 每次處理使用者請求,都需要重新 fork CGI 子程式、銷燬 CGI 子程式。
  • 一系列的 I/O 開銷降低了網路的吞吐量,造成了資源的浪費,在大併發時會產生嚴重的效能問題。

深入 FastCGI 協議

從功能上來講,CGI 協議已經完全能夠解決 Web 伺服器與 Web 應用之間的資料通訊問題。但是由於每個請求都需要重新 fork 出 CGI 子程式導致效能堪憂,所以基於 CGI 協議的基礎上做了改進便有了 FastCGI 協議,它是一種常駐型的 CGI 協議。

本質上來將 FastCGI 和 CGI 協議幾乎完全一樣,它們都可以從 Web 伺服器裡接收到相同的資料,不同之處在於採取了不同的通訊方式。

再來回顧一下 CGI 協議每次接收到 HTTP 請求時,都需要經歷 fork 出 CGI 子程式、執行處理並銷燬 CGI 子程式這一系列工作。

而 FastCGI 協議採用 程式間通訊(IPC) 來處理使用者的請求,下面我們就來看看它的執行原理。

FastCGI 協議執行原理

  • FastCGI 程式管理器啟動時會建立一個 主(Master) 程式和多個 CGI 直譯器程式(Worker 程式),然後等待 Web 伺服器的連線。
  • Web 伺服器接收 HTTP 請求後,將 CGI 報文通過 套接字(UNIX 或 TCP Socket)進行通訊,將環境變數和請求資料寫入標準輸入,轉發到 CGI 直譯器程式。
  • CGI 直譯器程式完成處理後將標準輸出和錯誤資訊從同一連線返回給 Web 伺服器。
  • CGI 直譯器程式等待下一個 HTTP 請求的到來。

為什麼是 FastCGI 而非 CGI 協議

如果僅僅因為工作模式的不同,似乎並沒有什麼大不了的。並沒到非要選擇 FastCGI 協議不可的地步。

然而,對於這個看似微小的差異,但意義非凡,最終的結果是實現出來的 Web 應用架構上的差異。

CGI 與 FastCGI 架構

在 CGI 協議中,Web 應用的生命週期完全依賴於 HTTP 請求的宣告週期。

對每個接收到的 HTTP 請求,都需要重啟一個 CGI 程式來進行處理,處理完成後必須關閉 CGI 程式,才能達到通知 Web 伺服器本次 HTTP 請求處理完成的目的。

但是在 FastCGI 中完全不一樣。

FastCGI 程式是常駐型的,一旦啟動就可以處理所有的 HTTP 請求,而無需直接退出。

再看 FastCGI 協議

通過前面的講解,我們相比已經可以很準確的說出來 FastCGI 是一種通訊協議 這樣的結論。現在,我們就將關注的焦點挪到協議本身,來看看這個協議的定義。

同 HTTP 協議一樣,FastCGI 協議也是有訊息頭和訊息體組成。

訊息頭資訊

主要的訊息頭資訊如下:

  • Version:用於表示 FastCGI 協議版本號。
  • Type:用於標識 FastCGI 訊息的型別 – 用於指定處理這個訊息的方法。
  • RequestID:標識出當前所屬的 FastCGI 請求。
  • Content Length: 資料包包體所佔位元組數。

訊息型別定義

  • BEGIN_REQUEST:從 Web 伺服器傳送到 Web 應用,表示開始處理新的請求。
  • ABORT_REQUEST:從 Web 伺服器傳送到 Web 應用,表示中止一個處理中的請求。比如,使用者在瀏覽器發起請求後按下瀏覽器上的「停止按鈕」時,會觸發這個訊息。
  • END_REQUEST:從 Web 應用傳送給 Web 伺服器,表示該請求處理完成。返回資料包裡包含「返回的程式碼」,它決定請求是否成功處理。
  • PARAMS:「流資料包」,從 Web 伺服器傳送到 Web 應用。此時可以傳送多個資料包。傳送結束標識為從 Web 伺服器發出一個長度為 0 的空包。且 PARAMS 中的資料型別和 CGI 協議一致。即我們使用 $_SERVER 獲取到的系統環境等。
  • STDIN:「流資料包」,用於 Web 應用從標準輸入中讀取出使用者提交的 POST 資料。
  • STDOUT:「流資料包」,從 Web 應用寫入到標準輸出中,包含返回給使用者的資料。

Web 伺服器和 FastCGI 互動過程

  • Web 伺服器接收使用者請求,但最終處理請求由 Web 應用完成。此時,Web 伺服器嘗試通過套接字(UNIX 或 TCP 套接字,具體使用哪個由 Web 伺服器配置決定)連線到 FastCGI 程式。
  • FastCGI 程式檢視接收到的連線。選擇「接收」或「拒絕」連線。如果是「接收」連線,則從標準輸入流中讀取資料包。
  • 如果 FastCGI 程式在指定時間內沒有成功接收到連線,則該請求失敗。否則,Web 伺服器傳送一個包含唯一的 RequestIDBEGIN_REQUEST 型別訊息給到 FastCGI 程式。後續所有資料包傳送都包含這個 RequestID
    然後,Web 伺服器傳送任意數量的 PARAMS 型別訊息到 FastCGI 程式。一旦傳送完畢,Web 伺服器通過傳送一個空 PARAMS 訊息包,然後關閉這個流。
    另外,如果使用者傳送了 POST 資料 Web 伺服器會將其寫入到 標準輸入(STDIN) 傳送給 FastCGI 程式。當所有 POST 資料傳送完成,會傳送一個空的 標準輸入(STDIN) 來關閉這個流。
  • 同時,FastCGI 程式接收到 BEGIN_REQUEST 型別資料包。它可以通過響應 END_REQUEST 來拒絕這個請求。或者接收並處理這個請求。如果接收請求,FastCGI 程式會等待接收所有的 PARAMS標準輸入資料包。
    然後,在處理請求並將返回結果寫入 標準輸出(STDOUT) 流。處理完成後,傳送一個空的資料包到標準輸出來關閉這個流,並且會傳送一個 END_REQUEST 型別訊息通知 Web 伺服器,告知它是否發生錯誤異常。

為什麼需要在訊息頭髮送 RequestID 這個標識?

如果是每個連線僅處理一個請求,傳送 RequestID 則略顯多餘。

但是我們的 Web 伺服器和 FastCGI 程式之間的連線可能處理多個請求,即一個連線可以處理多個請求。所以才需要採用資料包協議而不是直接使用單個資料流的原因:以實現「多路複用」。

因此,由於每個資料包都包含唯一的 RequestID,所以 Web 伺服器才能在一個連線上傳送任意數量的請求,並且 FastCGI 程式也能夠從一個連線上接收到任意數量的請求資料包。

另外我們還需要明確一點就是 Web 伺服器 與 FastCGI 程式間通訊是 無序的。即使我們在互動過程中看起來一個請求是有序的,但是我們的 Web 伺服器也有可能在同一時間發出幾十個 BEGIN_REQUEST 型別的資料包,以此類推。

PHP-FPM

其實講解完 CGI 和 FastCGI 協議,基本上我們就已經研究完 「Web 伺服器與 PHP 應用之間的通訊機制」這個問題了。但是對於我們 PHP 軟體工程師來講,可能還會遇到「什麼是 PHP-FPM」及其相關問題。這裡我們一併來稍微講解一下。

PHP-FPM 是 FastCGI 程式管理器(PHP FastCGI Process Manager),用於替換 PHP 核心的 FastCGI 的大部分附加功能(或者說一種替代的 PHP FastCGI 實現),對於高負載網站是非常有用的。

下面是官網中獲取到的它所支援的特性:

  • 支援平滑停止 / 啟動的高階程式管理功能;
  • 可以工作於不同的 uid/gid/chroot 環境下,並監聽不同的埠和使用不同的 php.ini 配置檔案(可取代 safe_mode 的設定);
  • stdout 和 stderr 日誌記錄;
  • 在發生意外情況的時候能夠重新啟動並快取被破壞的 opcode;
  • 檔案上傳優化支援;
  • “慢日誌” – 記錄指令碼(不僅記錄檔名,還記錄 PHP backtrace 資訊,可以使用 ptrace 或者類似工具讀取和分析遠端程式的執行資料)執行所導致的異常緩慢;
  • fastcgi_finish_request() – 特殊功能:用於在請求完成和重新整理資料後,繼續在後臺執行耗時的工作(錄入視訊轉換、統計處理等);
  • 動態/靜態子程式產生;
  • 基本 SAPI 執行狀態資訊(類似 Apache 的 mod_status);
  • 基於 php.ini 的配置檔案。

那麼 PHP-FPM 是如何工作的呢?

PHP-FPM 程式管理器有兩種程式組成,一個 Master 程式和多個 Worker 程式。Master 程式負責監聽埠,接收來自 Web 伺服器的請求,然後指派具體的 Worker 程式處理請求;worker 程式則一般有多個 (依據配置決定程式數),每個程式內部都嵌入了一個 PHP 直譯器,用來執行 PHP 程式碼。

Nginx 伺服器如何與 FastCGI 協同工作

Nginx 伺服器無法直接與 FastCGI 伺服器進行通訊,需要啟用 ngx_http_fastcgi_module 模組進行代理配置,才能將請求傳送給 FastCGI 服務。

其中,包括我們熟知的配置指令:

  • fastcgi_pass 用於設定 FastCGI 伺服器的 IP 地址(TCT 套接字)或 UNIX 套接字。
  • fastcgi_param 設定傳入 FastCGI 伺服器的引數。

你可以到 PHP FastCGI 例項教程 學習一些基本使用。

總結

到這裡我們基本就學習完 CGI、FastCGI、PHP-FPM以及 Nginx 伺服器與 FastCGI 服務通訊原理。一句話:

CGI 和 FastCGI 是一種協議和 HTTP 協議一樣位於應用層,與語言無關;PHP-FPM 是一種 FastCGI 協議的實現,能夠管理 FastCGI 程式。

擴充套件閱讀

相關文章