CGI全稱是「通用網關接口」(Common Gateway Interface), 它可讓一個客戶端,從網頁瀏覽器向執行在Web服務器上的程序請求數據。 CGI描述了客戶端和這個程序之間傳輸數據的一種標準。 CGI的一個目的是要獨立於任何語言的,因此CGI能夠用任何一種語言編寫,只要這種語言具備標準輸入、輸出和環境變量。 如php,perl,tcl等。php
FastCGI是Web服務器和處理程序之間通訊的一種協議, 是CGI的一種改進方案,FastCGI像是一個常駐(long-live)型的CGI, 它能夠一直執行,在請求到達時不會花費時間去fork一個進程來處理(這是CGI最爲人詬病的fork-and-execute模式)。 正是由於他只是一個通訊協議,它還支持分佈式的運算,即 FastCGI 程序能夠在網站服務器之外的主機上執行而且接受來自其它網站服務器來的請求。linux
FastCGI是語言無關的、可伸縮架構的CGI開放擴展,將CGI解釋器進程保持在內存中,以此得到較高的性能。 CGI程序反覆加載是CGI性能低下的主要緣由,若是CGI程序保持在內存中並接受FastCGI進程管理器調度, 則能夠提供良好的性能、伸縮性、Fail-Over特性等。apache
通常狀況下,FastCGI的整個工做流程是這樣的:api
Web Server啓動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)瀏覽器
FastCGI進程管理器自身初始化,啓動多個CGI解釋器進程(可見多個php-cgi)並等待來自Web Server的鏈接。緩存
當客戶端請求到達Web Server時,FastCGI進程管理器選擇並鏈接到一個CGI解釋器。 Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。服務器
FastCGI子進程完成處理後將標準輸出和錯誤信息從同一鏈接返回Web Server。當FastCGI子進程關閉鏈接時, 請求便告處理完成。FastCGI子進程接着等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個鏈接。 在CGI模式中,php-cgi在此便退出了。cookie
PHP的CGI實現了Fastcgi協議,是一個TCP或UDP協議的服務器接受來自Web服務器的請求, 當啓動時建立TCP/UDP協議的服務器的socket監聽,並接收相關請求進行處理。隨後就進入了PHP的生命週期: 模塊初始化,sapi初始化,處理PHP請求,模塊關閉,sapi關閉等就構成了整個CGI的生命週期。架構
以TCP爲例,在TCP的服務端,通常會執行這樣幾個操做步驟:app
調用socket函數建立一個TCP用的流式套接字;
調用bind函數將服務器的本地地址與前面建立的套接字綁定;
調用listen函數將新建立的套接字做爲監聽,等待客戶端發起的鏈接,當客戶端有多個鏈接鏈接到這個套接字時,可能須要排隊處理;
服務器進程調用accept函數進入阻塞狀態,直到有客戶進程調用connect函數而創建起一個鏈接;
當與客戶端建立鏈接後,服務器調用read_stream函數讀取客戶的請求;
處理完數據後,服務器調用write函數向客戶端發送應答。
TCP上客戶-服務器事務的時序如圖2.6所示:
圖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 _WIN32if (!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(); }}#elseret = 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增長了更多的功能。 在前面的章節中咱們提到了每一個SAPI都有一個專屬於它們本身的sapi_module_struct結構:cgi_sapi_module,其代碼定義以下:
/* {{{ sapi_module_struct cgi_sapi_module */static sapi_module_struct cgi_sapi_module = {"cgi-fcgi", /* name */"CGI/FastCGI", /* pretty name */ php_cgi_startup, /* startup */php_module_shutdown_wrapper, /* shutdown */ sapi_cgi_activate, /* activate */sapi_cgi_deactivate, /* deactivate */ sapi_cgibin_ub_write, /* unbuffered write */sapi_cgibin_flush, /* flush */NULL, /* get uid */sapi_cgibin_getenv, /* getenv */ php_error, /* error handler */ NULL, /* header handler */sapi_cgi_send_headers, /* send headers handler */NULL, /* send header handler */ sapi_cgi_read_post, /* read POST data */sapi_cgi_read_cookies, /* read Cookies */ sapi_cgi_register_variables, /* register server variables */sapi_cgi_log_message, /* Log message */NULL, /* Get request time */NULL, /* Child terminate */ STANDARD_SAPI_MODULE_PROPERTIES};/* }}} */
一樣,以讀取cookie爲例,當咱們在CGI環境下,在PHP中調用讀取Cookie時, 最終獲取的數據的位置是在激活SAPI時。它所調用的方法是read_cookies。 由SAPI實現來實現獲取cookie,這樣各個不一樣的SAPI就能根據本身的須要來實現一些依賴環境的方法。
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);
全部使用PHP的場合都須要定義本身的SAPI,例如在第一小節的Apache模塊方式中, sapi_module是apache2_sapi_module,其對應read_cookies方法的是php_apache_sapi_read_cookies函數, 而在咱們這裏,讀取cookie的函數是sapi_cgi_read_cookies。 從sapi_module結構能夠看出flush對應的是sapi_cli_flush,在win或非win下,flush對應的操做不一樣, 在win下,若是輸出緩存失敗,則會和嵌入式的處理同樣,調用php_handle_aborted_connection進入中斷處理程序, 而其它狀況則是沒有任何處理程序。這個區別經過cli_win.c中的PHP_CLI_WIN32_NO_CONSOLE控制。