TIPI020200-SAPI概述

前一小节介绍了PHP的生命周期, 所有的请求都是通过SAPI接口实现的. 在源码的SAPI目录存放了PHP对各种服务器抽象层的代码,例如命令行程序的实现, mod_php的apache模块实现以及fastcgi的实现等等.

在各个服务器抽象层之间遵守着相同的约定,这里我们称之为SAPI接口。每个服务器都需要实现各自己的_sapi_module_struct结构中的各个方法。 然后在这个接口层之下,关于PHP的公共部分,全部通过这个结构体的相关方法调用实现。 如cgi模式和apache2服务器中的启动方法:

cgi_sapi_module.startup(&cgi_sapi_module)   //  cgi模式 cgi/cgi_main.c文件

apache2_sapi_module.startup(&apache2_sapi_module);  //  apache2服务器  apache2handler/sapi_apache2.c文件

除了startup方法,sapi_module_struct结构还有许多其它方法。其部分定义如下:

struct _sapi_module_struct {
    char *name;         //  名字(标识用)
    char *pretty_name;  //  更好理解的名字(自己翻译的)

    int (*startup)(struct _sapi_module_struct *sapi_module);    //  启动函数
    int (*shutdown)(struct _sapi_module_struct *sapi_module);   //  关闭方法

    int (*activate)(TSRMLS_D);  // 激活
    int (*deactivate)(TSRMLS_D);    //  停用

    int (*ub_write)(const char *str, unsigned int str_length TSRMLS_DC);    //  不缓存的写操作(unbuffered write)
    void (*flush)(void *server_context);    //  flush
    struct stat *(*get_stat)(TSRMLS_D);     //  get uid
    char *(*getenv)(char *name, size_t name_len TSRMLS_DC); //  getenv

    void (*sapi_error)(int type, const char *error_msg, ...);   /* error handler */

    int (*header_handler)(sapi_header_struct *sapi_header, 
        sapi_header_op_enum op, sapi_headers_struct *sapi_headers TSRMLS_DC);   /* header handler */
    int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);   /* send headers handler */
    void (*send_header)(sapi_header_struct *sapi_header, void *server_context TSRMLS_DC);   /* send header handler */

    int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC); /* read POST data */
    char *(*read_cookies)(TSRMLS_D);    /* read Cookies */

    void (*register_server_variables)(zval *track_vars_array TSRMLS_DC);    /* register server variables */
    void (*log_message)(char *message);     /* Log message */
    time_t (*get_request_time)(TSRMLS_D);   /* Request Time */
    void (*terminate_process)(TSRMLS_D);    /* Child Terminate */

    char *php_ini_path_override;    //  覆盖的ini路径

    ...
    ...
};

以上的这些结构在各服务器的接口实现中都有定义。如apache2的定义:

static sapi_module_struct apache2_sapi_module = {
    "apache2handler",
    "Apache 2.0 Handler",

    php_apache2_startup,                /* startup */
    php_module_shutdown_wrapper,            /* shutdown */

    ...
}

整个SAPI类似于一个面向对象中的模板方法模式的应用。SAPI.c和SAPI.h文件所包含的一些函数就是模板方法模式中的抽象模板,各个服务器对于sapi_module的定义及相关实现则是一个个具体的模板。只是这里没有继承。

作者:TIPI团队

TIPI0201–PHP生命周期和Zend引擎

一切的开始: SAPI接口

通常我们编写PHP Web程序都是通过Apache或者Nginx这类Web服务器来测试脚本. 或者在命令行下通过php程序来执行PHP脚本. 执行完成脚本后,服务器应答,浏览器显示应答信息,或者在命令结束后在标准输出显示内容. 我们很少关心PHP解释器在哪里. 虽然通过Web服务器和命令行程序执行 脚本看起来很不一样. 实际上她们的工作是一样的. 命令行程序和Web程序类似, 命令行参数传递给要执行的脚本,相当于通过url 请求一个php页面. 脚本戳里完成后返回响应结果,只不过命令行响应的结果是显示在终端上. 脚本执行的开始都是通过SAPI接口 进行的. 下一节将对SAPI进行更为深入的介绍.

开始和结束

PHP通过SAPI开始以后会经过两个主要的阶段. 处理请求之前的开始阶段和请求之后的结束阶段. 开始阶段有两个过程, 第一个是在 整个SAPI生命周期内(例如Apache启动以后的整个生命周期内或者命令行程序整个执行过程中)的开始阶段(MINIT),该阶段只进行一次. 第二个过程 发生在请求阶段,例如通过url请求某个页面.则在每次请求之前都会进行初始化过程(RINIT请求开始). 例如PHP注册了一些PHP模块,则在MINIT阶段会回调所有模块的MINIT函数. 模块在这个阶段可以进行一些初始化工作,例如注册常量, 定义 模块使用的类等等. 一般的模块回调函数

PHP_MINIT_FUNCTION(myphpextension)
{
    // 注册常量或者类等初始化操作
    return SUCCESS; 
}

请求到达之后PHP初始化执行脚本的基本环境,例如创建一个执行环境,包括保存php运行过程中变量名称和变量值内容的符号表. 以及当前所有 的函数以及类等信息的符号表. 然后PHP会调用所有模块RINIT函数, 在这个阶段各个模块也可以执行一些相关的操作, 模块的RINIT函数和MINIT函数类似

PHP_RINIT_FUNCTION(myphpextension)
{
    // 例如记录请求开始时间
    // 随后在请求结束的时候记录结束时间.这样我们就能够记录下处理请求所花费的时间了
    return SUCCESS; 
}

请求处理完后就进入了结束阶段, 一般脚本执行到末尾或者通过调用exit()或者die()函数,php都将进入结束阶段. 和开始阶段对应,结束阶段也分为 两个环节,一个在请求结束后(RSHUWDOWN),一个在SAPI生命周期结束时(MSHUTDOWN).

PHP_RSHUTDOWN_FUNCTION(myphpextension)
{
    // 例如记录请求结束时间, 并把相应的信息写入到日至文件中.
    return SUCCESS; 
}

想要了解扩展开发的相关内容,请参考第十三章 扩展开发

单进程SAPI生命周期

CLI/CGI模式的PHP属于单进程的SAPI模式. 这类的请求只处理一次请求就关闭. 也就是只会经过如下几个环节 开始 – 请求开始 – 请求关闭 – 结束 SAPI 接口就完成了其生命周期

多进程SAPI生命周期

通常PHP是编译为一个apache的模块来处理PHP请求的. 通常Apache会采用多进程模式, apache启动后fork出多个子进程, 每个进程的内存空间独立, 每个子进程都会经过开始和结束环节,不过每个进程的开始阶段只在进程fork出来以来后进行, 在整个进程的生命周期内可能会处理多个请求. 只有 在Apache关闭或者进程被结束之后才会进行关闭阶段,在这两个阶段之间会随着每个请求重复请求开始-请求关闭的环节.

多线程的SAPI生命周期

而在多线程模式下和多进程中的某个进程类似,不同的是在整个进程的生命周期内会并行的重复着 请求开始-请求关闭的环节

Zend引擎

Zend引擎作为PHP实现的核心,提供了语言实现上的基础设施.例如: PHP的语法实现, 脚本的编译运行环境, 扩展机制以及内存管理等, 可以说Zend引擎是PHP的核心, 当然这里的PHP指的是官方的PHP实现(除了官方的实现,目前比较知名的有facebook的hiphop实现,不过到目前为止,PHP还没有一个标准的语言规范), 而PHP则提供了请求处理和其他Web服务器的接口(SAPI).

参考文献


Extending and Embedding PHP

作者:TIPI团队

安全?安全!安全!!

安全?安全!安全!!
在某日下班后,回家的路上,一个人走,走着走着,忽然我的手碰到了另一支手,艳遇?扭头!发现一新疆美男在左顾右盼。然而我背的包已经被拉开了一半。好险,我的kindle,钱包都在包里。

年关将近,是该注意安全了。特别是回家的路上,请各位兄弟照顾好贵重物品和老婆,防火防盗啊!

好吧,言归正转。我们还是说说我们在开发中的安全问题吧。

这里我们从一次PHP请求过程来说明一些常见的安全问题。

【从客户端到服务器应用层】
每次用户的请求都会先通过客户端发起请求,然后服务器接收请求并处理这个请求。
如果我们在客户端有验证(如必须输入用户名之类的),此时我们除了在客户端验证外,更重要的是在服务器的应用层进行验证(所有的验证必须在服务器有一份)。这些验证包括不能为空,输入内容的长度,输入内容的类型等。如果确定为数字型,可以考虑将其转化为数字类型后使用。
为什么要这样做呢?因为我们不相信客户端,也不相信客户端的验证。在某些情况下,对于用户输入的数据,我们不仅仅是过滤,在必要的时候可能需要直接提示用户输入错误。
如果我们在实际项目中要做类似于投票的功能,需要对用户进行限制,常规的,ip,验证码等。除此之外,如果是基于某个已有应用,并且这个应用是有账号这个概念的,那么我们可以加入基于账号的验证;如果整个投票活动是基于某个活动流程,那么我们也可以加入流程验证。

【从应用层到数据层】
在应用层接收到客户端的数据后,如果此时我们需要查询后台的数据进行相关计算,那么我们需要调用数据层获取数据,当把参数传给数据层后,数据层应该不相信应用层的数据,它需要按照自己的逻辑进行类型等的验证,然后再将查询的条件传给数据库。
这样也许可以减少一些SQL注入漏洞。

【从服务器应用层到客户端展示】
当应用层从数据层获取数据处理完后,需要在前台展示,此时会将计算结果返回给客户端。
在显示计算结果之前我们需要将计算结果进行过滤。因为我们不相信服务器(或者说数据层)返回给我们的信息。对,不相信他们。
这样也许可以减少一些存储式跨站脚本漏洞。

【服务器本身的问题】
就PHP本身而言,一些必要的参数,安全模式的开启。比如php.ini中的open_basedir参数。
或者在开发机器经常看到可以浏览整个目录的情况。
或者PHP中的打开register_globals(新的版本默认为关闭)

所有的说到底就一个原则:不要相信任何人!
此时想起了面向对象中的迪米特法则:不要和陌生人说话。
也许没有什么关联,只是在某些点上,大道相通。