运营研发团队 季伟滨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引擎原理有了比较浓厚的兴趣和探知欲,有时间的话,但愿能分享给你们,另外感谢个人同事朱栋同窗,一块儿跟代码的感受仍是很赞的。