成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

【PHP7源碼分析】PHP中$_POST揭秘

sf190404 / 882人閱讀

摘要:和進程的啟動過程類似,啟動過程有種進程角色啟動進程進程和進程。直到請求到來,將連接賦值給對象的字段。注當進程執(zhí)行完后會再次調(diào)用函數(shù),準備監(jiān)聽新的請求。當讀取到的時,會調(diào)用函數(shù)對進行解析,將中的以及存儲到結(jié)構(gòu)體中。

運營研發(fā)團隊 季偉濱

一、前言

前幾天的工作中,需要通過curl做一次接口測試。讓我意外的是,通過$_POST竟然無法獲取到Content-Type是application/json的http請求的body參數(shù)。
查了下php官網(wǎng)(http://php.net/manual/zh/rese...)對$_POST的描述,的確是這樣。后來通過file_get_contents("php://input")獲取到了原始的http請求body,然后對參數(shù)進行json_decode解決了接口測試的問題。事后,腦子里面冒出了挺多問題:

php-fpm是怎么讀取并解析FastCGI協(xié)議的?http請求的header和body分別都存儲在哪里?

對于Content-Type是application/x-www-form-urlencoded的請求,為什么通過$_POST可以拿到解析后的參數(shù)數(shù)組?

對于Content-Type是application/json的請求,為什么通過$_POST拿不到解析后的參數(shù)數(shù)組?

基于這幾個問題,對php代碼進行了一次新的學(xué)習(xí), 有一定的收獲,在這里記錄一下。
最后,編寫了一個叫postjson的php擴展,它在源代碼層面實現(xiàn)了feature:對于Content-Type是application/json的請求,可以通過$_POST拿到請求參數(shù)。

二、fpm整體流程

在分析之前,有必要對php-fpm整體流程有所了解。包括你可能想知道的fpm進程啟動過程、ini配置文件何時讀取,擴展在哪里被加載,請求數(shù)據(jù)在哪里被讀取等等,這里都會稍微提及一下,這樣看后面的時候,我們會比較清楚,某一個函數(shù)調(diào)用發(fā)生在整個流程的哪一個環(huán)節(jié),做到可識廬山真面目,哪怕身在此山中。

和Nginx進程的啟動過程類似,fpm啟動過程有3種進程角色:啟動shell進程、fpm master進程和fpm worker進程。上圖列出了各個進程在生命周期中執(zhí)行的主要函數(shù),其中標有顏色的表示和上面的問題答案有關(guān)聯(lián)的函數(shù)。下面概況的說明一下:

啟動shell進程

1.sapi_startup:SAPI啟動。將傳入的cgi_sapi_module的地址賦值給全局變量sapi_module,初始化全局變量SG,最后執(zhí)行php_setup_sapi_content_types函數(shù)?!具@個函數(shù)后面會詳細說明】

2.php_module_startup :模塊初始化。php.ini文件的解析,php動態(tài)擴展.so的加載、php擴展、zend擴展的啟動都是在這里完成的。

zend_startup:啟動zend引擎,設(shè)置編譯器、執(zhí)行器的函數(shù)指針,初始化相關(guān)HashTable結(jié)構(gòu)的符號表CG(function_table)、CG(class_table)以及CG(auto_globals),注冊Zend核心擴展zend_builtin_module(該過程會注冊Zend引擎提供的函數(shù):func_get_args、strlen、class_exists等),注冊標準常量如E_ALL、TRUE、FALSE等。

php_init_config:讀取php.ini配置文件并解析,將解析的key-value對存儲到configuration_hash這個hashtable中,并且將所有的php擴展(extension=xx.so)的擴展名稱保存到extension_lists.functions結(jié)構(gòu)中,將所有的zend擴展(zend_extension=xx.so)的擴展名稱保存到extension_lists.engine結(jié)構(gòu)中。

php_startup_auto_globals:向CG(auth_globals)中注冊_GET、_POST、_COOKIE、_SERVER等超全局變量鉤子,在后面合適的時機(實際上是php_hash_environment)會回調(diào)相應(yīng)的handler。

php_startup_sapi_content_types:設(shè)置sapi_module的default_post_reader和treat_data?!具@2個函數(shù)后面會詳細說明】

php_ini_register_extensions:遍歷extension_lists.functions,使用dlopen函數(shù)打開xx.so擴展文件,將所有的php擴展注冊到全局變量module_registry中,同時如果php擴展有實現(xiàn)函數(shù)的話,將實現(xiàn)的函數(shù)注冊到CG(function_table)。遍歷extension_lists.engine,使用dlopen函數(shù)打開xx.so擴展文件,將所有的zend擴展注冊到全局變量zend_extensions中。

zend_startup_modules:遍歷module_registry,調(diào)用所有php擴展的MINIT函數(shù)。

zend_startup_extensions:遍歷zend_extensions,調(diào)用所有zend擴展的startup函數(shù)。

3.fpm_init:fpm進程相關(guān)初始化。這個函數(shù)也比較重要。解析php-fpm.conf、fork master進程、安裝信號處理器、打開監(jiān)聽socket(默認9000端口)都是在這里完成的。啟動shell進程在fork之后不久就退出了。而master進程則通過setsid調(diào)用脫離了原來啟動shell的終端所在會話,成為了daemon進程。限于篇幅,這里不再展開。

master進程

fpm_run:根據(jù)php-fpm.conf的配置fork worker進程(一個監(jiān)聽端口對應(yīng)一個worker pool即進程池,worker進程從屬于worker pool,只處理該監(jiān)聽端口的請求)。然后進入fpm_event_loop函數(shù),無限等待事件的到來。

fpm_event_loop:事件循環(huán)。一直等待著信號事件或者定時器事件的發(fā)生。區(qū)別于Nginx的master進程使用suspend系統(tǒng)調(diào)用掛起進程,fpm master通過循環(huán)的調(diào)用epoll_wait(timeout為1s)來等待事件。

worker進程

fpm_init_request:初始化request對象。設(shè)置request的listen_socket為從父進程復(fù)制過來的相應(yīng)worker pool對應(yīng)的監(jiān)聽socket。

fcgi_accept_request:監(jiān)聽請求連接,讀取請求的頭信息。

1.accept系統(tǒng)調(diào)用:如果沒有請求到來,worker進程會阻塞在這里。直到請求到來,將連接fd賦值給request對象的fd字段。

2.select/poll系統(tǒng)調(diào)用:循環(huán)的調(diào)用select或者poll(timeout為5s),等待著連接fd上有可讀事件。如果連接fd一直不可讀,worker進程將一直在這里阻塞著。

3.fcgi_read_request:一旦連接fd上有可讀事件之后,會調(diào)用該函數(shù)對FastCGI協(xié)議進行解析,解析出http請求header以及fastcgi_param變量存儲到request的env字段中。

php_request_startup:請求初始化

1.zend_activate:重置垃圾回收器,初始化編譯器、執(zhí)行器、詞法掃描器。

2.sapi_activate:激活SAPI,讀取http請求body數(shù)據(jù)。

3.php_hash_environment:回調(diào)在php_startup_auto_globals函數(shù)中注冊的_GET,_POST,_COOKIE等超全局變量的鉤子,完成超全局變量的生成。

4.zend_activate_modules:調(diào)用所有php擴展的RINIT函數(shù)。

php_execute_script:使用Zend VM對php腳本文件進行編譯(詞法分析+語法分析)生成虛擬機可識別的opcodes,然后執(zhí)行這些指令。這塊很復(fù)雜,也是php語言的精華所在,限于篇幅這里不展開。

php_request_shutdown:請求關(guān)閉。調(diào)用注冊的register_shutdown_function回調(diào),調(diào)用__destruct析構(gòu)函數(shù),調(diào)用所有php擴展的RSHUTDOWN函數(shù),flush輸出內(nèi)容,發(fā)送http響應(yīng)header,清理全局變量,關(guān)閉編譯器、執(zhí)行器,關(guān)閉連接fd等。

        注:當worker進程執(zhí)行完php_request_shutdown后會再次調(diào)用fcgi_accept_request函數(shù),準備監(jiān)聽新的請求。這里可以看到一個worker進程只能順序的處理請求,在處理當前請求的過程中,該worker進程不會接受新的請求連接,這和Nginx worker進程的事件處理機制是不一樣的。
三、FastCGI協(xié)議的處理

言歸正傳,讓我們回到本文的主題,一步步接開$_POST的面紗。

大家都知道$_POST存儲的是對http請求body數(shù)據(jù)解析后的數(shù)組,但php-fpm并不是一個web server,它并不支持http協(xié)議,一般它通過FastCGI協(xié)議來和web server如Apache、Nginx進行數(shù)據(jù)通信。關(guān)于這個協(xié)議,已經(jīng)有其他同學(xué)寫的好幾篇很棒的文章來講述,如果對FastCGI不了解的,可以先讀一下這些文章。

一個FastCGI請求由三部分的數(shù)據(jù)包組成:FCGI_BEGIN_REQUEST數(shù)據(jù)包、FCGI_PARAMS數(shù)據(jù)包、FCGI_STDIN數(shù)據(jù)包。

FCGI_BEGIN_REQUEST表示請求的開始,它包括:

header

data:數(shù)據(jù)部分,承載著web server期望fpm扮演的角色role字段

FCGI_PARAMS主要用來傳輸http請求的header以及fastcgi_param變量數(shù)據(jù),它包括:

首header:表示FCGI_PARAMS的開始

data:承載著http請求header和fastcgi_params信息的key-value對組成的字符串

padding:填充字段

尾header:表示FCGI_PARAMS的結(jié)束

FCGI_STDIN用來傳輸http請求的body數(shù)據(jù),它包括:

首header:表示FCGI_STDIN的開始

data:承載著原始的http請求body數(shù)據(jù)

padding:填充字段

尾header:表示FCGI_STDIN的結(jié)束

php對FastCGI協(xié)議本身的處理上,可以分為了3個階段:頭信息讀取、body信息讀取、數(shù)據(jù)后置處理。下面一一介紹各個階段都做了些什么。

頭信息讀取

頭信息讀取階段只讀取FCGI_BEGIN_REQUEST和FCGI_PARAMS數(shù)據(jù)包。因此在這個階段只能拿到http請求的header以及fastcgi_param變量。在main/fastcgi.c中fcgi_read_request負責(zé)完成這個階段的讀取工作。從第二節(jié)可以看到,它在worker進程發(fā)現(xiàn)請求連接fd可讀之后被調(diào)用。

static int fcgi_read_request(fcgi_request *req)
{
    fcgi_header hdr;
    int len, padding;
    unsigned char buf[FCGI_MAX_LENGTH+8];
    ...

    //讀取到了FCGI_BEGIN_REQUEST的header
    if (hdr.type == FCGI_BEGIN_REQUEST && len == sizeof(fcgi_begin_request)) { 
        
        //讀取FCGI_BEGIN_REQUEST的data,存儲到buf里
        if (safe_read(req, buf, len+padding) != len+padding) { 
            return 0;
        }

        ...
        //分析buf里FCGI_BEGIN_REQUEST的data中FCGI_ROLE,一般是RESPONDER
        switch ((((fcgi_begin_request*)buf)->roleB1 << 8) + ((fcgi_begin_request*)buf)->roleB0) { 
            case FCGI_RESPONDER:
                fcgi_hash_set(&req->env, FCGI_HASH_FUNC("FCGI_ROLE", sizeof("FCGI_ROLE")-1), "FCGI_ROLE", sizeof("FCGI_ROLE")-1, "RESPONDER", sizeof("RESPONDER")-1);
                break;
            case FCGI_AUTHORIZER:
                fcgi_hash_set(&req->env, FCGI_HASH_FUNC("FCGI_ROLE", sizeof("FCGI_ROLE")-1), "FCGI_ROLE", sizeof("FCGI_ROLE")-1, "AUTHORIZER", sizeof("AUTHORIZER")-1);
                break;
            case FCGI_FILTER:
                fcgi_hash_set(&req->env, FCGI_HASH_FUNC("FCGI_ROLE", sizeof("FCGI_ROLE")-1), "FCGI_ROLE", sizeof("FCGI_ROLE")-1, "FILTER", sizeof("FILTER")-1);
                break;
            default:
                return 0;
        }

        //繼續(xù)讀下一個header
        if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) ||
            hdr.version < FCGI_VERSION_1) {
            return 0;
        }

        len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
        padding = hdr.paddingLength;

        while (hdr.type == FCGI_PARAMS && len > 0) {
            //讀取到了FCGI_PARAMS的首header(header中l(wèi)en大于0,表示FCGI_PARAMS數(shù)據(jù)包的開始)
            if (len + padding > FCGI_MAX_LENGTH) {
                return 0;
            }

            //讀取FCGI_PARAMS的data
            if (safe_read(req, buf, len+padding) != len+padding) {
                req->keep = 0;
                return 0;
            }

            //解析FCGI_PARAMS的data,將key-value對存儲到req.env中
            if (!fcgi_get_params(req, buf, buf+len)) {
                req->keep = 0;
                return 0;
            }

            //繼續(xù)讀取下一個header,下一個header有可能仍然是FCGI_PARAMS的首header,也有可能是FCGI_PARAMS的尾header
            if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) ||
                hdr.version < FCGI_VERSION_1) {
                req->keep = 0;
                return 0;
            }
            len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
            padding = hdr.paddingLength;
        }
    }
    ...
    return 1;
}

上面的代碼可以和FastCGI協(xié)議對照著去看,這會加深我們對FastCGI協(xié)議的理解。

總的來講,對于FastCGI協(xié)議,總是需要先讀取header,根據(jù)header中帶的類型以及長度繼續(xù)做不同的處理。

當讀取到FCGI_PARAMS的data時,會調(diào)用fcgi_get_params函數(shù)對data進行解析,將data中的http header以及fastcgi_params存儲到req.env結(jié)構(gòu)體中。FCGI_PARAMS的data格式是什么樣的呢?它是由一個個的key-value對組成的字符串,對于key-value對,通過keyLength+valueLength+key+value的形式來描述,因此FCGI_PARAMS的data的格式一般是這樣:

這里有一個細節(jié)需要注意,為了節(jié)省空間,在Length字段長度制定上,采取了長短2種表示法。如果key或者value的Length不超過127,那么相應(yīng)的Length字段用一個char來表示。最高位是0,如果相應(yīng)的Length字段大于127,那么相應(yīng)的Length字段用4個char來表示,第一個char的最高位是1。大部分http中的header以及fastcgi_params變量的key-value的長度其實都是不超過127的。

舉個栗子,在我的vm環(huán)境下,執(zhí)行如下curl命令:curl -H "Content-Type: application/json" -d "{"a":1}" http://10.179.195.72:8585/test/jiweibin,下面是我gdb時FCGI_PARAMS的data的結(jié)果: