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

运营研发团队 季伟滨

一、前言

前几天的工作中,需要通过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解决了接口测试的问题。事后,脑子里面冒出了挺多问题:

  • php-fpm是怎么读取并解析FastCGI协议的?http请求的header和body分别都存储在哪里?
  • 对于Content-Type是application/x-www-form-urlencoded的请求,为什么通过$_POST可以拿到解析后的参数数组?
  • 对于Content-Type是application/json的请求,为什么通过$_POST拿不到解析后的参数数组?

基于这几个问题,对php代码进行了一次新的学习, 有一定的收获,在这里记录一下。
最后,编写了一个叫postjson的php扩展,它在源代码层面实现了feature:对于Content-Type是application/json的请求,可以通过$_POST拿到请求参数。

二、fpm整体流程

在分析之前,有必要对php-fpm整体流程有所了解。包括你可能想知道的fpm进程启动过程、ini配置文件何时读取,扩展在哪里被加载,请求数据在哪里被读取等等,这里都会稍微提及一下,这样看后面的时候,我们会比较清楚,某一个函数调用发生在整个流程的哪一个环节,做到可识庐山真面目,哪怕身在此山中。

clipboard.png

和Nginx进程的启动过程类似,fpm启动过程有3种进程角色:启动shell进程、fpm master进程和fpm worker进程。上图列出了各个进程在生命周期中执行的主要函数,其中标有颜色的表示和上面的问题答案有关联的函数。下面概况的说明一下:

启动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:监听请求连接,读取请求的头信息。

    • 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:请求初始化

    • 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进程的事件处理机制是不一样的。

三、FastCGI协议的处理

言归正传,让我们回到本文的主题,一步步接开$_POST的面纱。

大家都知道$_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;
       
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值