本文首發於 深入剖析 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 服務器發送一個包含唯一的 RequestID 的 BEGIN_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 進程。
擴展閲讀
- https://blog.cuiyongjian.com/...
- https://zhuanlan.zhihu.com/p/...
- https://stackoverflow.com/que...
- https://paper.seebug.org/289/
- https://blog.csdn.net/shreck6...
- http://www.phppan.com/2011/05...
- https://github.com/pangudashu...
- https://github.com/YuanLianDu...
- http://blog.51cto.com/1358182...
- http://haiyangxu.github.io/po...
- https://www.cnblogs.com/xuewe...
- https://www.zybuluo.com/phper...
- https://www.awaimai.com/371.html
- http://tiankonguse.com/blog/?...
- https://www.digitalocean.com/...
- http://www.whizkidtech.redpri...
- https://fastcgi-archives.gith...
- http://php.net/manual/zh/inst...
- http://php.net/manual/zh/rese...
- http://www.php-internals.com/...
- https://www.yanxurui.cc/posts...