PHP - 增强性mhash函数

今天使用php的加密函数mhash
的时候,报错: Fatal error
: Call to undefined function mhash()

mhash是php的内置函数但是使用却报错..

一番研究总结两种方法:

1,导入php_mhash.dll扩展文件,除此之外还要导入 libmhash.dll (mhash 库的载入依赖这个文件) ,

在 Apache 的配置文件 Httpd.conf 中加载   LoadFile C:/php/libmhash.dll” 。

2,使用自定义的mhash增强函数。

function hmac_md5($key, $data)
{
    if (extension_loaded('mhash'))
    {
        return bin2hex(mhash
(MHASH_MD5, $data, $key));
    }

    $b = 64;
    if (strlen($key) > $b)
    {
        $key = pack('H*', md5($key));
    }
    $key  = str_pad($key, $b, chr(0x00));
    $ipad = str_pad('', $b, chr(0x36));
    $opad = str_pad('', $b, chr(0x5c));

    $k_ipad = $key ^ $ipad;
    $k_opad = $key ^ $opad;

    return md5($k_opad . pack('H*', md5($k_ipad . $data)));
}

hmac_md5
函数里的参数$key和$data对应mhash原有的3,2参数。

这两个方法都可以顺利的使用上php
mhash
加密函数

用PHP建立XML-RPC的Web Sevice系統

PHP 中集成了XML-RPC和SOAP两种协议的访问,都是集中在xmlrpc扩展当中。另外,在PHP的PEAR中,不管是 PHP 4还是PHP 5,都已经默认集成了XML-RPC扩展,而且该扩展跟xmlrpc扩展无关,能够独立实现XML-RPC的协议交互,如果没有xmlrpc扩展,建议使用PEAR::XML-RPC扩展。
Web Service介绍

Web Service就是为了异构系统的通信而产生的,它基本的思想就是使用基于XML的HTTP的远程调用提供一种标准的机制,而省去建立一种新协议的需求。目前进行Web Service通信有两种协议标准,一种是XML-RPC,另外一种是SOAP。 XML-RPC比较简单,出现时间比较早,SOAP比较复杂,主要是一些需要稳定、健壮、安全并且复杂交互的时候使用。

我们这里主要是以XML-RPC来简单描述Web Service的交互过程,部分内容来自PHP手册,更详细内容,建议参考手册。

 

安 装xmlrpc扩展

如果你的系统中没有安装xmlrpc的php扩展,那么请正确安装。

在Windows平台下,首先把PHP安装目录下的扩展php_xmlrpc.dll放到C:\Windows或者C:\Winnt 目录下,(PHP4的扩展在C:\php\extensions目录中,PHP5的扩展在C: \php\ext目录中),同时在C:\Windows\php.ini或者C: \Winnt\php.ini中把extension=php_xmlrpc.dll前面的分号";"去掉,然后重启Web服务器后查看phpinfo()有没有XML-RPC项目就能够确定是否已经正确安装xmlrpc扩展。

在Unix/Linux平台下,如果没有安装xmlrpc扩展,请在重新编译PHP,在configure的时候请加入 --with-xmlrpc 选项,然后查看phpinfo()看是否正常安装xmlrpc。

(注意:以下操作都是建立在xmlrpc扩张正常安装前提下,请务必正确安装。)

 

XML-RPC工作原理

XML-RPC大致就是整个过程就是使用XML 来进行通信。首先构造一个RPC 服务器端用来出来从RPC客户端传递过来的使用XML封装的请求,并且把处理结果通过XML的形式返回给RPC客户端,客户端就去分析XML获取自己需要 的数据。

XML-RPC的服务器端必须有现成的函数提供给客户端调用, 并且客户端提交的请求中的函数和方法必须和服务器端的一致,否则将无法获取所需要的结果。

下 面我进行简单的代码来描述整个过程。

 

XML-RPC实践

服务器端使用xmlrpc_server_create函数产生一个服务器端,然后把需要需要暴露的RPC调用接口进行注册,接受 RPC客户端POST过来的XML数据,然后进行处理,处理结果通过XML的形式显示给客户端。

代码如下: rpc_server.php

<?php

/**

* 函数:提供给RPC客户端调用的函数

* 参数:

* $method 客户端需要调用的函数

* $params 客户端需要调用的函数的参数数组

* 返回:返回指定调用结果

*/

function rpc_server_func($method, $params) {

$parameter = $params[0];

if ($parameter == "get"){

$return = ''This data by get method'';

}else{

$return = ''Not specify method or params'';

}

return $return;

}

//产生一个XML-RPC的服务器端

$xmlrpc_server = xmlrpc_server_create();

//注册一个服务器端调用的方法rpc_server,实际指向的是rpc_server_func函数

xmlrpc_server_register_method($xmlrpc_server, "rpc_server", "rpc_server_func");

//接受客户端POST过来的XML数据

$request = $HTTP_RAW_POST_DATA;

// 执行调用客户端的XML请求后获取执行结果

$xmlrpc_response = xmlrpc_server_call_method($xmlrpc_server, $request, null);

//把函数处理后的结果XML进行输出

header(''Content-Type: text/xml'');

echo $xmlrpc_response;

//销毁XML-RPC服务器端资源

xmlrpc_server_destroy($xmlrpc_server);

?>

服务器端构造好了,那么再构造我们的RPC客户端。客户端大致通过Socket访问XML-RPC服务器端的80端口,然后把需要调用的RPC接口封装到XML里,通过POST请求提 交给RPC服务器端,最后获取服务器端返回结果。

代码如下:rpc_client.php

<?php

/**

* 函数:提供给客户端进行连接XML-RPC服务器端的函数

* 参数:

* $host 需要连接的主机

* $port 连接主机的端口

* $rpc_server XML-RPC服务器端文件

* $request 封装的XML请求信息

* 返回:连接成功成功返回由服务器端返回的XML信息,失败返回false

*/

function rpc_client_call($host, $port, $rpc_server, $request) {

//打开指定的服务器端

$fp = fsockopen($host, $port);

//构造需要进行通信的XML-RPC 服务器端的查询POST请求信息

$query = "POST $rpc_server HTTP/1.0\nUser_Agent: XML-RPC
Client\nHost: ".$host."\nContent-Type: text/xml\nContent-Length:
".strlen($request)."\n\n".$request."\n";

//把构造好的HTTP协议发送给服务器,失败返回false

if (!fputs($fp, $query, strlen($query))) {

$errstr = "Write error";

return false;

}

//获取从服务器端返回的所有信息,包括HTTP 头和XML信息

$contents = '''';

while (!feof($fp)){

$contents .= fgets($fp);

}

// 关闭连接资源后返回获取的内容

fclose($fp);

return $contents;

}

//构造连接RPC服务器端的信息

$host = ''localhost'';

$port = 80;

$rpc_server = ''/~heiyeluren/rpc_server.php'';

//把需要发送的XML请求进行编码成XML,需要调用的方法是rpc_server,参数是get

$request = xmlrpc_encode_request(''rpc_server'', ''get'');

//调用rpc_client_call函数把所有请求发送给XML-RPC服务器端后获取信息

$response = rpc_client_call($host, $port, $rpc_server, $request);

//分析从服务器端返回的XML,去掉HTTP头信息,并且把XML 转为PHP能识别的字符串

$split = ''<?phpxml version=?>1.0" encoding="iso-8859-1""'';

$xml = explode($split, $response);

$xml = $split . array_pop($xml);

$response = xmlrpc_decode($xml);

//输出从RPC服务器端获取的信息

print_r($response);

?>

大致我们上面的例子就是提交一个叫做rpc_server的方法过去,参数是get,然后获取服务器端的返回,服务器端返回的XML 数据是:

<?xml version="1.0" encoding="iso-8859-1"?>
<methodResponse>
<params>
<param>
<value>
<string>This data by get method</string>
</value>
</param>
</params>
</methodResponse>

那么我们再通过 xmlrpc_decode函数把这个XML编码为PHP的字符串,我们就能够随意处理了,整个Web Service交互完成。


结束语

不管是XML-RPC也好,SOAP也罢, 只要能够让我们稳定、安全的进行远程过程的调用,完成我们的项目,那么就算整个Web Service就是成功的。另外,如果可以的话,也可以尝试使用PEAR中的XML-RPC来实现上面类似的操作,说不定会更简单,更适合你使 用。

简单的使用XML-RPC进行Web Service交互就完成了,部分代码参考PHP手册,想获取详细信息建议参考手册,如果文章有不正确,请指正。

PHP断点续传的实现


$fname = './MMLDZG.mp3';  
$fp = fopen($fname,'rb');  
$fsize = filesize($fname);  
if (isset($_SERVER['HTTP_RANGE']) && ($_SERVER['HTTP_RANGE'] != "") && preg_match("/^bytes=([0-9]+)-$/i", $_SERVER['HTTP_RANGE'], $match) && ($match[1] < $fsize)) {
    $start = $match[1];
} else {
    $start = 0;
}
@header("Cache-control: public"); @header("Pragma: public");
if ($star--> 0) {  
    fseek($fp, $start);  
    Header("HTTP/1.1 206 Partial Content");  
    Header("Content-Length: " . ($fsize - $start));  
    Header("Content-Ranges: bytes" . $start . "-" . ($fsize - 1) . "/" . $fsize);  
} else {  
    header("Content-Length: $fsize");  
    Header("Accept-Ranges: bytes");  
}  
@header("Content-Type: application/octet-stream");  
@header("Content-Disposition: attachment;filename=mmdld.mp3");  
fpassthru($fp);  
 

fpassthru() 函数输出文件指针处的所有剩余数据。

该函数将给定的文件指针从当前的位置读取到 EOF,并把结果写到输出缓冲区。

php 字母大小写转换总结

1.将字符转换成小写

strtolower(): 该函数将传入的字符参数所有的字符转换成小写,并以小定形式放回这个字符

 

2.将字符转成大写

strtoupper(): 该函数的作用同strtolower函数相反,是将传入的字符参数字符全部转换大写,并以大写形式返回这个字符串.用法strtolowe()一 样.

 

3.将字符串首字符转换大写

ucfirst(): 该函数的作用是将字符串的第一个字符改成大写,该函数返回字符大写字符串.用法strtolowe()一样.

 


4.将字符串每个单词的首字符转换大写

ucwords(): 该函数将传入的字符串的每个单词的首字符变成大写.如"hello world",经过该函数处理后,将返回"Hello Word".用法strtolowe()一样.

get_magic_quotes_gpc()、set_magic_quotes_runtime()和get_magic_quotes_runtime()


1、PHP中set_magic_quotes_runtime()函数的作用: 
函数来修改PHP.ini文件中的 magic_quotes_runtime变量状态,如果想获得magic_quotes_runtime变量状态用get_magic_quotes_runtime这个函数如果返回0表示本功能被关闭,如果返回1表示本功能已经开启。 magic_quotes_runtime功能是当它被开启的时候所有外部引入的数据资料或者文件等等都会自动转为含有反斜线溢出字符资料。比如:用户数据库提交的数据中含有\" '这些符号的时候它就会在这些符号的前面自动加上"\"转义符。 
这个属性在PHP4以前的版本都是默认关闭的,PHP4.0以后的版本如果程序要用到将它关闭的时候直接写成set_magic_quotes_runtime(0)将其关闭。

2.get_magic_quotes_gpc函数作用:

函数取得 PHP 环境配置变量 magic_quotes_gpc (GPC, Get/Post/Cookie) 值。返回 0 表示关闭本功能返回 1 表示本功能打开。当

magic_quotes_gpc 打开时,所有的双引号), \ (反斜线) and 空字符会自动加上转义符\;

默认情况下,PHP 指令 magic_quotes_gpc 为 on,它主要是对所有的 GET、POST 和 COOKIE 数据自动运行 addslashes()。不要对已经被 magic_quotes_gpc转义过的字符使用 addslashes(),因为这样会导致双转义。遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。

 

当然,如果php.ini中的magic_quotes_gpc项为On,但有时又不想转义某项的特殊字符,可以使用stripslashes()去掉其中的 \

 

使用get_magic_quotes_gpc特别需要注意的一点是,get_magic_quotes_gpc对$_REQUEST的变量不起作用,也就是说即使get_magic_quotes_gpc的状态开启,$_REQUEST的变量不会自动转义

其实这个函数就是判断有PHP有没有自动调用addslashes 这个函数,

 

区别

1.magic_quotes_runtime 可以通过set_magic_quotes_runtime()和 get_magic_quotes_runtime() 来进行设置和读取 ; magic_quotes_gpc() 只可以通过get_magic_quotes_gpc()来读取 没有set_magic_quotes_gpc()这样的函数 也不能通过ini_set('magic_quotes_gpc',1) 这样来设置 他只能通过手动在php.ini文件中修改;

2 magic_quotes_gpc 处理的是post \get\ie 传递过来的内容【相反的函数是:stripslashes(); 把 '/' 干掉】

magic_quotes_rumtime 处理的是数据库 或文件中的内容

set_magic_quotes_runtime() 和 get_magic_quotes_runtime() 

代码

  if ((int)get_magic_quotes_gpc() > 0) {

    $in = array(&$_GET, &$_POST, &$_COOKIE);

    while (list($k, $v) = each($in)) {

      foreach ($v as $key => $val) {

        if (!is_array($val)) {

          $in[$k][$key] = stripslashes($val);

          continue;

        }

        $in[] =& $in[$k][$key];

      }

    }

    unset($in);

    unset($k);

    unset($v);

    unset($key);

    unset($val);

  }


 

PHP autoload和spl_autoload自动加载机制详解

 

 

PHP autoload机制详解

(1) autoload机制概述

使用PHP的OO模式开发系统时,通常大家习惯上将每个的实现都存放在一个单独的文件里,这样会很容易实现对进行复用,同时将来维护时也很便利。这 也是OO设计的基本思想之一。在PHP5之前,如果需要使用一个,只需要直接使用include/require将其包含进来即可。下面是一个实际的例 子:

/* Person.class.php */
<?php
class Person {
var $name, $age;

function __construct ($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
?>

/* no_autoload.php */
<?php
require_once (”Person.class.php”);

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

在这个例子中,no-autoload.php文件需要使用Person,它使用了require_once将其包含,然后就可以直接使用Person实例化一个对象。

但 随着项目规模的不断扩大,使用这种方式会带来一些隐含的问题:如果一个PHP文件需要使用其它,那么就需要很的require/include 句,这样有可能会造成遗漏或者包含进不必要文件。如果大量的文件都需要使用其它的,那么要保证每个文件都包含正确的文件肯定是一个噩梦。

PHP5为这个问题提供了一个解决方案,这就是的自动装载(autoload)机制。autoload机制可以使得PHP程序有可能在使用时才自动包含文件,而不是一开始就将所有的文件include进来,这种机制也称为lazy loading。

下面是使用autoload机制加载Person的例子:

/* autoload.php */
<?php
function __autoload($classname) {
require_once ($classname . “class.php”);
}

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

通常PHP5在使用一个时,如果发现这个没有加载,就会自动运行__autoload()函数,在这个函数中我们可以加载需要使用。在我们这个简 单的例子中,我们直接将名加上扩展名" title="扩展名">扩展名”.class.php”构成了文件名,然后使用require_once将其加载。从这个例子中,我们可以看出 autoload至少要做三件事情,第一件事是根据名确定文件名,第二件事是确定文件所在的磁盘路径(在我们的例子是最简单的情况,调用它们的 PHP程序文件在同一个文件夹下),第三件事是将从磁盘文件加载系统中。第三步最简单,只需要使用include/require即可。要实现第一 步,第二步的功能,必须在开发时约定名与磁盘文件的映射方法,只有这样我们才能根据名找到它对应的磁盘文件

因 此,当有大量的文件要包含的时候,我们只要确定相应的规则,然后在__autoload()函数中,将名与实际的磁盘文件对应起来,就可以实现 lazy loading的效果。从这里我们也可以看出__autoload()函数的实现中最重要的是名与实际的磁盘文件映射规则的实现。

但现在问题来了,如果在一个系统的实现中,如果需要使用其它的库,这些库可能是由不同的开发人员编写的,其名与实际的磁盘文件的映射规则不尽相 同。这时如果要实现文件的自动加载,就必须在__autoload()函数中将所有的映射规则全部实现,这样的话__autoload()函数有可能 会非常复杂,甚至无法实现。最后可能会导致__autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。在 这种情况下,难道就没有更简单清晰的解决办法了吧?答案当然是:NO! 在看进一步的解决方法之前,我们先来看一下PHP中的autoload机制是如何实现的。

(2) PHP的autoload机制的实现

我 们知道,PHP文件的执行分为两个独立的过程,第一步是将PHP文件编译成普通称之为OPCODE的字节序列(实际上是编译成一个叫做 zend_op_array字节数组),第二步是由一个虚拟机来执行这些OPCODE。PHP的所有行为都是由这些OPCODE来实现的。因此,为了研 究PHP中autoload的实现机制,我们将autoload.php文件编译成opcode,然后根据这些OPCODE来研究PHP在这过程中都做了 些什么:

/* autoload.php 编译后的OPCODE列表,是使用作者开发的OPDUMP工具
* 生成的结果,可以到网站 http://www.phpinternals.com/ 下载软件
*/
1: <?php
2: // require_once (”Person.php”);
3: 
4: function __autoload ($classname) {
0 NOP                
0 RECV                1
5:    if (!class_exists($classname)) {
1 SEND_VAR            !0
2 DO_FCALL            ‘class_exists’ [extval:1]
3 BOOL_NOT            $0 =>RES[~1]     
4 JMPZ                ~1, ->8
6:     require_once ($classname. “.class.php”);
5 CONCAT              !0, ‘.class.php’ =>RES[~2]     
6 INCLUDE_OR_EVAL     ~2, REQUIRE_ONCE
7:    }
7 JMP                 ->8
8: }
8 RETURN              null
9: 
10: $p = new Person(’Fred’, 35);
1 FETCH_CLASS         ‘Person’ =>RES[:0]     
2 NEW                 :0 =>RES[$1]     
3 SEND_VAL            ‘Fred’
4 SEND_VAL            35
5 DO_FCALL_BY_NAME     [extval:2]
6 ASSIGN              !0, $1
11: 
12: var_dump ($p);
7 SEND_VAR            !0
8 DO_FCALL            ‘var_dump’ [extval:1]
13: ?>

在 autoload.php的第10行代码中我们需要为Person实例化一个对象。因此autoload机制一定会在该行编译后的opcode中有所体 现。从上面的第10行代码生成的OPCODE中我们知道,在实例化对象Person时,首先要执行FETCH_CLASS指令。我们就从PHP对 FETCH_CLASS指令的处理过程开始我们的探索之旅。

通过查阅PHP的源代码(我使用的是PHP 5.3alpha2版本)可以发现如下的调用序列

ZEND_VM_HANDLER(109, ZEND_FETCH_CLASS, &hellip;) (zend_vm_def.h 1864行)
=> zend_fetch_class (zend_execute_API.c 1434行)
=>zend_lookup_class_ex (zend_execute_API.c 964行)
=> zend_call_function(&fcall_info, &fcall_cache) (zend_execute_API.c 1040行)

在最后一步的调用之前,我们先看一下调用时的关键参数

/* 设置autoload_function变量值为”__autoload” */
fcall_info.function_name = &autoload_function;   // Ooops, 终于发现”__autoload”了
&hellip;
fcall_cache.function_handler = EG(autoload_func); // autoload_func !

zend_call_function 是Zend Engine中最重要的函数之一,其主要功能是执行用户在PHP程序中自定义的函数或者PHP本身的库函数。zend_call_function有两个 重要的指针形参数fcall_info, fcall_cache,它们分别指向两个重要的结构,一个是zend_fcall_info, 另一个是zend_fcall_info_cache。zend_call_function主要工作流程如下:如果 fcall_cache.function_handler指针为NULL,则尝试查找函数名为fcall_info.function_name的函 数,如果存在的话,则执行之;如果fcall_cache.function_handler不为NULL,则直接执行 fcall_cache.function_handler指向的函数

现在我们清楚了,PHP在实例化一个 对象时(实际上在实现接口,使用常数或中的静态变量调用中的静态方法都会如此),首先会在系统查找(或接口)是否存在,如果不存在的话就 尝试使用autoload机制加载。而autoload机制的主要执行过程为:

(1) 检查执行器全局变量函数指针autoload_func是否为NULL。
(2) 如果autoload_func==NULL, 则查找系统中是否定义有__autoload()函数,如果没有,则报告错误并退出。
(3) 如果定义了__autoload()函数,则执行__autoload()尝试加载,并返回加载结果。
(4) 如果autoload_func不为NULL,则直接执行autoload_func指针指向的函数用来加载。注意此时并不检查__autoload()函数是否定义。

真 相终于大白,PHP提供了两种方法来实现自动装载机制,一种我们前面已经提到过,是使用用户定义的__autoload()函数,这通常在PHP源程序中 来实现;另外一种就是设计一个函数,将autoload_func指针指向它,这通常使用C言在PHP扩展中实现。如果既实现了 __autoload()函数,又实现了autoload_func(将autoload_func指向某一PHP函数),那么只执行 autoload_func函数

(3) SPL autoload机制的实现

SPL 是Standard PHP Library(标准PHP库)的缩写。它是PHP5引入的一个扩展库,其主要功能包括autoload机制的实现及包括各种Iterator接口或。 SPL autoload机制的实现是通过将函数指针autoload_func指向自己实现的具有自动装载功能函数来实现的。SPL有两个不同的函数 spl_autoload, spl_autoload_call,通过将autoload_func指向这两个不同的函数地址来实现不同的自动加载机制

spl_autoload 是SPL实现的默认的自动加载函数,它的功能比较简单。它可以接收两个参数,第一个参数是$class_name,表示名,第二个参 数$file_extensions是可选的,表示文件扩展名" title="扩展名">扩展名,可以在$file_extensions中指定扩展名" title="扩展名">扩展名,护展名之间用分号隔开即 可;如果不指定的话,它将使用默认的扩展名" title="扩展名">扩展名.inc或.php。spl_autoload首先将$class_name变为小写,然后在所有的 include path中搜索$class_name.inc或$class_name.php文件(如果不指定$file_extensions参数的话),如果找 到,就加载文件。你可以手动使用spl_autoload(”Person”, “.class.php”)来加载Person。实际上,它跟require/include差不,不同的它可以指定扩展名" title="扩展名">扩展名。

怎 样让spl_autoload自动起作用呢,也就是将autoload_func指向spl_autoload?答案是使用 spl_autoload_register函数。在PHP脚本中第一次调用spl_autoload_register()时不使用任何参数,就可以将 autoload_func指向spl_autoload

通过上面的说明我们知道,spl_autoload功能比较简单,而且它是在SPL扩展中实现的,我们无法扩充它的功能。如果想实现自己的更灵活的自动加载机制怎么办呢?这时,spl_autoload_call函数闪亮登场了。

我 们先看一下spl_autoload_call的实现有何奇妙之处。在SPL模块内部,有一个全局变量autoload_functions,它本质上是 一个HashTable,不过我们可以将其简单的看作一个链表,链表中的每一个元素都是一个函数指针,指向一个具有自动加载功能函数。 spl_autoload_call本身的实现很简单,只是简单的按顺序执行这个链表中每个函数,在每个函数执行完成后都判断一次需要的是否已经加载, 如果加载成功就直接返回,不再继续执行链表中的其它函数。如果这个链表中所有的函数都执行完成后还没有加载,spl_autoload_call就直接 退出,并不向用户报告错误。因此,使用了autoload机制,并不能保证就一定能正确的自动加载,关键还是要看你的自动加载函数如何实现。

那 么自动加载函数链表autoload_functions是谁来维护呢?就是前面提到的spl_autoload_register函数。它可以将用户定 义的自动加载函数注册到这个链表中,并将autoload_func函数指针指向spl_autoload_call函数(注意有一种情况例外,具体是哪 种情况留给大家思考)。我们也可以通过spl_autoload_unregister函数将已经注册的函数从autoload_functions链表 中删除。

上节说过,当autoload_func指针非空时,就不会自动执行__autoload()函数 了,现在autoload_func已经指向了spl_autoload_call,如果我们还想让__autoload()函数起作用应该怎么办呢?当 然还是使用spl_autoload_register(__autoload)调用将它注册到autoload_functions链表中。

现在回到第一节最后的问题,我们有了解决方案:根据每个库不同的命名机制实现各自的自动加载函数,然后使用spl_autoload_register分别将其注册到SPL自动加载函数队列中就可了。这样我们就不用维护一个非常复杂的__autoload函数了。

(4) autoload效率问题及对策

使 用autoload机制时,很人的第一反应就是使用autoload会降低系统效率,甚至有人干脆提议为了效率不要使用autoload。在我们了解了 autoload实现的原理后,我们知道autoload机制本身并不是影响系统效率原因,甚至它还有可能提高系统效率,因为它不会将不需要的加载系统中。

那么为什么很人都有一个使用autoload会降低系统效率的印象呢?实际上,影响autoload机制效率本身恰恰是用户设计的自动加载函数。如果它 不能高效的将名与实际的磁盘文件(注意,这里指实际的磁盘文件,而不仅仅是文件名)对应起来,系统将不得不做大量的文件是否存在(需要在每个 include path中包含的路径中去寻找)的判断,而判断文件是否存在需要做磁盘I/O操作,众所周知磁盘I/O操作的效率很低,因此这才是使得autoload机 制效率降低的罪魁祸首!

因此,我们在系统设计时,需要定义一套清晰的将名与实际磁盘文件映射的机制。这个规则越简单越明确,autoload机制效率就越高。

结论:autoload机制并不是天然的效率低下,只有滥用autoload,设计不好的自动装载函数才会导致其效率的降低。

 

 

PHP autoload机制详解

(1) autoload机制概述

使用PHP的OO模式开发系统时,通常大家习惯上将每个的实现都存放在一个单独的文件里,这样会很容易实现对进行复用,同时将来维护时也很便利。这 也是OO设计的基本思想之一。在PHP5之前,如果需要使用一个,只需要直接使用include/require将其包含进来即可。下面是一个实际的例 子:

/* Person.class.php */
<?php
class Person {
var $name, $age;

function __construct ($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
?>

/* no_autoload.php */
<?php
require_once (”Person.class.php”);

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

在这个例子中,no-autoload.php文件需要使用Person,它使用了require_once将其包含,然后就可以直接使用Person实例化一个对象。

但 随着项目规模的不断扩大,使用这种方式会带来一些隐含的问题:如果一个PHP文件需要使用其它,那么就需要很的require/include 句,这样有可能会造成遗漏或者包含进不必要文件。如果大量的文件都需要使用其它的,那么要保证每个文件都包含正确的文件肯定是一个噩梦。

PHP5为这个问题提供了一个解决方案,这就是的自动装载(autoload)机制。autoload机制可以使得PHP程序有可能在使用时才自动包含文件,而不是一开始就将所有的文件include进来,这种机制也称为lazy loading。

下面是使用autoload机制加载Person的例子:

/* autoload.php */
<?php
function __autoload($classname) {
require_once ($classname . “class.php”);
}

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

通常PHP5在使用一个时,如果发现这个没有加载,就会自动运行__autoload()函数,在这个函数中我们可以加载需要使用。在我们这个简 单的例子中,我们直接将名加上扩展名" title="扩展名">扩展名”.class.php”构成了文件名,然后使用require_once将其加载。从这个例子中,我们可以看出 autoload至少要做三件事情,第一件事是根据名确定文件名,第二件事是确定文件所在的磁盘路径(在我们的例子是最简单的情况,调用它们的 PHP程序文件在同一个文件夹下),第三件事是将从磁盘文件加载系统中。第三步最简单,只需要使用include/require即可。要实现第一 步,第二步的功能,必须在开发时约定名与磁盘文件的映射方法,只有这样我们才能根据名找到它对应的磁盘文件

因 此,当有大量的文件要包含的时候,我们只要确定相应的规则,然后在__autoload()函数中,将名与实际的磁盘文件对应起来,就可以实现 lazy loading的效果。从这里我们也可以看出__autoload()函数的实现中最重要的是名与实际的磁盘文件映射规则的实现。

但现在问题来了,如果在一个系统的实现中,如果需要使用其它的库,这些库可能是由不同的开发人员编写的,其名与实际的磁盘文件的映射规则不尽相 同。这时如果要实现文件的自动加载,就必须在__autoload()函数中将所有的映射规则全部实现,这样的话__autoload()函数有可能 会非常复杂,甚至无法实现。最后可能会导致__autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。在 这种情况下,难道就没有更简单清晰的解决办法了吧?答案当然是:NO! 在看进一步的解决方法之前,我们先来看一下PHP中的autoload机制是如何实现的。

(2) PHP的autoload机制的实现

我 们知道,PHP文件的执行分为两个独立的过程,第一步是将PHP文件编译成普通称之为OPCODE的字节序列(实际上是编译成一个叫做 zend_op_array字节数组),第二步是由一个虚拟机来执行这些OPCODE。PHP的所有行为都是由这些OPCODE来实现的。因此,为了研 究PHP中autoload的实现机制,我们将autoload.php文件编译成opcode,然后根据这些OPCODE来研究PHP在这过程中都做了 些什么:

/* autoload.php 编译后的OPCODE列表,是使用作者开发的OPDUMP工具
* 生成的结果,可以到网站 http://www.phpinternals.com/ 下载软件
*/
1: <?php
2: // require_once (”Person.php”);
3: 
4: function __autoload ($classname) {
0 NOP                
0 RECV                1
5:    if (!class_exists($classname)) {
1 SEND_VAR            !0
2 DO_FCALL            ‘class_exists’ [extval:1]
3 BOOL_NOT            $0 =>RES[~1]     
4 JMPZ                ~1, ->8
6:     require_once ($classname. “.class.php”);
5 CONCAT              !0, ‘.class.php’ =>RES[~2]     
6 INCLUDE_OR_EVAL     ~2, REQUIRE_ONCE
7:    }
7 JMP                 ->8
8: }
8 RETURN              null
9: 
10: $p = new Person(’Fred’, 35);
1 FETCH_CLASS         ‘Person’ =>RES[:0]     
2 NEW                 :0 =>RES[$1]     
3 SEND_VAL            ‘Fred’
4 SEND_VAL            35
5 DO_FCALL_BY_NAME     [extval:2]
6 ASSIGN              !0, $1
11: 
12: var_dump ($p);
7 SEND_VAR            !0
8 DO_FCALL            ‘var_dump’ [extval:1]
13: ?>

在 autoload.php的第10行代码中我们需要为Person实例化一个对象。因此autoload机制一定会在该行编译后的opcode中有所体 现。从上面的第10行代码生成的OPCODE中我们知道,在实例化对象Person时,首先要执行FETCH_CLASS指令。我们就从PHP对 FETCH_CLASS指令的处理过程开始我们的探索之旅。

通过查阅PHP的源代码(我使用的是PHP 5.3alpha2版本)可以发现如下的调用序列

ZEND_VM_HANDLER(109, ZEND_FETCH_CLASS, &hellip;) (zend_vm_def.h 1864行)
=> zend_fetch_class (zend_execute_API.c 1434行)
=>zend_lookup_class_ex (zend_execute_API.c 964行)
=> zend_call_function(&fcall_info, &fcall_cache) (zend_execute_API.c 1040行)

在最后一步的调用之前,我们先看一下调用时的关键参数

/* 设置autoload_function变量值为”__autoload” */
fcall_info.function_name = &autoload_function;   // Ooops, 终于发现”__autoload”了
&hellip;
fcall_cache.function_handler = EG(autoload_func); // autoload_func !

zend_call_function 是Zend Engine中最重要的函数之一,其主要功能是执行用户在PHP程序中自定义的函数或者PHP本身的库函数。zend_call_function有两个 重要的指针形参数fcall_info, fcall_cache,它们分别指向两个重要的结构,一个是zend_fcall_info, 另一个是zend_fcall_info_cache。zend_call_function主要工作流程如下:如果 fcall_cache.function_handler指针为NULL,则尝试查找函数名为fcall_info.function_name的函 数,如果存在的话,则执行之;如果fcall_cache.function_handler不为NULL,则直接执行 fcall_cache.function_handler指向的函数

现在我们清楚了,PHP在实例化一个 对象时(实际上在实现接口,使用常数或中的静态变量调用中的静态方法都会如此),首先会在系统查找(或接口)是否存在,如果不存在的话就 尝试使用autoload机制加载。而autoload机制的主要执行过程为:

(1) 检查执行器全局变量函数指针autoload_func是否为NULL。
(2) 如果autoload_func==NULL, 则查找系统中是否定义有__autoload()函数,如果没有,则报告错误并退出。
(3) 如果定义了__autoload()函数,则执行__autoload()尝试加载,并返回加载结果。
(4) 如果autoload_func不为NULL,则直接执行autoload_func指针指向的函数用来加载。注意此时并不检查__autoload()函数是否定义。

真 相终于大白,PHP提供了两种方法来实现自动装载机制,一种我们前面已经提到过,是使用用户定义的__autoload()函数,这通常在PHP源程序中 来实现;另外一种就是设计一个函数,将autoload_func指针指向它,这通常使用C言在PHP扩展中实现。如果既实现了 __autoload()函数,又实现了autoload_func(将autoload_func指向某一PHP函数),那么只执行 autoload_func函数

(3) SPL autoload机制的实现

SPL 是Standard PHP Library(标准PHP库)的缩写。它是PHP5引入的一个扩展库,其主要功能包括autoload机制的实现及包括各种Iterator接口或。 SPL autoload机制的实现是通过将函数指针autoload_func指向自己实现的具有自动装载功能函数来实现的。SPL有两个不同的函数 spl_autoload, spl_autoload_call,通过将autoload_func指向这两个不同的函数地址来实现不同的自动加载机制

spl_autoload 是SPL实现的默认的自动加载函数,它的功能比较简单。它可以接收两个参数,第一个参数是$class_name,表示名,第二个参 数$file_extensions是可选的,表示文件扩展名" title="扩展名">扩展名,可以在$file_extensions中指定扩展名" title="扩展名">扩展名,护展名之间用分号隔开即 可;如果不指定的话,它将使用默认的扩展名" title="扩展名">扩展名.inc或.php。spl_autoload首先将$class_name变为小写,然后在所有的 include path中搜索$class_name.inc或$class_name.php文件(如果不指定$file_extensions参数的话),如果找 到,就加载文件。你可以手动使用spl_autoload(”Person”, “.class.php”)来加载Person。实际上,它跟require/include差不,不同的它可以指定扩展名" title="扩展名">扩展名。

怎 样让spl_autoload自动起作用呢,也就是将autoload_func指向spl_autoload?答案是使用 spl_autoload_register函数。在PHP脚本中第一次调用spl_autoload_register()时不使用任何参数,就可以将 autoload_func指向spl_autoload

通过上面的说明我们知道,spl_autoload功能比较简单,而且它是在SPL扩展中实现的,我们无法扩充它的功能。如果想实现自己的更灵活的自动加载机制怎么办呢?这时,spl_autoload_call函数闪亮登场了。

我 们先看一下spl_autoload_call的实现有何奇妙之处。在SPL模块内部,有一个全局变量autoload_functions,它本质上是 一个HashTable,不过我们可以将其简单的看作一个链表,链表中的每一个元素都是一个函数指针,指向一个具有自动加载功能函数。 spl_autoload_call本身的实现很简单,只是简单的按顺序执行这个链表中每个函数,在每个函数执行完成后都判断一次需要的是否已经加载, 如果加载成功就直接返回,不再继续执行链表中的其它函数。如果这个链表中所有的函数都执行完成后还没有加载,spl_autoload_call就直接 退出,并不向用户报告错误。因此,使用了autoload机制,并不能保证就一定能正确的自动加载,关键还是要看你的自动加载函数如何实现。

那 么自动加载函数链表autoload_functions是谁来维护呢?就是前面提到的spl_autoload_register函数。它可以将用户定 义的自动加载函数注册到这个链表中,并将autoload_func函数指针指向spl_autoload_call函数(注意有一种情况例外,具体是哪 种情况留给大家思考)。我们也可以通过spl_autoload_unregister函数将已经注册的函数从autoload_functions链表 中删除。

上节说过,当autoload_func指针非空时,就不会自动执行__autoload()函数 了,现在autoload_func已经指向了spl_autoload_call,如果我们还想让__autoload()函数起作用应该怎么办呢?当 然还是使用spl_autoload_register(__autoload)调用将它注册到autoload_functions链表中。

现在回到第一节最后的问题,我们有了解决方案:根据每个库不同的命名机制实现各自的自动加载函数,然后使用spl_autoload_register分别将其注册到SPL自动加载函数队列中就可了。这样我们就不用维护一个非常复杂的__autoload函数了。

(4) autoload效率问题及对策

使 用autoload机制时,很人的第一反应就是使用autoload会降低系统效率,甚至有人干脆提议为了效率不要使用autoload。在我们了解了 autoload实现的原理后,我们知道autoload机制本身并不是影响系统效率原因,甚至它还有可能提高系统效率,因为它不会将不需要的加载系统中。

那么为什么很人都有一个使用autoload会降低系统效率的印象呢?实际上,影响autoload机制效率本身恰恰是用户设计的自动加载函数。如果它 不能高效的将名与实际的磁盘文件(注意,这里指实际的磁盘文件,而不仅仅是文件名)对应起来,系统将不得不做大量的文件是否存在(需要在每个 include path中包含的路径中去寻找)的判断,而判断文件是否存在需要做磁盘I/O操作,众所周知磁盘I/O操作的效率很低,因此这才是使得autoload机 制效率降低的罪魁祸首!

因此,我们在系统设计时,需要定义一套清晰的将名与实际磁盘文件映射的机制。这个规则越简单越明确,autoload机制效率就越高。

结论:autoload机制并不是天然的效率低下,只有滥用autoload,设计不好的自动装载函数才会导致其效率的降低。