PHP中的CGI實現

    PHP的CGI實現本質是是以socket編程實現一個TCP或UDP協議的服務器,當啓動時,創建TCP/UDP協議的服務器的socket監聽, 並接收相關請求進行處理。這只是請求的處理,在此基礎上添加模塊初始化,sapi初始化,模塊關閉,sapi關閉等就構成了整個CGI的生命週期。

以TCP爲例,在TCP的服務端,一般會執行這樣幾個操作步驟:

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

TCP上客戶-服務器事務的時序如圖2.6所示:
在這裏插入圖片描述
    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增加了更多的功能。 在前面的章節中我們提到了每個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。

SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);

    對於每一個服務器在加載時,我們都指定了sapi_module,在第一小節的Apache模塊方式中, sapi_module是apache2_sapi_module,其對應read_cookies方法的是php_apache_sapi_read_cookies函數, 而在我們這裏,讀取cookie的函數是sapi_cgi_read_cookies。 再次說明定義SAPI結構的理由:統一接口,面向接口的編程,具有更好的擴展性和適應性

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章