CGI,FastCGI,PHP-CGI,PHP-FPM

CGI 簡介

CGI全稱是「通用網關接口」(Common Gateway Interface),它可讓一個客戶端,從網頁瀏覽器向執行在Web服務器上的程序請求數據。 CGI描述了客戶端和這個程序之間傳輸數據的一種標準。 CGI的一個目的是要獨立於任何語言的,因此CGI能夠用任何一種語言編寫,只要這種語言具備標準輸入、輸出和環境變量。 如php,perl,tcl等。php

CGI 的運行原理

  1. 客戶端訪問某個 URL 地址以後,經過 GET/POST/PUT 等方式提交數據,並經過 HTTP 協議向 Web 服務器發出請求。
  2. 服務器端的 HTTP Daemon(守護進程)啓動一個子進程。而後在子進程中,將 HTTP 請求裏描述的信息經過標準輸入 stdin 和環境變量傳遞給 URL 指定的 CGI 程序,並啓動此應用程序進行處理,處理結果經過標準輸出 stdout 返回給 HTTP Daemon 子進程。
  3. 再由 HTTP Daemon 子進程經過 HTTP 協議返回給客戶端。

上面的這段話理解可能仍是比較抽象,下面咱們就經過一次 GET 請求爲例進行詳細說明。html

 

圖2.7 CGI 運行原理示舉例示意圖
圖2.7 CGI 運行原理示舉例示意圖

 

如圖所示,本次請求的流程以下:linux

  1. 客戶端訪問 http://127.0.0.1:9003/cgi-bin/user?id=1
  2. 127.0.0.1 上監聽 9003 端口的守護進程接受到該請求
  3. 經過解析 HTTP 頭信息,得知是 GET 請求,而且請求的是 /cgi-bin/ 目錄下的 user 文件。
  4. 將 uri 裏的 id=1 經過存入 QUERY_STRING 環境變量。
  5. Web 守護進程 fork 一個子進程,而後在子進程中執行 user 程序,經過環境變量獲取到id
  6. 執行完畢以後,將結果經過標準輸出返回到子進程。
  7. 子進程將結果返回給客戶端。

FastCGI 簡介

FastCGI是Web服務器和處理程序之間通訊的一種協議, 是CGI的一種改進方案,FastCGI像是一個常駐(long-lived)型的CGI, 它能夠一直執行,在請求到達時不會花費時間去fork一個進程來處理(這是CGI最爲人詬病的fork-and-execute模式)。 正是由於他只是一個通訊協議,它還支持分佈式的運算,因此 FastCGI 程序能夠在網站服務器之外的主機上執行,而且能夠接受來自其它網站服務器的請求。api

FastCGI 是與語言無關的、可伸縮架構的 CGI 開放擴展,將 CGI 解釋器進程保持在內存中,以此得到較高的性能。 CGI 程序反覆加載是 CGI 性能低下的主要緣由,若是 CGI 程序保持在內存中並接受 FastCGI 進程管理器調度, 則能夠提供良好的性能、伸縮性、Fail-Over 特性等。瀏覽器

FastCGI 工做流程以下:

  1. FastCGI 進程管理器自身初始化,啓動多個 CGI 解釋器進程,並等待來自 Web Server 的鏈接。
  2. Web 服務器與 FastCGI 進程管理器進行 Socket 通訊,經過 FastCGI 協議發送 CGI 環境變量和標準輸入數據給 CGI 解釋器進程。
  3. CGI 解釋器進程完成處理後將標準輸出和錯誤信息從同一鏈接返回 Web Server。
  4. CGI 解釋器進程接着等待並處理來自 Web Server 的下一個鏈接。

 

圖2.8 FastCGI 運行原理示舉例示意圖
圖2.8 FastCGI 運行原理示舉例示意圖

FastCGI 與傳統 CGI 模式的區別之一則是 Web 服務器不是直接執行 CGI 程序了,而是經過 Socket 與 FastCGI 響應器(FastCGI 進程管理器)進行交互,也正是因爲 FastCGI 進程管理器是基於 Socket 通訊的,因此也是分佈式的,Web 服務器能夠和 CGI 響應器服務器分開部署。Web 服務器須要將數據 CGI/1.1 的規範封裝在遵循 FastCGI 協議包中發送給 FastCGI 響應器程序。緩存

FastCGI 協議

可能上面的內容理解起來仍是很抽象,這是因爲第一對FastCGI協議尚未一個大概的認識,第二沒有實際代碼的學習。因此須要預先學習下 FastCGI 協議,不必定須要徹底看懂,可大體瞭解以後,看完本篇再結合着學習理解消化。服務器

下面結合 PHP 的 FastCGI 的代碼進行分析,不做特殊說明如下代碼均來自於 PHP 源碼。網絡

FastCGI 消息類型

FastCGI 將傳輸的消息作了不少類型的劃分,其結構體定義以下:架構

typedef enum _fcgi_request_type { FCGI_BEGIN_REQUEST = 1, /* [in] */ FCGI_ABORT_REQUEST = 2, /* [in] (not supported) */ FCGI_END_REQUEST = 3, /* [out] */ FCGI_PARAMS = 4, /* [in] environment variables */ FCGI_STDIN = 5, /* [in] post data */ FCGI_STDOUT = 6, /* [out] response */ FCGI_STDERR = 7, /* [out] errors */ FCGI_DATA = 8, /* [in] filter data (not supported) */ FCGI_GET_VALUES = 9, /* [in] */ FCGI_GET_VALUES_RESULT = 10 /* [out] */ } fcgi_request_type;

消息的發送順序

下圖是一個比較常見消息傳遞流程socket

 

圖2.9 FastCGI 消息傳遞流程示意圖
圖2.9 FastCGI 消息傳遞流程示意圖

 

最早發送的是FCGI_BEGIN_REQUEST,而後是FCGI_PARAMSFCGI_STDIN,因爲每一個消息頭(下面將詳細說明)裏面可以承載的最大長度是65535,因此這兩種類型的消息不必定只發送一次,有可能連續發送屢次。

FastCGI 響應體處理完畢以後,將發送FCGI_STDOUTFCGI_STDERR,同理也可能屢次連續發送。最後以FCGI_END_REQUEST表示請求的結束。 須要注意的一點,FCGI_BEGIN_REQUESTFCGI_END_REQUEST分別標識着請求的開始和結束,與整個協議息息相關,因此他們的消息體的內容也是協議的一部分,所以也會有相應的結構體與之對應(後面會詳細說明)。而環境變量、標準輸入、標準輸出、錯誤輸出,這些都是業務相關,與協議無關,因此他們的消息體的內容則無結構體對應。

因爲整個消息是二進制連續傳遞的,因此必須定義一個統一的結構的消息頭,這樣以便讀取每一個消息的消息體,方便消息的切割。這在網絡通信中是很是常見的一種手段。

FastCGI 消息頭

如上,FastCGI 消息分10種消息類型,有的是輸入有的是輸出。而全部的消息都以一個消息頭開始。其結構體定義以下:

typedef struct _fcgi_header { unsigned char version; unsigned char type; unsigned char requestIdB1; unsigned char requestIdB0; unsigned char contentLengthB1; unsigned char contentLengthB0; unsigned char paddingLength; unsigned char reserved; } fcgi_header;

字段解釋下:

version標識FastCGI協議版本。 type 標識FastCGI記錄類型,也就是記錄執行的通常職能。 requestId標識記錄所屬的FastCGI請求。 contentLength記錄的contentData組件的字節數。

關於上面的xxB1xxB0的協議說明:當兩個相鄰的結構組件除了後綴「B1」和「B0」以外命名相同時,它表示這兩個組件可視爲估值爲B1<<8 + B0的單個數字。該單個數字的名字是這些組件減去後綴的名字。這個約定概括了一個由超過兩個字節表示的數字的處理方式。

好比協議頭中requestIdcontentLength表示的最大值就是 65535。

#include <stdio.h>
#include <stdlib.h> #include <limits.h>   int main() { unsigned char requestIdB1 = UCHAR_MAX; unsigned char requestIdB0 = UCHAR_MAX; printf("%d\n", (requestIdB1 << 8) + requestIdB0); // 65535 }

你可能會想到若是一個消息體長度超過65535怎麼辦,則分割爲多個相同類型的消息發送便可。

PHP中的CGI實現

PHP的CGI實現了FastCGI協議,是一個TCP或UDP協議的服務器接受來自Web服務器的請求, 當啓動時建立TCP/UDP協議的服務器的socket監聽,並接收相關請求進行處理。隨後就進入了PHP的生命週期: 模塊初始化,sapi初始化,處理PHP請求,模塊關閉,sapi關閉等就構成了整個CGI的生命週期。

以TCP爲例,在TCP的服務端,通常會執行這樣幾個操做步驟:

  1. 調用socket函數建立一個TCP用的流式套接字;
  2. 調用bind函數將服務器的本地地址與前面建立的套接字綁定;
  3. 調用listen函數將新建立的套接字做爲監聽,等待客戶端發起的鏈接,當客戶端有多個鏈接鏈接到這個套接字時,可能須要排隊處理;
  4. 服務器進程調用accept函數進入阻塞狀態,直到有客戶進程調用connect函數而創建起一個鏈接;
  5. 當與客戶端建立鏈接後,服務器調用read_stream函數讀取客戶的請求;
  6. 處理完數據後,服務器調用write函數向客戶端發送應答。

TCP上客戶-服務器事務的時序如圖2.6所示:

 

圖2.6 TCP上客戶-服務器事務的時序
圖2.6 TCP上客戶-服務器事務的時序

 

PHP的CGI實現從cgi_main.c文件的main函數開始,在main函數中調用了定義在fastcgi.c文件中的初始化,監聽等函數。 對比TCP的流程,咱們查看PHP對TCP協議的實現,雖然PHP自己也實現了這些流程,可是在main函數中一些過程被封裝成一個函數實現。 對應TCP的操做流程,PHP首先會執行建立socket,綁定套接字,建立監聽:

if (bindpath) { fcgi_fd = fcgi_listen(bindpath, 128); // 實現socket監聽,調用fcgi_init初始化 ... }

在fastcgi.c文件中,fcgi_listen函數主要用於建立、綁定socket並開始監聽,它走完了前面所列TCP流程的前三個階段,

    if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 || ... bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 || listen(listen_socket, backlog) < 0) { ... }

當服務端初始化完成後,進程調用accept函數進入阻塞狀態,在main函數中咱們看到以下代碼:

    while (parent) { do { pid = fork(); // 生成新的子進程 switch (pid) { case 0: // 子進程 parent = 0;   /* don't catch our signals */ sigaction(SIGTERM, &old_term, 0); // 終止信號 sigaction(SIGQUIT, &old_quit, 0); // 終端退出符 sigaction(SIGINT, &old_int, 0); // 終端中斷符 break; ... default: /* Fine */ running++; break; } while (parent && (running < children));   ... while (!fastcgi || fcgi_accept_request(&request) >= 0) { SG(server_context) = (void *) &request; init_request_info(TSRMLS_C); CG(interactive) = 0; ... }

如上的代碼是一個生成子進程,並等待用戶請求。在fcgi_accept_request函數中,程序會調用accept函數阻塞新建立的進程。 當用戶的請求到達時,fcgi_accept_request函數會判斷是否處理用戶的請求,其中會過濾某些鏈接請求,忽略受限制客戶的請求, 若是程序受理用戶的請求,它將分析請求的信息,將相關的變量寫到對應的變量中。 其中在讀取請求內容時調用了safe_read方法。以下所示: [main() -> fcgi_accept_request() -> fcgi_read_request() -> safe_read()]

static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count) { size_t n = 0; do { ... // 省略 對win32的處理 ret = read(req->fd, ((char*)buf)+n, count-n); // 非win版本的讀操做 ... // 省略 } while (n != count);   }

如上對應服務器端讀取用戶的請求數據。

在請求初始化完成,讀取請求完畢後,就該處理請求的PHP文件了。 假設這次請求爲PHP_MODE_STANDARD則會調用php_execute_script執行PHP文件。 在此函數中它先初始化此文件相關的一些內容,而後再調用zend_execute_scripts函數,對PHP文件進行詞法分析和語法分析,生成中間代碼, 並執行zend_execute函數,從而執行這些中間代碼。關於整個腳本的執行請參見第三節 腳本的執行。

在處理完用戶的請求後,服務器端將返回信息給客戶端,此時在main函數中調用的是fcgi_finish_request(&request, 1); fcgi_finish_request函數定義在fastcgi.c文件中,其代碼以下:

int fcgi_finish_request(fcgi_request *req, int force_close) { int ret = 1;   if (req->fd >= 0) { if (!req->closed) { ret = fcgi_flush(req, 1); req->closed = 1; } fcgi_close(req, force_close, 1); } return ret; }

如上,當socket處於打開狀態,而且請求未關閉,則會將執行後的結果刷到客戶端,並將請求的關閉設置爲真。 將數據刷到客戶端的程序調用的是fcgi_flush函數。在此函數中,關鍵是在於答應頭的構造和寫操做。 程序的寫操做是調用的safe_write函數,而safe_write函數中對於最終的寫操做針對win和linux環境作了區分, 在Win32下,若是是TCP鏈接則用send函數,若是是非TCP則和非win環境同樣使用write函數。以下代碼:

#ifdef _WIN32
if (!req->tcp) { ret = write(req->fd, ((char*)buf)+n, count-n); } else { ret = send(req->fd, ((char*)buf)+n, count-n, 0); if (ret <= 0) { errno = WSAGetLastError(); } } #else ret = write(req->fd, ((char*)buf)+n, count-n); #endif

在發送了請求的應答後,服務器端將會執行關閉操做,僅限於CGI自己的關閉,程序執行的是fcgi_close函數。 fcgi_close函數在前面提的fcgi_finish_request函數中,在請求應答完後執行。一樣,對於win平臺和非win平臺有不一樣的處理。 其中對於非win平臺調用的是write函數。

以上是一個TCP服務器端實現的簡單說明。這只是咱們PHP的CGI模式的基礎,在這個基礎上PHP增長了更多的功能。 

php-fpm

FastCGI接口方式在腳本解析服務器上啓動一個或者多個守護進程對動態腳本進行解析,這些進程就是FastCGI進程管理器,或者稱之爲FastCGI引擎, spawn-fcgi與PHP-FPM就是支持PHP的兩個FastCGI進程管理器。

FPM(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-CGI的FastCGI進程

什麼是PHP-CGI

  PHP-CGI是PHP自帶的FastCGI管理器。

  啓動PHP-CGI,使用以下命令:

php-cgi -b 127.0.0.1:9000

  PHP-CGI的不足

  一、php-cgi變動php.ini配置後需重啓php-cgi才能讓新的php-ini生效,不能夠平滑重啓

  二、直接殺死php-cgi進程,php就不能運行了。(PHP-FPM和Spawn-FCGI就沒有這個問題,守護進程會平滑重新生成新的子進程。)

  什麼是PHP-FPM

  PHP-FPM是一個PHP FastCGI管理器,是隻用於PHP的,能夠在 http://php-fpm.org/download下載獲得.

  PHP-FPM實際上是PHP源代碼的一個補丁,旨在將FastCGI進程管理整合進PHP包中。必須將它patch到你的PHP源代碼中,在編譯安裝PHP後纔可使用。

  如今咱們能夠在最新的PHP 5.3.2的源碼樹裏下載獲得直接整合了PHP-FPM的分支,聽說下個版本會融合進PHP的主分支去。相對Spawn-FCGI,PHP-FPM在CPU和內存方面的控制都更勝一籌,並且前者很容易崩潰,必須用crontab進行監控,而PHP-FPM則沒有這種煩惱。

  PHP5.3.3已經集成php-fpm了,再也不是第三方的包了。PHP-FPM提供了更好的PHP進程管理方式,能夠有效控制內存和進程、能夠平滑重載PHP配置,比spawn-fcgi具備更多有點,因此被PHP官方收錄了。在./configure的時候帶 –enable-fpm參數便可開啓PHP-FPM。

  使用PHP-FPM來控制PHP-CGI的FastCGI進程

/usr/local/php/sbin/php-fpm{start|stop|quit|restart|reload|logrotate}

--start 啓動php的fastcgi進程
--stop 強制終止php的fastcgi進程
--quit 平滑終止php的fastcgi進程
--restart 重啓php的fastcgi進程
--reload 從新平滑加載php的php.ini
--logrotate 從新啓用log文件 
相關文章
相關標籤/搜索