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

運營研發團隊 季偉濱php

1、前言

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

  • php-fpm是怎麼讀取並解析FastCGI協議的?http請求的header和body分別都存儲在哪裏?
  • 對於Content-Type是application/x-www-form-urlencoded的請求,爲何經過$_POST能夠拿到解析後的參數數組?
  • 對於Content-Type是application/json的請求,爲何經過$_POST拿不到解析後的參數數組?web

基於這幾個問題,對php代碼進行了一次新的學習, 有必定的收穫,在這裏記錄一下。
最後,編寫了一個叫postjson的php擴展,它在源代碼層面實現了feature:對於Content-Type是application/json的請求,能夠經過$_POST拿到請求參數。shell

2、fpm總體流程

在分析以前,有必要對php-fpm總體流程有所瞭解。包括你可能想知道的fpm進程啓動過程、ini配置文件什麼時候讀取,擴展在哪裏被加載,請求數據在哪裏被讀取等等,這裏都會稍微說起一下,這樣看後面的時候,咱們會比較清楚,某一個函數調用發生在整個流程的哪個環節,作到可識廬山真面目,哪怕身在此山中。json

clipboard.png

和Nginx進程的啓動過程相似,fpm啓動過程有3種進程角色:啓動shell進程、fpm master進程和fpm worker進程。上圖列出了各個進程在生命週期中執行的主要函數,其中標有顏色的表示和上面的問題答案有關聯的函數。下面概況的說明一下:api

啓動shell進程

  • 1.sapi_startup:SAPI啓動。將傳入的cgi_sapi_module的地址賦值給全局變量sapi_module,初始化全局變量SG,最後執行php_setup_sapi_content_types函數。【這個函數後面會詳細說明】
  • 2.php_module_startup :模塊初始化。php.ini文件的解析,php動態擴展.so的加載、php擴展、zend擴展的啓動都是在這裏完成的。數組

    • zend_startup:啓動zend引擎,設置編譯器、執行器的函數指針,初始化相關HashTable結構的符號表CG(function_table)、CG(class_table)以及CG(auto_globals),註冊Zend核心擴展zend_builtin_module(該過程會註冊Zend引擎提供的函數: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結構中,將全部的zend擴展(zend_extension=xx.so)的擴展名稱保存到extension_lists.engine結構中。
    • php_startup_auto_globals:向CG(auth_globals)中註冊_GET、_POST、_COOKIE、_SERVER等超全局變量鉤子,在後面合適的時機(其實是php_hash_environment)會回調相應的handler。
    • php_startup_sapi_content_types:設置sapi_module的default_post_reader和treat_data。【這2個函數後面會詳細說明】
    • php_ini_register_extensions:遍歷extension_lists.functions,使用dlopen函數打開xx.so擴展文件,將全部的php擴展註冊到全局變量module_registry中,同時若是php擴展有實現函數的話,將實現的函數註冊到CG(function_table)。遍歷extension_lists.engine,使用dlopen函數打開xx.so擴展文件,將全部的zend擴展註冊到全局變量zend_extensions中。
    • zend_startup_modules:遍歷module_registry,調用全部php擴展的MINIT函數。
    • zend_startup_extensions:遍歷zend_extensions,調用全部zend擴展的startup函數。
  • 3.fpm_init:fpm進程相關初始化。這個函數也比較重要。解析php-fpm.conf、fork master進程、安裝信號處理器、打開監聽socket(默認9000端口)都是在這裏完成的。啓動shell進程在fork以後不久就退出了。而master進程則經過setsid調用脫離了原來啓動shell的終端所在會話,成爲了daemon進程。限於篇幅,這裏再也不展開。

master進程

  • fpm_run:根據php-fpm.conf的配置fork worker進程(一個監聽端口對應一個worker pool即進程池,worker進程從屬於worker pool,只處理該監聽端口的請求)。而後進入fpm_event_loop函數,無限等待事件的到來。
  • fpm_event_loop:事件循環。一直等待着信號事件或者定時器事件的發生。區別於Nginx的master進程使用suspend系統調用掛起進程,fpm master經過循環的調用epoll_wait(timeout爲1s)來等待事件。

worker進程

  • fpm_init_request:初始化request對象。設置request的listen_socket爲從父進程複製過來的相應worker pool對應的監聽socket。
  • fcgi_accept_request:監聽請求鏈接,讀取請求的頭信息。cookie

    • 1.accept系統調用:若是沒有請求到來,worker進程會阻塞在這裏。直到請求到來,將鏈接fd賦值給request對象的fd字段。
    • 2.select/poll系統調用:循環的調用select或者poll(timeout爲5s),等待着鏈接fd上有可讀事件。若是鏈接fd一直不可讀,worker進程將一直在這裏阻塞着。
    • 3.fcgi_read_request:一旦鏈接fd上有可讀事件以後,會調用該函數對FastCGI協議進行解析,解析出http請求header以及fastcgi_param變量存儲到request的env字段中。
  • php_request_startup:請求初始化php7

    • 1.zend_activate:重置垃圾回收器,初始化編譯器、執行器、詞法掃描器。
    • 2.sapi_activate:激活SAPI,讀取http請求body數據。
    • 3.php_hash_environment:回調在php_startup_auto_globals函數中註冊的_GET,_POST,_COOKIE等超全局變量的鉤子,完成超全局變量的生成。
    • 4.zend_activate_modules:調用全部php擴展的RINIT函數。
  • php_execute_script:使用Zend VM對php腳本文件進行編譯(詞法分析+語法分析)生成虛擬機可識別的opcodes,而後執行這些指令。這塊很複雜,也是php語言的精華所在,限於篇幅這裏不展開。
  • php_request_shutdown:請求關閉。調用註冊的register_shutdown_function回調,調用__destruct析構函數,調用全部php擴展的RSHUTDOWN函數,flush輸出內容,發送http響應header,清理全局變量,關閉編譯器、執行器,關閉鏈接fd等。
注:當worker進程執行完php_request_shutdown後會再次調用fcgi_accept_request函數,準備監聽新的請求。這裏能夠看到一個worker進程只能順序的處理請求,在處理當前請求的過程當中,該worker進程不會接受新的請求鏈接,這和Nginx worker進程的事件處理機制是不同的。

3、FastCGI協議的處理

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

你們都知道$_POST存儲的是對http請求body數據解析後的數組,但php-fpm並非一個web server,它並不支持http協議,通常它經過FastCGI協議來和web server如Apache、Nginx進行數據通訊。關於這個協議,已經有其餘同窗寫的好幾篇很棒的文章來說述,若是對FastCGI不瞭解的,能夠先讀一下這些文章。

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

clipboard.png

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

    • header
    • data:數據部分,承載着web server指望fpm扮演的角色role字段
  • FCGI_PARAMS主要用來傳輸http請求的header以及fastcgi_param變量數據,它包括:

    • 首header:表示FCGI_PARAMS的開始
    • data:承載着http請求header和fastcgi_params信息的key-value對組成的字符串
    • padding:填充字段
    • 尾header:表示FCGI_PARAMS的結束
  • FCGI_STDIN用來傳輸http請求的body數據,它包括:

    • 首header:表示FCGI_STDIN的開始
    • data:承載着原始的http請求body數據
    • padding:填充字段
    • 尾header:表示FCGI_STDIN的結束

php對FastCGI協議自己的處理上,能夠分爲了3個階段:頭信息讀取、body信息讀取、數據後置處理。下面一一介紹各個階段都作了些什麼。

clipboard.png

頭信息讀取

頭信息讀取階段只讀取FCGI_BEGIN_REQUEST和FCGI_PARAMS數據包。所以在這個階段只能拿到http請求的header以及fastcgi_param變量。在main/fastcgi.c中fcgi_read_request負責完成這個階段的讀取工做。從第二節能夠看到,它在worker進程發現請求鏈接fd可讀以後被調用。

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;
        }

        //繼續讀下一個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中len大於0,表示FCGI_PARAMS數據包的開始)
            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;
            }

            //繼續讀取下一個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協議對照着去看,這會加深咱們對FastCGI協議的理解。

總的來說,對於FastCGI協議,老是須要先讀取header,根據header中帶的類型以及長度繼續作不一樣的處理。

當讀取到FCGI_PARAMS的data時,會調用fcgi_get_params函數對data進行解析,將data中的http header以及fastcgi_params存儲到req.env結構體中。FCGI_PARAMS的data格式是什麼樣的呢?它是由一個個的key-value對組成的字符串,對於key-value對,經過keyLength+valueLength+key+value的形式來描述,所以FCGI_PARAMS的data的格式通常是這樣:

clipboard.png

這裏有一個細節須要注意,爲了節省空間,在Length字段長度制定上,採起了長短2種表示法。若是key或者value的Length不超過127,那麼相應的Length字段用一個char來表示。最高位是0,若是相應的Length字段大於127,那麼相應的Length字段用4個char來表示,第一個char的最高位是1。大部分http中的header以及fastcgi_params變量的key-value的長度其實都是不超過127的。

舉個栗子,在個人vm環境下,執行以下curl命令:curl -H "Content-Type: application/json" -d '{"a":1}' http://10.179.195.72:8585/test/jiweibin,下面是我gdb時FCGI_PARAMS的data的結果:

\017?SCRIPT_FILENAME/home/weibin/codedir/mis_deploy/mis/src/index.php/test/jiweibin\f\000QUERY_STRING\016\004REQUEST_METHODPOST\f\020CONTENT_TYPEapplication/json\016\001CO
NTENT_LENGTH7\v SCRIPT_NAME/mis/src/index.php/test/ji...

能夠看到第一個key-value對是"017?SCRIPT_FILENAME/home/weibin/codedir/mis_deploy/mis/src/index.php/test/jiweibin",keyLength是'017',它是8進制,轉成十進制是15,valueLength是字符'?',字符'?'對應的數值是63,也就是valueLength是63,所以按keyLength日後讀取15個長度的字符,取到了key是:"SCRIPT_FILENAME",繼續前移讀取63個字符,取到value是:"/home/weibin/codedir/mis_deploy/mis/src/index.php/test/jiweibin"。以此類推,咱們解析出一個個key-value對,能夠看到CONTENT_TYPE=application/json也在其中。

在fcgi_get_params裏面解析了某一個key-value對以後,會調用fcgi_hash_set函數將key-value存儲到req.env結構體中。req.env結構體的類型是fcgi_hash:

typedef struct _fcgi_hash {
    fcgi_hash_bucket  *hash_table[FCGI_HASH_TABLE_SIZE]; //hashtable,共128個slot,每個slot存儲着指向bucket的指針
    fcgi_hash_bucket  *list; //按插入順序的逆序排列的bucket鏈表頭指針
    fcgi_hash_buckets *buckets; //存儲bucket的物理內存
    fcgi_data_seg     *data; //存儲字符串的堆內存首地址
}   ;

這個hashtable的實現採用了廣泛採用的鏈地址法思路,不過bucket的內存分配(malloc)並非每次都須要進行的,而是在hash初始化的時候,一次性預分配一個大小爲128的連續的數組。上面的buckets指針指向這段內存。同時hashtable還維護了一個按照元素插入順序逆序排列的全局單鏈表,list指向了這個鏈表的頭元素。每個bucket元素包括對key進行hash以後的hash_value、key的length、key的字符指針、value的length、value的字符指針、相同slot中下個bucket元素指針,全局單鏈表的下一個bucket元素指針。bucket中key和value並不直接存儲字符數組(由於長度未知),而只是存儲字符指針,真正的字符數組存儲在hashtable的data指向的內存中。

下圖展現了當我解析出FCGI_ROLE(經過解析FCGI_BEGIN_REQUEST)以及第一個key-value對(SCRIPT_FILENAME="/home/weibin...")時,內存的示意圖:

clipboard.png

body信息讀取

該階段負責處理FCGI_STDIN數據包,這個數據包承載着原始http post請求的body數據。

也許你會想,爲何在頭信息讀取的時候,不一樣時將body數據讀取出來呢?答案是爲了適配多種Content-Type不一樣的行爲。

感興趣的同窗能夠作下實驗,針對Content-Type爲multipart/form-data類型的請求,從$_POST能夠拿到body數據,但卻不能經過php://input獲取到原始的body數據流。而對於Content-Type爲x-www-form-urlencoded的請求,這2者是均可以獲取到的。

下表總結了3種不一樣的Content-Type的行爲差別,本節咱們說明php://input的行爲差別緣由之所在,而$_POST的差別則要在下一節進行講解。

clipboard.png

在body信息讀取階段,對不一樣的Content-Type差別化處理的關鍵節點發生在sapi_read_post_data函數,見下圖,展現了差別化處理的總體流程:

clipboard.png

下面咱們基於上圖,結合着代碼進行詳細分析。(代碼可能會稍微多一點,這塊代碼比較核心,不是很好經過圖的方式去畫)

fpm在接收到請求鏈接而且讀取並解析完頭信息以後,會調用php_request_startup執行請求初始化。它又調用sapi_activate函數,該函數會判斷若是當前請求是POST請求,那麼會調用sapi_read_post_data函數對body數據進行讀取。

SAPI_API void sapi_activate(void)
{
    ...
    /* Handle request method */
    if (SG(server_context)) {
        if (PG(enable_post_data_reading)
        &&  SG(request_info).content_type
        &&  SG(request_info).request_method
        && !strcmp(SG(request_info).request_method, "POST")) {
            /* HTTP POST may contain form data to be processed into variables
             * depending on given content type */
            sapi_read_post_data();   //根據不一樣的Content-Type進行post數據的讀取
        } else {
            SG(request_info).content_type_dup = NULL;
        }

       ...
    }
    ...
}

而在sapi_read_post_data中,會首先從SG(known_post_content_types)這個hashtable中查詢是否有對應的鉤子,若是有則調用,若是沒有,則使用默認的處理方式。

static void sapi_read_post_data(void)
{
    ...

    /* now try to find an appropriate POST content handler */
    if ((post_entry = zend_hash_str_find_ptr(&SG(known_post_content_types), content_type,
            content_type_length)) != NULL) { //content_type已註冊鉤子
        /* found one, register it for use */
        SG(request_info).post_entry = post_entry; //將鉤子保存到SG
        post_reader_func = post_entry->post_reader; //鉤子中的post_reader函數指針賦值給post_reader_func
    } else {
        /* fallback */
        SG(request_info).post_entry = NULL;
        if (!sapi_module.default_post_reader) {
            /* no default reader ? */
            SG(request_info).content_type_dup = NULL;
            sapi_module.sapi_error(E_WARNING, "Unsupported content type:  '%s'", content_type);
            return;
        }
    }
    ...

    if(post_reader_func) { //若是post_reader_func不爲空,執行post_reader_func
        post_reader_func();
    }

    //不然,執行默認的處理邏輯(之因此post_reader_func和sapi_module.default_post_reader互斥,關鍵的邏輯在sapi_module.default_post_reader裏面實現)
    if(sapi_module.default_post_reader) { 
        sapi_module.default_post_reader();
    }
}

SG(known_post_content_types)中爲哪些Content-Type安裝了鉤子呢?答案是隻有2種:application/x-www-form-urlencoded和multipart/form-data。在第二節曾經提到,在SAPI啓動階段,會執行一個神祕函數php_setup_sapi_content_types,它會遍歷php_post_entries數組,將上面2個Content-Type對應的鉤子註冊到SG的known_post_content_types這個hashtable中。

#define DEFAULT_POST_CONTENT_TYPE "application/x-www-form-urlencoded" 
#define MULTIPART_CONTENT_TYPE "multipart/form-data"
 
int php_setup_sapi_content_types(void) 
{
    sapi_register_post_entries(php_post_entries);  //安裝內置的Content_Type處理鉤子

    return SUCCESS;
}
 
static sapi_post_entry php_post_entries[] = { 
    { DEFAULT_POST_CONTENT_TYPE, sizeof(DEFAULT_POST_CONTENT_TYPE)-1, sapi_read_standard_form_data, php_std_post_handler },
    { MULTIPART_CONTENT_TYPE,    sizeof(MULTIPART_CONTENT_TYPE)-1,    NULL,                         rfc1867_post_handler },
    { NULL, 0, NULL, NULL }
};
 
struct _sapi_post_entry {
    char *content_type;
    uint content_type_len;
    void (*post_reader)(void); //post數據讀取函數指針
    void (*post_handler)(char *content_type_dup, void *arg); //post數據後置處理函數指針,見下一小節
};
 
typedef struct _sapi_post_entry sapi_post_entry;

鉤子包含了2個函數指針,post_reader在本階段會被調用,而post_handler會在數據後置處理階段被調用。從上面代碼能夠看到,php爲application/x-www-form-urlencoded安裝的鉤子的post_reader函數指針指向sapi_read_standard_form_data,而multipart/form-data雖然鉤子已安裝,可是post_reader函數指針爲NULL,因此在本階段不進行任何處理。

讓咱們繼續跟蹤sapi_read_standard_form_data都幹了些什麼,它的總體流程能夠參考下圖:

clipboard.png

首先,它會建立一個phpstream,並將SG(request_info).request_body指向這個phpstream(phpstream是php對io的封裝,比較複雜,這裏不展開)。而後調用sapi_read_post_block函數讀取htt ppost請求的body數據,內部它會調用sapi_cgi_read_post函數,這個函數會判斷頭信息裏是否存在REQUEST_BODY_FILE字段(REQUEST_BODY_FILE用來在nginx和fpm傳遞size特別大的body時或者傳遞上傳的文件時只傳遞文件名,這裏不展開),若是有則直接讀REQUEST_BODY_FILE對應的文件,若是沒有則調用fcgi_read函數解析FCGI_STDIN數據包。最後將讀取的數據寫入到以前建立的phpstream中。

php://input其實就是基於這個stream作的讀取包裝。對於multipart/form-data,因爲安裝的鉤子中post_reader是NULL,在本階段並未作任何事兒,所以沒法經過php://input獲取到原始的post body數據流。

下面對照着上面的流程,跟蹤下代碼:

SAPI_API SAPI_POST_READER_FUNC(sapi_read_standard_form_data)
{
    //建立phpstream
    SG(request_info).request_body = php_stream_temp_create_ex(TEMP_STREAM_DEFAULT, SAPI_POST_BLOCK_SIZE, PG(upload_tmp_dir)); 

    if (sapi_module.read_post) {
        size_t read_bytes;

        for (;;) {
            char buffer[SAPI_POST_BLOCK_SIZE];

            //調用sapi_module.read_post讀取FCGI_STDIN數據包
            read_bytes = sapi_read_post_block(buffer, SAPI_POST_BLOCK_SIZE);

            if (read_bytes > 0) {
                //將body數據寫到SG(request_info).request_body這個phpstream
                if (php_stream_write(SG(request_info).request_body, buffer, read_bytes) != read_bytes) {
                   ...
                }
            }
            ...
            if (read_bytes < SAPI_POST_BLOCK_SIZE) {
                /* done */
                break;
            }
        }
        php_stream_rewind(SG(request_info).request_body);
    }
}

sapi_read_post_block內部會調用sapi_module.read_post函數指針,而對於php-fpm而言,sapi_module.read_post指向sapi_cgi_read_post函數,該函數內部會調用fcgi_read讀取FCGI_STDIN數據流。

static sapi_module_struct cgi_sapi_module = {
    "fpm-fcgi",                     /* name */
    ...
    sapi_cgi_read_post,             /* read POST data */ 
    sapi_cgi_read_cookies,          /* read Cookies */
    ...
    STANDARD_SAPI_MODULE_PROPERTIES
};
 
static size_t sapi_cgi_read_post(char *buffer, size_t count_bytes)
{
    ...
    while (read_bytes < count_bytes) {
        ...
        if (request_body_fd == -1) {
            //檢查是否有REQUEST_BODY_FILE頭
            char *request_body_filename = FCGI_GETENV(request, "REQUEST_BODY_FILE");

            if (request_body_filename && *request_body_filename) {
                request_body_fd = open(request_body_filename, O_RDONLY);
                ...
            }
        }

        /* If REQUEST_BODY_FILE variable not available - read post body from fastcgi stream */
        if (request_body_fd < 0) {
            //若是沒有REQUEST_BODY_FILE頭,繼續按照FastCGI協議讀取FCGI_STDIN數據包
            tmp_read_bytes = fcgi_read(request, buffer + read_bytes, count_bytes - read_bytes);
        } else {
            //若是有REQUEST_BODY_FILE頭,從文件讀取body數據
            tmp_read_bytes = read(request_body_fd, buffer + read_bytes, count_bytes - read_bytes);
        }
        ...
        read_bytes += tmp_read_bytes;
    }
    return read_bytes;
}
 
int fcgi_read(fcgi_request *req, char *str, int len)
{
    int ret, n, rest;
    fcgi_header hdr;
    unsigned char buf[255];

    n = 0;
    rest = len;
    while (rest > 0) {
        if (req->in_len == 0) { //第一次循環,讀取header
            if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) ||
                hdr.version < FCGI_VERSION_1 ||
                hdr.type != FCGI_STDIN) { //若是header不是STDIN,異常退出
                req->keep = 0;
                return 0;
            }
            req->in_len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
            req->in_pad = hdr.paddingLength;
            if (req->in_len == 0) {
                return n;
            }
        }
        
        //讀取FCGI_STDIN的data
        if (req->in_len >= rest) {
            ret = (int)safe_read(req, str, rest);
        } else {
            ret = (int)safe_read(req, str, req->in_len);
        }
        ...
    }
    return n;
}

至此,咱們跟蹤完成了application/x-www-form-urlencoded的整個body讀取過程。

再回過頭來看下application/json,因爲並無爲它安裝鉤子,在sapi_read_post_data時,使用默認的處理方式。這裏的默認行爲會執行sapi_module.default_post_reader函數指針指向的函數。而這個函數指針指向哪一個函數呢?

在第二節講到的php_module_startup函數中有一個php_startup_sapi_content_types函數,它會指定sapi_module.default_post_reader是php_default_post_reader。

int php_startup_sapi_content_types(void)
{
    sapi_register_default_post_reader(php_default_post_reader); //設置default_post_reader
    sapi_register_treat_data(php_default_treat_data);
    sapi_register_input_filter(php_default_input_filter, NULL);
    return SUCCESS;
}
 
SAPI_API SAPI_POST_READER_FUNC(php_default_post_reader)
{
    if (!strcmp(SG(request_info).request_method, "POST")) {  //若是是POST請求
        if (NULL == SG(request_info).post_entry) { //若是Content-Type沒有對應的鉤子
            /* no post handler registered, so we just swallow the data */
            sapi_read_standard_form_data(); //和application/x-www-form-urlencoded同樣的處理邏輯
        }
    }
}

在php_default_post_reader中,咱們看到,其實它執行的仍然是sapi_read_standard_form_data函數,也就是在body信息讀取階段,儘管application/json沒有註冊鉤子,可是它和application/x-www-form-urlencoded仍然保持這一致的處理邏輯。這也解釋了,爲何application/json能夠經過php://input拿到原始post數據。

到如今,php://input的行爲差別已是能夠解釋的清了,而$_POST咱們須要繼續跟蹤下去。

數據後置處理

數據後置處理階段是用來對原始的body數據作後置處理的,$_POST就是在這個階段產生。下圖展現了在數據後置處理階段,php執行的函數流程。

clipboard.png

第二節講到,在php_module_startup函數中,會調用php_startup_auto_globals向CG(auto_globals)這個hashtable註冊超全局變量_GET、_POST、_COOKIE、_SERVER的鉤子,而後在合適的時機回調。

void php_startup_auto_globals(void)
{
    zend_register_auto_global(zend_string_init("_GET", sizeof("_GET")-1, 1), 0, php_auto_globals_create_get);
    zend_register_auto_global(zend_string_init("_POST", sizeof("_POST")-1, 1), 0, php_auto_globals_create_post);
    zend_register_auto_global(zend_string_init("_COOKIE", sizeof("_COOKIE")-1, 1), 0, php_auto_globals_create_cookie);
    zend_register_auto_global(zend_string_init("_SERVER", sizeof("_SERVER")-1, 1), PG(auto_globals_jit), php_auto_globals_create_server);
    zend_register_auto_global(zend_string_init("_ENV", sizeof("_ENV")-1, 1), PG(auto_globals_jit), php_auto_globals_create_env);
    zend_register_auto_global(zend_string_init("_REQUEST", sizeof("_REQUEST")-1, 1), PG(auto_globals_jit), php_auto_globals_create_request);
    zend_register_auto_global(zend_string_init("_FILES", sizeof("_FILES")-1, 1), 0, php_auto_globals_create_files);
}

而這個合適的時機就是php_request_startup中在sapi_activate以後執行的php_hash_environment函數。該函數內部會調用zend_activate_auto_globals函數,這個函數遍歷全部註冊的auto global,回調相應的鉤子。而$_POST對應的鉤子是php_auto_globals_create_post。

PHPAPI int php_hash_environment(void)
{
    memset(PG(http_globals), 0, sizeof(PG(http_globals)));
    zend_activate_auto_globals(); //激活超全局變量,回調startup時註冊的鉤子
    if (PG(register_argc_argv)) {
        php_build_argv(SG(request_info).query_string, &PG(http_globals)[TRACK_VARS_SERVER]);
    }
    return SUCCESS;
}
 
ZEND_API void zend_activate_auto_globals(void) /* {{{ */
{
    zend_auto_global *auto_global;

    ZEND_HASH_FOREACH_PTR(CG(auto_globals), auto_global) { //遍歷全部的超全局變量
        if (auto_global->jit) {
            auto_global->armed = 1;
        } else if (auto_global->auto_global_callback) {
            auto_global->armed = auto_global->auto_global_callback(auto_global->name); //回調鉤子函數
        } else {
            auto_global->armed = 0;
        }
    } ZEND_HASH_FOREACH_END();
}

php_auto_globals_create_post作了什麼操做呢?下圖展現了它的總體流程。

clipboard.png

在PG裏有一個http_globals字段,它是包含6個zval的數組。這6個zval分別用來臨時存儲 _POST、_GET、_COOKIE、_SERVER、_ENV和_FILES 數據。

struct _php_core_globals {
    ...
    zval http_globals[6]; //0-$_POST 1-$_GET 2-$_COOKIE 3-$_SERVER 4-$_ENV 5-$_FILES
    ...
};

對於一個簡單的post請求:curl -d "a=1" http://10.179.195.72:8585/test/jiweibin ,Content-Type是application/x-www-form-urlencoded,php_auto_globals_create_post所作的操做能夠分這麼幾步:

  • 讀取上一階段寫入到SG(request_info).request_body這個phpstream中的body數據到內存buf。這裏body數據是"a=1"這個字符串。
  • 解析post body數據(按&分割key-value對,按=分割key和value),並將解析後的數據經過調用add_post_vars函數,寫入到PG(http_globals)[0]這個zval中,zval的類型是數組類型。
  • 最後,爲了讓Zend引擎能夠經過_POST這個字符串索引到上一步解析的zval,咱們須要以"_POST"爲key,剛剛zval爲value註冊到php Zend引擎的全局變量符號表EG(symbol_table)中。

在php_auto_globals_create_post函數中, 當發現當前的請求是POST請求時,會調sapi_module.treat_data函數指針。在php_module_startup階段,php會設置sapi_module.treat_data函數指針指向php_default_treat_data函數。該函數會最終完成body數據解析並存儲到PG(http_globals)[0]這個zval中。在調用完php_default_treat_data以後,會將"_POST"和PG(http_globals)[0]註冊到符號表EG(symbol_table)。代碼以下:

static zend_bool php_auto_globals_create_post(zend_string *name)
{
    if (PG(variables_order) &&
            (strchr(PG(variables_order),'P') || strchr(PG(variables_order),'p')) &&
        !SG(headers_sent) &&
        SG(request_info).request_method &&
        !strcasecmp(SG(request_info).request_method, "POST")) { 
        sapi_module.treat_data(PARSE_POST, NULL, NULL); //從stream中讀取並解析body數據,存儲到PG(http_globals)[0]
    } else {
        zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_POST]);
        array_init(&PG(http_globals)[TRACK_VARS_POST]);
    }

    zend_hash_update(&EG(symbol_table), name, &PG(http_globals)[TRACK_VARS_POST]); //將'_POST'和PG(http_globals)[0]註冊到EG(symbol_table)
    Z_ADDREF(PG(http_globals)[TRACK_VARS_POST]);

    return 0; /* don't rearm */
}

在php_default_treat_data中,對於POST請求,會從新初始化PG(http_globals)[0](TRACK_VARS_POST是一個宏,在編譯階段會被替換爲0),而後調用sapi_handle_post函數,該函數會回調在SAPI啓動階段爲Content-Type安裝的鉤子中的post_handler函數指針。

SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data)
{
    ...
    zval array;
    ...

    ZVAL_UNDEF(&array);
    switch (arg) {
        case PARSE_POST:
        case PARSE_GET:
        case PARSE_COOKIE:
            array_init(&array);
            switch (arg) {
                case PARSE_POST:
                    zval_ptr_dtor(&PG(http_globals)[TRACK_VARS_POST]); //析構zval,釋放上一次請求的舊數組內存
                    ZVAL_COPY_VALUE(&PG(http_globals)[TRACK_VARS_POST], &array);  //從新初始化zval,指向新的空數組內存
                    break;
                ...
            }
            break;
        default:
            ZVAL_COPY_VALUE(&array, destArray);
            break;
    }

    if (arg == PARSE_POST) {
        sapi_handle_post(&array); //回調Content-Type鉤子
        return;
    }
    ...
}
 
SAPI_API void sapi_handle_post(void *arg)
{
    //若是Content-Type已經安裝鉤子
    if (SG(request_info).post_entry && SG(request_info).content_type_dup) { 
        SG(request_info).post_entry->post_handler(SG(request_info).content_type_dup, arg); //調用相應鉤子的post_handler函數指針
        efree(SG(request_info).content_type_dup);
        SG(request_info).content_type_dup = NULL;
    }

對於application/x-www-form-urlencoded,post_handler是php_std_post_handler。

SAPI_API SAPI_POST_HANDLER_FUNC(php_std_post_handler)
{
    zval *arr = (zval *) arg; //arg指向PG(http_globals)[0]
    php_stream *s = SG(request_info).request_body;
    post_var_data_t post_data;

    if (s && SUCCESS == php_stream_rewind(s)) {
        memset(&post_data, 0, sizeof(post_data));

        while (!php_stream_eof(s)) {
            char buf[SAPI_POST_HANDLER_BUFSIZ] = {0};
 
            //讀取上一階段被寫入的phpstream
            size_t len = php_stream_read(s, buf, SAPI_POST_HANDLER_BUFSIZ); 

            if (len && len != (size_t) -1) {
                smart_str_appendl(&post_data.str, buf, len);
 
                //解析並插入到arr中,arr指向PG(http_globals)[0]
                if (SUCCESS != add_post_vars(arr, &post_data, 0)) {
                    smart_str_free(&post_data.str);
                    return;
                }
            }

            if (len != SAPI_POST_HANDLER_BUFSIZ){ //讀到最後了
                break;
            }
        }

        if (post_data.str.s) {
            //解析並插入到arr中,arr指向PG(http_globals)[0]
            add_post_vars(arr, &post_data, 1);
            smart_str_free(&post_data.str);
        }
    }
}

對於multipart/form-data,post_handler是rfc1867_post_handler。因爲它的代碼過長,這裏再也不貼代碼了。因爲在body信息讀取階段,鉤子的post_reader是空,因此rfc1867_post_handler會一邊作FCGI_STDIN數據包的讀取,一邊作解析存儲工做,最終將數據包中的key-value對存儲到PG(http_globals)[0]中。另外,該函數還會對上傳的文件進行處理,有興趣的同窗能夠讀下這個函數。

對於application/json,因爲未安裝任何鉤子,因此在這裏不會作任何事情,PG(http_globals)[0]是空數組。所以若是Content-Type是application/json,是沒法獲取到$_POST變量的。

php_auto_globals_create_post執行的最後,須要進行全局變量符號表的註冊操做,這是爲何呢?其實這和Zend引擎的代碼執行有關係了。Zend引擎的編譯器碰到$_POST時,opcode會是ZEND_FETCH_R或者ZEND_FETCH_W(其中操做數是'_POST',fetch_type是global),在執行階段執行器會去EG(symbol_table)中根據key='_POST'去找到對應的zval。所以這裏的註冊操做是有必要的。

讓咱們用一個例子來驗證下opcode,寫一個簡單的php腳本test.php:

<?php
var_dump($_POST);

安裝vld擴展以後,執行php -dvld.active=1 test.php,能夠看到opcode是FETCH_R,正如咱們預期。它會先從全局符號表中查找'_POST'對應的zval,而後賦值給$0(主函數棧的第一個變量,該變量是隱式聲明)。

clipboard.png

4、postjson擴展

到這裏,咱們已經對$_POST的總體流程以及細節有所瞭解。讓咱們作點什麼吧,寫一個擴展,來讓application/json的請求也能夠享受到$_POST這個超全局變量帶來的便利。(這個擴展的生產環境的意義不大,徹底能夠在php層經過php://input拿到請求body,更多的是學以至用的學習意義)

如何來實現咱們的擴展呢? 上面咱們知道,之因此拿不到是由於沒有爲application/json安裝鉤子,致使在數據後置處理階段並無作post body的解析,因此這裏咱們須要安裝一個鉤子,鉤子的post_reader能夠是NULL(這樣會走默認邏輯),也能夠和application/x-www-form-urlencoded保持一致:sapi_read_standard_form_data。而post_handler則須要咱們編寫了,post_handler咱們取名:php_json_post_handler。

下圖展現了postjson擴展總體的執行流程:

  • 它在模塊初始化時,zend_startup_modules執行以後,會調用該擴展的MINIT函數,MINIT函數裏面會進行ini entry註冊,並獲取到關心的ini配置的值(這裏咱們會註冊一個開關配置postjson.parse表示是否開啓擴展),若是擴展開啓,咱們會向SG(known_post_content_types)註冊application/json的鉤子。
  • 而後在請求初始化時,FastCGI協議處理的數據後置處理階段,回調咱們的鉤子函數php_json_post_handler,完成json格式的post body的解析以及將解析後的key-value存儲到PG(http_globals)[0]的操做。
  • 後續php的框架代碼php_auto_globals_create_post會完成後續的符號表註冊操做。

clipboard.png

關於php_json_post_handler,對json的解析是一個複雜的過程,咱們可使用現有的輪子,看下php的json擴展是如何實現的:

static PHP_FUNCTION(json_decode)
{
    char *str;
    size_t str_len;
    zend_bool assoc = 0; /* return JS objects as PHP objects by default */
    zend_long depth = PHP_JSON_PARSER_DEFAULT_DEPTH;
    zend_long options = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|bll", &str, &str_len, &assoc, &depth, &options) == FAILURE) {
        return;
    }

    JSON_G(error_code) = 0;

    if (!str_len) {
        JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX;
        RETURN_NULL();
    }

    /* For BC reasons, the bool $assoc overrides the long $options bit for PHP_JSON_OBJECT_AS_ARRAY */
    if (assoc) {
        options |=  PHP_JSON_OBJECT_AS_ARRAY;
    } else {
        options &= ~PHP_JSON_OBJECT_AS_ARRAY;
    }

    php_json_decode_ex(return_value, str, str_len, options, depth); //解析str,存儲到return_value這個zval中
}

咱們可使用php_json_decode_ex(它內部使用yacc完成語法解析)這個函數來作json解析,將return_value替換爲&PG(http_globals)[0]。而str則從SG(request_info).request_body這個phpstream中去讀取。因此,總體的思路已經通了,下面咱們來操做一下。

生成擴展骨架

進入到源碼目前的ext目錄:cd /home/weibin/offcial_code/php/7.0.6/php-7.0.6/ext,執行 ./ext_skel --extname=postjson,這時在代碼目錄下能夠看到postjson.c和php_postjson.h等文件。

編輯php_postjson.h文件

咱們的擴展能夠在php.ini中開關,開的方式是postjson.parse=On,關的方式是postjson.parse=Off,因此這裏咱們須要定義一個存儲這個開關的結構體,parse字段表示這個開關。定義了2個常量:JSON_CONTENT_TYPE和CHUNK_SIZE,分別用來表示application/json的Content-Type和讀取phpstream時的buffer大小。

#ifndef PHP_POSTJSON_H
#define PHP_POSTJSON_H
 
#include "SAPI.h"
#include "ext/json/php_json.h"
#include "php_globals.h"
 
extern zend_module_entry postjson_module_entry;
#define phpext_postjson_ptr &postjson_module_entry
#define PHP_POSTJSON_VERSION "0.1.0" /* Replace with version number for your extension */
 
#ifdef PHP_WIN32
#    define PHP_POSTJSON_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#    define PHP_POSTJSON_API __attribute__ ((visibility("default")))
#else
#    define PHP_POSTJSON_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif
 
ZEND_BEGIN_MODULE_GLOBALS(postjson)
    zend_long  parse;  //存儲配置的結構體
ZEND_END_MODULE_GLOBALS(postjson)
 
SAPI_POST_HANDLER_FUNC(php_json_post_handler);
 
#define JSON_CONTENT_TYPE "application/json"
#define CHUNK_SIZE    8192
 
/* Always refer to the globals in your function as POSTJSON_G(variable).
   You are encouraged to rename these macros something shorter, see
   examples in any other php module directory.
*/
#define POSTJSON_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(postjson, v)
#if defined(ZTS) && defined(COMPILE_DL_POSTJSON)
ZEND_TSRMLS_CACHE_EXTERN()
#endif
#endif    /* PHP_POSTJSON_H */

編輯postjson.c文件

這裏定義ini配置,鉤子數組post_entries,實現php_json_post_handler,並改寫MINIT函數,判斷ini中開關postjson.parse是否開啓,若是開啓,則註冊鉤子。

在php_json_post_handler中分配一個8k的zend_string,讀取SG(request_info).request_body這個phpstream到一個8k的buffer,若是一次讀取不完,分屢次讀取,zend_string不斷擴容,最終包含整個json字符串。最後調用php_json_decode_ex函數完成json串解析並存儲到PG(http_globlas)[0]中。

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_postjson.h"

ZEND_DECLARE_MODULE_GLOBALS(postjson)
/* True global resources - no need for thread safety here */
static int le_postjson;
 
//postjson擴展使用到的ini
PHP_INI_BEGIN()
    STD_PHP_INI_BOOLEAN("postjson.parse",      "0", PHP_INI_ALL, OnUpdateLong, parse, zend_postjson_globals, postjson_globals)
PHP_INI_END()

static sapi_post_entry post_entries[] = { //定義Content-Type鉤子
    { JSON_CONTENT_TYPE,    sizeof(JSON_CONTENT_TYPE)-1,    sapi_read_standard_form_data,  php_json_post_handler },
    { NULL, 0, NULL, NULL }
};
SAPI_POST_HANDLER_FUNC(php_json_post_handler){ //post handler
    size_t ret = 0;
    char *ptr;
    size_t len = 0, max_len;
    int step = CHUNK_SIZE;
    int min_room = CHUNK_SIZE / 4;
    int persistent = 0;
    zend_string *result;
    php_stream *s = SG(request_info).request_body;
    if (s && SUCCESS == php_stream_rewind(s)) {
        max_len = step;
    
        result = zend_string_alloc(max_len, persistent);
        ptr = ZSTR_VAL(result);
        while ((ret = php_stream_read(s, ptr, max_len - len)))    { //讀取SG(request_info).request_body這個phpstream
            len += ret;
            if (len + min_room >= max_len) {
                result = zend_string_extend(result, max_len + step, persistent);
                max_len += step;
                ptr = ZSTR_VAL(result) + len;
            } else {
                ptr += ret;
            }
        }
        if (len) {
            result = zend_string_truncate(result, len, persistent);
            ZSTR_VAL(result)[len] = '\0';
            //解析json,並存儲到PG(http_globals)[0]
            php_json_decode_ex(&PG(http_globals)[TRACK_VARS_POST], ZSTR_VAL(result), ZSTR_LEN(result), PHP_JSON_OBJECT_AS_ARRAY, PHP_JSON_PARSER_DEFAULT_DEPTH);
        } else {
            zend_string_free(result);
            result = NULL;
        }
    }
}

static void php_postjson_init_globals(zend_postjson_globals *postjson_globals)
{
    postjson_globals->parse = 0;
}

 
PHP_MINIT_FUNCTION(postjson)
{
    ZEND_INIT_MODULE_GLOBALS(postjson, php_postjson_init_globals, NULL);
    REGISTER_INI_ENTRIES();
    int parse = (int)POSTJSON_G(parse);
    if(parse == 1){ //若是ini中postjson.parse開啓,那麼將application/json的鉤子註冊到SG(known_post_content_types)中
        sapi_register_post_entries(post_entries);    
    }
    return SUCCESS;
}

const zend_function_entry postjson_functions[] = { //這裏咱們不註冊任何php函數
        PHP_FE_END    /* Must be the last line in postjson_functions[] */
};

static zend_module_dep module_deps[] = { //本擴展依賴php的json擴展
    ZEND_MOD_REQUIRED("json")
    ZEND_MOD_END
};

zend_module_entry postjson_module_entry = {
    STANDARD_MODULE_HEADER_EX,NULL,
    module_deps,
    "postjson",
    postjson_functions,
    PHP_MINIT(postjson),
    PHP_MSHUTDOWN(postjson),
    PHP_RINIT(postjson),        /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(postjson),    /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(postjson),
    PHP_POSTJSON_VERSION,
    STANDARD_MODULE_PROPERTIES
};
...

編譯安裝

phpize 

   configure --with-php-config=../php-config

   make

   make install

配置php.ini

增長post配置:

[postjson]
extension="postjson.so"
postjson.parse=On

驗證是否安裝成功:php -m|grep postjson

clipboard.png

測試

重啓php-fpm,kill -USR2 cat /home/weibin/php7/var/run/php-fpm.pid

編寫測試腳本:

<?php
namespace xxx\Test;

class Jiweibin{
    function index() {
        var_dump($_POST);
        var_dump(file_get_contents("php://input"));
    }

}

執行curl命令,curl -H "Content-Type: application/json" -d '{"a":1}' http://10.179.195.72:8585/test/jiweibin,執行結果以下,咱們看到經過$_POST能夠拿到解析後的post數據了,搞定。

clipboard.png

5、總結

本篇wiki,從源碼角度分析了php中_POST的原理,展示了FastCGI協議的總體處理流程,以及針對不一樣Content-Type的處理差別化,併爲application/json動手編寫了php擴展,實現了_POST的解析,但願你們有所收穫。但本篇wiki並非終點,經過編寫這篇wiki,對json解析(yacc)、Zend引擎原理有了比較濃厚的興趣和探知慾,有時間的話,但願能分享給你們,另外感謝個人同事朱棟同窗,一塊兒跟代碼的感受仍是很讚的。

相關文章
相關標籤/搜索