運營研發團隊 季偉濱php
前幾天的工做中,須要經過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
對於Content-Type是application/json的請求,爲何經過$_POST拿不到解析後的參數數組?web
基於這幾個問題,對php代碼進行了一次新的學習, 有必定的收穫,在這裏記錄一下。
最後,編寫了一個叫postjson的php擴展,它在源代碼層面實現了feature:對於Content-Type是application/json的請求,能夠經過$_POST拿到請求參數。shell
在分析以前,有必要對php-fpm總體流程有所瞭解。包括你可能想知道的fpm進程啓動過程、ini配置文件什麼時候讀取,擴展在哪裏被加載,請求數據在哪裏被讀取等等,這裏都會稍微說起一下,這樣看後面的時候,咱們會比較清楚,某一個函數調用發生在整個流程的哪個環節,作到可識廬山真面目,哪怕身在此山中。json
和Nginx進程的啓動過程相似,fpm啓動過程有3種進程角色:啓動shell進程、fpm master進程和fpm worker進程。上圖列出了各個進程在生命週期中執行的主要函數,其中標有顏色的表示和上面的問題答案有關聯的函數。下面概況的說明一下:api
2.php_module_startup :模塊初始化。php.ini文件的解析,php動態擴展.so的加載、php擴展、zend擴展的啓動都是在這裏完成的。數組
fcgi_accept_request:監聽請求鏈接,讀取請求的頭信息。cookie
php_request_startup:請求初始化php7
注:當worker進程執行完php_request_shutdown後會再次調用fcgi_accept_request函數,準備監聽新的請求。這裏能夠看到一個worker進程只能順序的處理請求,在處理當前請求的過程當中,該worker進程不會接受新的請求鏈接,這和Nginx worker進程的事件處理機制是不同的。
言歸正傳,讓咱們回到本文的主題,一步步接開$_POST的面紗。app
你們都知道$_POST存儲的是對http請求body數據解析後的數組,但php-fpm並非一個web server,它並不支持http協議,通常它經過FastCGI協議來和web server如Apache、Nginx進行數據通訊。關於這個協議,已經有其餘同窗寫的好幾篇很棒的文章來說述,若是對FastCGI不瞭解的,能夠先讀一下這些文章。
一個FastCGI請求由三部分的數據包組成:FCGI_BEGIN_REQUEST數據包、FCGI_PARAMS數據包、FCGI_STDIN數據包。
FCGI_BEGIN_REQUEST表示請求的開始,它包括:
FCGI_PARAMS主要用來傳輸http請求的header以及fastcgi_param變量數據,它包括:
FCGI_STDIN用來傳輸http請求的body數據,它包括:
尾header:表示FCGI_STDIN的結束
php對FastCGI協議自己的處理上,能夠分爲了3個階段:頭信息讀取、body信息讀取、數據後置處理。下面一一介紹各個階段都作了些什麼。
頭信息讀取階段只讀取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的格式通常是這樣:
這裏有一個細節須要注意,爲了節省空間,在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...")時,內存的示意圖:
該階段負責處理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的差別則要在下一節進行講解。
在body信息讀取階段,對不一樣的Content-Type差別化處理的關鍵節點發生在sapi_read_post_data函數,見下圖,展現了差別化處理的總體流程:
下面咱們基於上圖,結合着代碼進行詳細分析。(代碼可能會稍微多一點,這塊代碼比較核心,不是很好經過圖的方式去畫)
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都幹了些什麼,它的總體流程能夠參考下圖:
首先,它會建立一個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執行的函數流程。
第二節講到,在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作了什麼操做呢?下圖展現了它的總體流程。
在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所作的操做能夠分這麼幾步:
在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(主函數棧的第一個變量,該變量是隱式聲明)。
到這裏,咱們已經對$_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擴展總體的執行流程:
後續php的框架代碼php_auto_globals_create_post會完成後續的符號表註冊操做。
關於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.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 */
這裏定義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
增長post配置:
[postjson] extension="postjson.so" postjson.parse=On
驗證是否安裝成功:php -m|grep postjson
重啓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數據了,搞定。
本篇wiki,從源碼角度分析了php中_POST的原理,展示了FastCGI協議的總體處理流程,以及針對不一樣Content-Type的處理差別化,併爲application/json動手編寫了php擴展,實現了_POST的解析,但願你們有所收穫。但本篇wiki並非終點,經過編寫這篇wiki,對json解析(yacc)、Zend引擎原理有了比較濃厚的興趣和探知慾,有時間的話,但願能分享給你們,另外感謝個人同事朱棟同窗,一塊兒跟代碼的感受仍是很讚的。