思考PHP之五:访问控制

面向对象三大特性:封装性、继承性和多态性。 封装隐藏了对象内部的细节和实现, 使对象能够集中而完整的描述并对应一个具体的事物。 它使对象只提供对外的访问接口,这样可以在不改变接口的前提下改变实现细节,而且能使对象自我完备。 除此之外,封装还可以增强安全性和简化编程。 继承的目的是为了实现代码复用,它是一种一般化与特殊化的关系,其中子类是父类的细化。 在实现继承时最需要考虑的问题是子类和父类是不是”IS-A”的关系。

PHP(其它面向对象的语言也类似)对于封装和继承的一些特性是通过访问控制实现。访问控制的作用是控制成员变量,成员方法和基类。 曾经一直以为访问控制的作用仅仅是控制一个类的成员方法和成员变量,这把自己的思维局限于一类一对象了, 这两个方面的控制是PHP对面向对象中封装特性的支持。 把思维拉升到面向对象的体系之上,访问控制也控制了基类(或父类)的行为,或者说控制了继承特性的某些方面。

PHP中关于访问控制的关键字和Java等其它面向对象语言一样,如下:

  • public 所定义的类成员可以在任何地方被访问
  • protected 所定义的类成员则可以被其所在类、其所在类的子类和父类访问
  • private 定义的类成员则只能被其所在类访问。

以上的类成员包括成员变量和成员函数。不管是成员变量还是成员方法,PHP默认都是public。 在Java中访问控制默认为包可见,在C++中访问控制默认为私有(private),而PHP则是公有的(public),这比Java还要open。 笔者认为这是PHP的一个历史遗留问题。如果可以重新设计PHP,可能是另一个结果,并且这也是语言的对于访问的态度问题。

前面介绍的各个访问控制是针对封装性,对于继承性,如下:

  • public/protected 可以被继承
  • private 没有被继承

实际上,在PHP中,私有方法也会被继承下来,只是其上下文没有改变(还是父类),从而在调用的时候出错。

一般来说,private定义的成员只能被内部调用,仅供当前类使用,这在PHP的源码中检查访问权限控制时, 以private的成员会检查是否属于当前类体现。public定义的成员则属于类或对象的外部接口, 声明的public成员最好是定义好后就不要再变更,这会影响到调用了类的这些方法的相关客户。 好的public和private的设计对于对象本身的自我完备的实际有重大的意义。

但是public关键字有一些二义性。对于封装性,它是公有的,任何地方都可以访问的成员;对于继承性, 它允许子类继承此成员。同时兼顾这两个特性,当我们把它作为一个接口提供给外部使用时就会有一些歧义: 子类可以覆盖该成员方法,同时也可以调用访方法,如果子类覆盖了该成员方法并调用了该方法, 则它的实现就和你当初作为接口提供给外部时的含义有一些不同了。和public一样,protected也有类似的问题。 可以思考一下:各语言这样实现的目的是什么?是否有更好的方案?

PHP中的前缀自增(++i) 和后缀自增 (i++)

当我们学第一门语言时,比如大学课程中的C语言程序设计,也许曾经被前缀自增(++i) 和后缀自增 (i++)纠结过。 曾经以为我们懂了:

  • i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1
  • ++i :先增加后引用,让i先加1,然后在i所在的表达式中使用i的新值

这个表达基本没错,只能说不够精确。在《Expert C Programming》这本书中的附录中,有这样一段说明: ++i表示取i的地址,增加它的内容,然后把值放在寄存器中;i++表示取i的地址,把它的值装入寄存器中,然后增加内存中的i的值。 这里的寄存器存放的就是我们在表达式中使用的值。

在PHP中也有++$i和$i++,那么Zend内核是如何实现这两种自增方式的呢? 看下面一个例子,在不运行这段代码的情况下,你认为会输出什么呢?

$i = 0;
$i = $i++;
echo $i;

咱们先不论答案是什么?我们直接从Zend内核查看这种自增操作的实现。

使用VLD查看包含了$i++和++$i的PHP代码生成的中间代码:

$i = 0;
$i++;
++$i;

使用VLD命令(php -dvld.active=1 -dvld.verbosity=3 t.php)查看详细参数:

number of ops:  8
compiled vars:  !0 = $i
line     # *  op                           fetch          ext  return  operands
--------------------------------------------------------------------------------
-
   2     0  >   EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         1      ASSIGN                                                    OP1[IS_CV !0 ] OP2[ ,  IS_CONST (0) 0 ]
   3     2      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         3      POST_INC                                          RES[  IS_TMP_VAR ~1 ]       OP1[  IS_CV !0 ]
         4      FREE                                                      OP1[IS_TMP_VAR ~1 ]
   4     5      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         6      PRE_INC                                                   OP1[IS_CV !0 ]
   5     7    > RETURN                                                    OP1[IS_CONST (0) 1 ]

branch: #  0; line:     2-    5; sop:     0; eop:     7
path #1: 0,

从VLD扩展的输出信息可以知道,前缀自增(++$i)对应的opcode为PRE_INC,后缀自增($i++)对应的opcode为POST_INC。 首先我们看前缀自增(++$i),++$i没有返回值或者说它的返回值为空。 根据中间代码和VLD显示的OP1的参数类型, 我们可以知道++$i的中间代码在执行是最终调用的是Zend/zend_vm_execute.h文件中的ZEND_PRE_INC_SPEC_CV_HANDLER函数。 在ZEND_PRE_INC_SPEC_CV_HANDLER函数中有几个关键点:

  • CV类型变量的获取,它是调用_get_zval_ptr_ptr_cv获取CV类型变量。 这里的CV类型的变量是PHP编译期间的类似于缓存的作用,主要作用是提高某些变量的存储速度。
  • increment_function函数,不管是实例变量,类变量或者常规的变量,最终都是调用increment_function函数实现变量的增加操作。 在这个函数中,程序会根据变量的类型做出不同的处理,在PHP5.3.1这个版本中,PHP支持IS_LONG、IS_DOUBLE、IS_NULL和IS_STRING四种类型。 如果变量的类型是IS_NULL,程序会将变量的值赋值为1。如果变量类型是字符串,程序会将其转化成整形或浮点型进行计算。
  • 使用RETURN_VALUE_UNUSED宏清除返回结果,这个宏的作用是将result变量的类型设置为EXT_TYPE_UNUSED类型。

前缀自增(++$i)操作在Zend内核中本质上是操作变量本身,而且在表达式中使用的也是这个变量本身。

了解了++$i的实现,我们来看下可能使用得更多的$i++操作的实现。 同样,从中间代码POST_INC和OP1的类型是IS_CV,我们可以在Zend/zend_vm_execute.h文件中找到其实现为ZEND_POST_INC_SPEC_CV_HANDLER。 与前面的ZEND_PRE_INC_SPEC_CV_HANDLER相比,它们都有一个取CV类型变量的过程,也有一个increment_function函数增加变量值的过程, 但是除此之外它多了一个操作,同时也少了一个操作。 它多的一个操作是:

EX_T(opline->result.u.var).tmp_var = **var_ptr;
zendi_zval_copy_ctor(EX_T(opline->result.u.var).tmp_var);

这两行代码的作用是初始化返回值到临时变量,并且将原始的$i的值存储在这,这就是我们在前面使用VLD查看生成的中间代码其结果为RES[ IS_TMP_VAR ~1 ]的原因。 在这个初始化完成后,程序会继续执行增加操作,在增加操作完成后,它就结束了,而之前的++$i操作则会将result设置为UNUSED类型,这就是它少的那个操作。

后缀自增($i++)在表达式中使用的是存放在临时变量中原先的变量值,而变量本身的值已经增加了。 在PHP中这种变量的分离是通过临时变量+返回值解决。

到这里,我们可以回答最开始的问题了,它会输出0。因为在表达式中$i++的返回值是一个临时变量,也就是$i原来的值,也就是0。

PHP的CGI实现

FastCGI简介

CGI全称是“通用网关接口”(Common Gateway Interface), 它可以让一个客户端,从网页浏览器向执行在Web服务器上的程序请求数据。 CGI描述了客户端和这个程序之间传输数据的一种标准。 CGI的一个目的是要独立于任何语言的,所以CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php,perl,tcl等

FastCGI像是一个常驻(long-live)型的CGI, 它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。 它还支持分布式的运算, 即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。

FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。 众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中并接受FastCGI进程管理器调度, 则可以提供良好的性能、伸缩性、Fail- Over特性等等。

一般情况下,FastCGI的整个工作流程是这样的。

  1. Web Server启动时载入FastCGI进程管理器(IIS ISAPI或Apache Module)
  2. FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web Server的连接。
  3. 当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi。
  4. FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。 在CGI模式中,php-cgi在此便退出了。

PHP中的CGI实现

PHP的CGI实现本质是是以socket编程实现一个TCP或UDP协议的服务器,当启动时,创建TCP/UDP协议的服务器的socket监听, 并接收相关请求进行处理。这只是请求的处理,在此基础上添加模块初始化,sapi初始化,模块关闭,sapi关闭等就构成了整个CGI的生命周期。

以TCP为例,在TCP的服务端,一般会执行这样几个操作步骤:

  1. 调用socket函数创建一个TCP用的流式套接字;
  2. 调用bind函数将服务器的本地地址与前面创建的套接字绑定;
  3. 调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;
  4. 服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;
  5. 当与客户端创建连接后,服务器调用read_stream函数读取客户的请求;
  6. 处理完数据后,服务器调用write函数向客户端发送应答。

TCP上客户-服务器事务的时序如图2.6所示:

TCP上客户-服务器事务的时序

PHP的CGI实现从cgi_main.c文件的main函数开始,在main函数中调用了定义在fastcgi.c文件中的初始化,监听等函数。 对比TCP的流程,我们查看PHP对TCP协议的实现,虽然PHP本身也实现了这些流程,但是在main函数中一些过程被封装成一个函数实现。 对应TCP的操作流程,PHP首先会执行创建socket,绑定套接字,创建监听:

if (bindpath) {
    fcgi_fd = fcgi_listen(bindpath, 128);   //  实现socket监听,调用fcgi_init初始化
    ...
}

在fastcgi.c文件中,fcgi_listen函数主要用于创建、绑定socket并开始监听,它走完了前面所列TCP流程的前三个阶段,

    if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 ||
        ...
        bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 ||
        listen(listen_socket, backlog) < 0) {
        ...
    }

当服务端初始化完成后,进程调用accept函数进入阻塞状态,在main函数中我们看到如下代码:

    while (parent) {
        do {
            pid = fork();   //  生成新的子进程
            switch (pid) {
            case 0: //  子进程
                parent = 0;

                /* don't catch our signals */
                sigaction(SIGTERM, &old_term, 0);   //  终止信号
                sigaction(SIGQUIT, &old_quit, 0);   //  终端退出符
                sigaction(SIGINT,  &old_int,  0);   //  终端中断符
                break;
                ...
                default:
                /* Fine */
                running++;
                break;
        } while (parent && (running < children));

    ...
        while (!fastcgi || fcgi_accept_request(&request) >= 0) {
        SG(server_context) = (void *) &request;
        init_request_info(TSRMLS_C);
        CG(interactive) = 0;
                    ...
            }

如上的代码是一个生成子进程,并等待用户请求。在fcgi_accept_request函数中,程序会调用accept函数阻塞新创建的进程。 当用户的请求到达时,fcgi_accept_request函数会判断是否处理用户的请求,其中会过滤某些连接请求,忽略受限制客户的请求, 如果程序受理用户的请求,它将分析请求的信息,将相关的变量写到对应的变量中。 其中在读取请求内容时调用了safe_read方法。如下所示: [main() -> fcgi_accept_request() -> fcgi_read_request() -> safe_read()]

static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
    size_t n = 0;
    do {
    ... //  省略  对win32的处理
        ret = read(req->fd, ((char*)buf)+n, count-n);   //  非win版本的读操作
    ... //  省略
    } while (n != count);

}

如上对应服务器端读取用户的请求数据。

在请求初始化完成,读取请求完毕后,就该处理请求的PHP文件了。 假设此次请求为PHP_MODE_STANDARD则会调用php_execute_script执行PHP文件。 在此函数中它先初始化此文件相关的一些内容,然后再调用zend_execute_scripts函数,对PHP文件进行词法分析和语法分析,生成中间代码, 并执行zend_execute函数,从而执行这些中间代码。关于整个脚本的执行请参见第三节 脚本的执行。

在处理完用户的请求后,服务器端将返回信息给客户端,此时在main函数中调用的是fcgi_finish_request(&request, 1); fcgi_finish_request函数定义在fastcgi.c文件中,其代码如下:

int fcgi_finish_request(fcgi_request *req, int force_close)
{
int ret = 1;

if (req->fd >= 0) {
    if (!req->closed) {
        ret = fcgi_flush(req, 1);
        req->closed = 1;
    }
    fcgi_close(req, force_close, 1);
}
return ret;
}

如上,当socket处于打开状态,并且请求未关闭,则会将执行后的结果刷到客户端,并将请求的关闭设置为真。 将数据刷到客户端的程序调用的是fcgi_flush函数。在此函数中,关键是在于答应头的构造和写操作。 程序的写操作是调用的safe_write函数,而safe_write函数中对于最终的写操作针对win和linux环境做了区分, 在Win32下,如果是TCP连接则用send函数,如果是非TCP则和非win环境一样使用write函数。如下代码:

#ifdef _WIN32
if (!req->tcp) {
    ret = write(req->fd, ((char*)buf)+n, count-n);
} else {
    ret = send(req->fd, ((char*)buf)+n, count-n, 0);
    if (ret <= 0) {
            errno = WSAGetLastError();
    }
}
#else
ret = write(req->fd, ((char*)buf)+n, count-n);
#endif

在发送了请求的应答后,服务器端将会执行关闭操作,仅限于CGI本身的关闭,程序执行的是fcgi_close函数。 fcgi_close函数在前面提的fcgi_finish_request函数中,在请求应答完后执行。同样,对于win平台和非win平台有不同的处理。 其中对于非win平台调用的是write函数。

以上是一个TCP服务器端实现的简单说明。这只是我们PHP的CGI模式的基础,在这个基础上PHP增加了更多的功能。 在前面的章节中我们提到了每个SAPI都有一个专属于它们自己的sapi_module_struct结构:cgi_sapi_module,其代码定义如下:

/* {{{ sapi_module_struct cgi_sapi_module
 */
static sapi_module_struct cgi_sapi_module = {
"cgi-fcgi",                     /* name */
"CGI/FastCGI",                  /* pretty name */

php_cgi_startup,                /* startup */
php_module_shutdown_wrapper,    /* shutdown */

sapi_cgi_activate,              /* activate */
sapi_cgi_deactivate,            /* deactivate */

sapi_cgibin_ub_write,           /* unbuffered write */
sapi_cgibin_flush,              /* flush */
NULL,                           /* get uid */
sapi_cgibin_getenv,             /* getenv */

php_error,                      /* error handler */

NULL,                           /* header handler */
sapi_cgi_send_headers,          /* send headers handler */
NULL,                           /* send header handler */

sapi_cgi_read_post,             /* read POST data */
sapi_cgi_read_cookies,          /* read Cookies */

sapi_cgi_register_variables,    /* register server variables */
sapi_cgi_log_message,           /* Log message */
NULL,                           /* Get request time */
NULL,                           /* Child terminate */

STANDARD_SAPI_MODULE_PROPERTIES
};
/* }}} */

同样,以读取cookie为例,当我们在CGI环境下,在PHP中调用读取Cookie时, 最终获取的数据的位置是在激活SAPI时。它所调用的方法是read_cookies。

SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);

对于每一个服务器在加载时,我们都指定了sapi_module,在第一小节的Apache模块方式中, sapi_module是apache2_sapi_module,其对应read_cookies方法的是php_apache_sapi_read_cookies函数, 而在我们这里,读取cookie的函数是sapi_cgi_read_cookies。 再次说明定义SAPI结构的理由:统一接口,面向接口的编程,具有更好的扩展性和适应性。

参考资料

  • http://www.fastcgi.com/drupal/node/2
  • http://baike.baidu.com/view/641394.htm

这是TIPI项目第二章第三小节修改后的版本内容,虽然还有一些问题,但是较之前的版本还是有所进步, 至少我们在努力…