奶瓶(NP博士)的好文,真的很实在

村里版主奶瓶的好文,留下来记念他老人家。

这里所说的“大型”应用不是说像Google、eBay、Yahoo这类大型网站的具体实施,我也没有意图劝说读者放弃自己的概念和信仰,只是希望大家的系统可以运行得更快更流畅,可以承载更多的用户在线,希望可以给php的初学者一点帮助。
    关于PHP的执行效率,网上的专题文章很多,多以PHP、Java几个阵营的争论开始,以一个不确定的期待结束,很少看见一个明确的结论。确实,程序的执行效率是很难从比较中得出的。应用的方面不同,执行环境不同,效率的差别会差得比较大。而且效率也是需要权衡的,大家都知道汇编语言很底层,可以写出非常高效的程序,但是我还很少,应该说是几乎没看过有人用汇编做Web开发,而且有能力用汇编写出高效程序的人似乎都是值得大家仰视的,哈哈~我们没有必要去讨论PHP和汇编到底差多少,只要知道自己的PHP和别人的PHP差多少就可以了。
   
    首先,先要明确这篇文章的前提:必须有一台或更多的可以被自己操纵的服务器,而不是虚拟主机空间。毕竟可以在虚拟主机上运行的通用系统已经有了很多经典的作品和成熟的框架,效率挖掘已经被前辈们做得非常出色了,它们的很多理念也被现在很多PHP用户继承和发展,越来越多的所谓“框架”也像满天繁星一样,我也不想再去写那个,因为第一我自己的水平也不怎么样,写不出什么新鲜玩意来,写出来也招人笑,第二是写这个的文章太多了,说法也太多了,混乱是造成很多富有激情的未来天才程序员夭折的最大元凶。
    在独立服务器上执行的程序和在虚拟主机上可以运行的程序在效率优化方面有着很大差别。您当然可以把一套discuz不加修改地安装在一台甚至一堆独立服务器上,不过,它真的得到最大的性能优化吗,您真的对得起这一堆服务器吗?
独立服务器指的是,使用者对这台机器有完全的控制权,包括安装、删除软件,配置系统参数甚至修改源代码。基于这样一个开放的硬件平台,性能也不仅仅是体现在速度上,还包括安全性、稳定性等。和虚拟主机不同,用户必须自己配置Web服务器参数,安装和配置PHP、数据库,以及安装各种乱七八糟的东西(我喜欢这么说),当然还要对它们负责。

    首先提出几个名词:执行时间、模板、数据库封装、Cache、Buffer、Hash、守护进程、crontab。
    执行时间,谁都知道,就是一个程序从执行开始到执行结束所用的时间。因为Web是瞬时的、无状态的,所以执行时间是Web程序执行效率的一个指标,它并不适合衡量C/S程序或者后台守护的程序,因为它们很多都是持续运行的。页面执行时间的一个典型例子就是Discuz论坛页面最下方的时间显式,通常Discuz都是几毫秒到几十毫秒,和所用的平台、数据量和当前系统压力有关。
模板大家再熟悉不过,虽然有很多人只是在用,但是不知道为什么在用。模板在传统上来说是划分逻辑层的一种途径,在MVC上结构里,它把表示层和下层分离,在实际使用中,它方便程序员和界面设计人员分工合作。然而,现在很多场合中,由于模板的不当使用,它非但没有起到促进程序员和界面设计人员分工合作,反倒成为程序员和美工互相仇视的罪魁(我好像在以前的帖子里这样说过),很多人在抱怨他们不得不花很多时间在整理模板上。
    数据库封装似乎和Java的关系更大,它对多种数据库系统提供一个统一调用接口,通常是一些封装好的类,这些类有时也完成一些比如SQL检查、过滤等工作。PHPLIB里的DB封装、PEAR DB、Adodb等都很有名,用的人也很多。
Cache和Buffer看起来好像是一种东西,Cache叫做缓存而Buffer叫做缓冲。在硬件概念中,Cache的用途是连接两种速度不同的设备,比如寄存器和内存、CPU和PCI-Bus、IDE总线和硬盘。Buffer的原意是类似弹簧的一种缓冲器,用来减轻或吸收冲击的震动的东西。Buffer是一种数据预存取的方式,它用于临时存储数据并以与接收速度不同的速度传输。Buffer的更新方式可以是按时间间隔自动刷新,而Cache则更讲究“命中率”,将当前时间段使用频繁的少量数据放到高速设备中方便读写。在程序开发中,固然没有什么高速、低速设备,不过数据源是可以有不同读写效率的。对于少量数据,文本文件的读写通常就要比数据库存取效率好,而同样是文本文件读写,在tmpfs上的效率就要比直接的磁盘IO效率好。Buffer更多地体现在进程通信和队列上,很多时候并不是因为接收方没有能力更快地读取,而是没有必要更快地读取。
    守护进程是一种在后台连续执行的程序,它通常是起到监视、控制流程、对外提供服务等作用。比如Apache本身就可以被理解成一个守护进程,虽然它实际上是由很多个经常更新的进程组成(主进程是固定的)。
    Crontab是UNIX/Linux的定时程序,有点像Windows的“计划任务”,它设定在多少个时间间隔后或者是某一个时间点执行特定的程序。它通常用来完成自动更新、清除临时数据等一段时间自动执行一次的操作。
    另外一个比较特别的概念(说它特别是相对于习惯了通用系统开发的人来说),是当我们拥有了一台独立的服务器之后,完全没必要把自己局限在PHP所能提供的功能范围内,当我们不知不觉地成为系统的主人后,要努力发现到这一点,我们有很多东西可以用的。PHP不是万能的(这简直是一定的),对于它的功能上的不足,完全可以用Perl来弥补,Perl做为一种通用语言,可以提供更多的功能选择,砂砾一样密的模块给这个随意得有些变态的语言提供了无穷的能量。对于PHP性能上的不足,完全可以用C来补充。PHP的根本就是由C继承来,PHP本身也是由C开发,用C来做PHP的扩展是完全合理的。
Linux本身就是由C和Perl在支撑(我这样说完全不是为了夸大Perl的地位,大家可以去看看一个标准的Linux中有多少Perl脚本,离开Perl之后这个系统是不是觉得像个残疾人)。PHP从C中继承了大部分的语法,从Perl中学习了大部分Web特性、函数和那个貌似与开源很矛盾的“$”符号(PHP早期就是一个Perl脚本)。

    我发现我很能写废话,哈哈……
    下面来分析我在使用的一些代码(注:Linux独立服务器适用。我好像已经放弃对Windows和虚拟主机做大型开发很长时间了)。里面使用了一些也许很熟悉也许很陌生也许很变态的方法。我的系统是RedHat AS3,没有什么特别的,PHP版本是4.4.0,MySQL是4.1。我从来没有刻意地去写一些必须用到PHP5的新特性的代码,除非真的必须用到。
    我的Web根目录在/www下,Apache、PHP都是默认安装在/usr/local/下,MySQL是下载的编译好的二进制版本,我也一样把它丢在那里。因为只是用于测试,我不想它看起来很乱,至于在实际项目中,尤其是多台服务器的情况下,需要好好地部署一下你的系统。
    为了使系统的结构清晰一些,我把需要使用的文件都放在了二级目录下面。
    下面是通用头文件/includes/kernel/common.inc.php的一些片断:


<?php
if (!defined('IN_BSG')) {
     exit;
}
?>

上面的代码保证它只能被合法的程序所调用,而不会被其它的文件include。如果正在执行的程序没有定义一个'IN_BSG'常量,它在include这个common.inc.php之后程序会终止。


<?php
list($usec, $sec) = explode(" ", microtime());
$page_time_start = $usec + $sec;
?>


这两行大家可能都会比较熟悉,这是计算程序的开始执行时间的。在程序结束之前,还会再计算一下这个,为的是得出程序执行所耗费的时间。如果你不在意这些,可以放心地把它注释掉。


<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE); // This will NOT report uninitialized variables
//error_reporting(E_ALL);
set_magic_quotes_runtime(0);


// Be paranoid with passed vars
if (@ini_get('register_globals')) {
     foreach ($_REQUEST as $var_name => $void) {
             unset(${$var_name});
     }
}
?>


上面这些,是一些基本的设置,包括错误提示级别。如果你的php.ini中打开了register_globals(它常会带来危险并使人感到困惑),我们要把它随便设置的那些全局变量删掉。


<?php
if (!get_magic_quotes_gpc()) {
     if (is_array($_GET)) {
             while (list($k, $v) = each($_GET)) {
                 if (is_array($_GET[$k])) {
                     while (list($k2, $v2) = each($_GET[$k])) {
                             $_GET[$k][$k2] = addslashes($v2);
                     }
                     @reset($_GET[$k]);
                 }
                 else {
                     $_GET[$k] = addslashes($v);
                 }
             }
             @reset($_GET);
     }
    
     if (is_array($_POST)) {
             while (list($k, $v) = each($_POST)) {
                 if (is_array($_POST[$k])) {
                     while (list($k2, $v2) = each($_POST[$k])) {
                             $_POST[$k][$k2] = addslashes($v2);
                     }
                     @reset($_POST[$k]);
                 }
                 else {
                     $_POST[$k] = addslashes($v);
                 }
             }
             @reset($_POST);
     }
    
     if (is_array($_COOKIE)) {
             while (list($k, $v) = each($_COOKIE)) {
                 if (is_array($_COOKIE[$k])) {
                     while (list($k2, $v2) = each($_COOKIE[$k])) {
                             $_COOKIE[$k][$k2] = addslashes($v2);
                     }
                     @reset($_COOKIE[$k]);
                 }
                 else {
                     $_COOKIE[$k] = addslashes($v);
                 }
             }
             @reset($_COOKIE);
     }
}


define('STRIP', (get_magic_quotes_gpc()) ? true : false);
?>

上面的一陀,显而易见,它在做转义过滤所有来自客户端的输入。

    下面的部分是对系统的初始化。之前的部分,可能和普通的程序没什么两样,但是下面这一段,我保证你没见过。


<?php
// Init System
require('../../includes/kernel/config.inc.php');


// First Startup? Init the tmpfs
if (!is_dir ($data_root) || !is_dir ($includes_root)) {
     if (!is_writable ($tmpfs_root))
             die ('TMPFS FAILED!!!');
    
     require_once('../../includes/kernel/pkg.inc.' . $phpEx);
     @mkdir ($data_root);
     @mkdir ($includes_root);
    
     $pkg = new BsmPkg ();
     $pkg->target_dir = $data_root;
     $pkg->filename = $tmpfs_pkg_data_filename;
     $pkg->unpack_into_dir ();
    
     $pkg->target_dir = $includes_root;
     $pkg->filename = $tmpfs_pkg_includes_filename;
     $pkg->unpack_into_dir ();
}
?>

包含一个config.inc.php看起来很正常,它里面有一些关于系统的设置参数(这个文件后面会有),然后它会检查$data_root和$includes_root目录是否存在,并检查$tmpfs_root目录是否可写。这里的$data_root和$includes_root按照规定,是属于$tmpfs_root的下一级目录,而$tmpfs_root是整个系统使用的tmpfs根路径,它负责保存我们系统的临时数据,其中$includes_root用来保存那些需要被执行体包含的include文件,$data_root用来存放Cache、模板编译结果等数据文件。
    tmpfs是Linux里的一种特殊分区格式。区别于ext3等,tmpfs创建于内存和交换区上。Linux有一个默认的shm就是tmpfs类型,通常mount在/dev/shm上。tmpfs和ramfs有些相似,不同的是它会用到交换区。
    tmpfs的最大好处是IO速度。毕竟纯粹的物理磁盘操作效率无法和内存相比,而且tmpfs使用起来也很方便,它基本不需要做什么其它设置就可以像普通的物理硬盘一样使用,它对程序来说是透明的。
    tmpfs的使用方法与Linux挂载其它类型的分区格式一样,可以用mount命令来挂载,也可以在fstab中设置。
    * * * * * *
    当系统检测到$tmpfs_root确实存在且可写,而$data_root和$include_root不存在,表示这是系统第一次在运行,它会用内置的一个压缩/解压文件的一个类来把事先准备好的data和includes压缩文件解压到$tmpfs_root中,这个类处理的格式是我自创的,它保持了源目录结构,并保存了文件的属性。它也会对每一个文件做文件长度和MD5校验。这个类位于/includes/kernel/pkg.inc.php
    这里提及一个细节,我学习了PHPBB中的$phpEx的概念,整个系统中除了调用common.inc.php和config.inc.php外,其它调用php文件的地方都没有写“.php”扩展名,而是用了一个$phpEx变量代替,这个变量的值在config文件中可以修改,这样做的好处是我们随时可以把系统中的php程序改换扩展名。比如我们修改了Apache配置,让php解释器来解释一种叫做.hello的文件,就可以方便地把整个系统的所有被include的php程序扩展名改成.hello,再把config中的$phpEx的值改成“hello”,这样你的系统看起来就像是使用一种没人见过的Hello语言编写的了,哈哈……
    includes这个压缩文件中包含了/includes目录中的所有内容,它被解压到$tmpfs_root(我的系统中是/opt/tmp/)中,这样,在/opt/tmp/includes中就有我们想要的所有include文件了,调用它比直接调用/includes要快很多。
    下面的部分就是调用已经解压好的一些include文件


<?php
// Include Kernel file
require($includes_root . 'db/' . $global_db_dbms . '.' . $phpEx);
require($includes_root . 'kernel/constants.inc.' . $phpEx);
require($includes_root . 'kernel/template.inc.' . $phpEx);
require($includes_root . 'kernel/session.inc.' . $phpEx);
require($includes_root . 'kernel/cache.inc.' . $phpEx);
require($includes_root . 'kernel/log.inc.' . $phpEx);
require($includes_root . 'kernel/shm.inc.' . $phpEx);


require($includes_root . 'function/basic.function.' . $phpEx);
require($includes_root . 'function/file.function.' . $phpEx);
?>

接着创建一个通用的数据库连接$db,它的属性也都在config.inc.php中设置。



<?php
// Init the DB Connection
$db = new $sql_db;


// Connect to DB
$db->sql_connect($global_db_host, $global_db_user, $global_db_pass, $global_db_name, $global_db_port, false);
?>


创建成功后,记得把密码清空



<?php
// We do not need this any longer, unset for safety purposes
unset($global_db_pass);
?>

创建日志对象


<?php
// Init Log
$log = new BsmLog ('bsg');
?>


这又是一个精彩部分,创建一个共享内存对象


<?php
// Init the Shared Memory
$shm = new BsmShm;
if ($shm->shm_id) {
     define ('SHM_SUPPORT', true);
}
?>


如果系统是第一次运行,则在共享内存中标记一个运行标志SHM_VAR_SYS_RUN = true(系统的constants.inc.php是常量表,里面定义了系统要使用的常量)



<?php
if (defined ('SHM_SUPPORT') && !@$shm->get_var (SHM_VAR_SYS_RUN)) {
     $shm->put_var (SHM_VAR_SYS_RUN, true);
}
?>


我写了一个Session类,也许它的效率并不很好,我只是写来玩玩……如果你觉得它的效率不行,可以使用系统Session,或者使用Sky同学的SessionD,哈哈——做个广告……


<?php
// Init the User Defined Session
$mSession = array ();
$sess = new BsmSession;
$sess->mSession_Start ();
?>


一长串乱七八糟的代码,只是为了获得访问者IP:


<?php
// Proc clients' IP address'
if(getenv('HTTP_X_FORWARDED_FOR') != '') {
     $client_ip = (!empty($_SERVER['REMOTE_ADDR']) ) ? $_SERVER['REMOTE_ADDR'] : ( (!empty($_ENV['REMOTE_ADDR']) ) ? $_ENV['REMOTE_ADDR'] : $REMOTE_ADDR);


     $entries = explode(',', getenv('HTTP_X_FORWARDED_FOR'));
     reset($entries);
     while (list(, $entry) = each($entries)) {
             $entry = trim($entry);
             if (preg_match("/^([0-9]+.[0-9]+.[0-9]+.[0-9]+)/", $entry, $ip_list) ) {
                 $private_ip = array('/^0./', '/^127.0.0.1/', '/^192.168..*/', '/^172.((1[6-9])|(2[0-9])|(3[0-1]))..*/', '/^10..*/', '/^224..*/', '/^240..*/');
                 $found_ip = preg_replace($private_ip, $client_ip, $ip_list[1]);


                 if ($client_ip != $found_ip) {
                     $client_ip = $found_ip;
                     break;
                 }
             }
     }
}
else
{
     $client_ip = (!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : ((!empty($_ENV['REMOTE_ADDR'])) ? $_ENV['REMOTE_ADDR'] : $REMOTE_ADDR);
}
?>

获得环境参数的Cache,并返回它的值。环境参数是什么内容要看具体做的是什么,比如它是一个普通网站,参数中可能会包括站点名称、作者、首页布局、使用的模板、几个模块、每个模块显式几行、每行显式几个图片这些值。这些值一般都是在后台中可以修改然后存入数据库中的。cache.inc.php中的obtain_config函数负责在第一次执行时把它们从数据库中取出,并放到一个可以被更快地访问到的地方,比如shm中,或者是tmpfs上的一个文件,优化它是非常有必要的,因为这批数据被使用得太频繁了。


<?php
// Grab global variables, re-cache if necessary
$CONF = obtain_config();
?>


然后是处理gz压缩。下面这些内容是明显通用化的,甚至包括检查php版本。前面我已经说过,我们是在控制自己的独立服务器,所以你完全可以把它简写成你自己需要的样子甚至完全删掉它。


<?php
// Setting the ob_gzhandler
if ($CONF['gz_compress']) {
     $phpver = phpversion ();


     $useragent = (isset ($_SERVER["HTTP_USER_AGENT"])) ? $_SERVER["HTTP_USER_AGENT"] : $HTTP_USER_AGENT;


     if ($phpver >= '4.0.4pl1' && (strstr ($useragent, 'compatible') || strstr ($useragent, 'Gecko'))) {
             if (extension_loaded ('zlib')) {
                 ob_start ('ob_gzhandler');
             }
     }
     elseif ($phpver > '4.0') {
             if (strstr ($HTTP_SERVER_VARS['HTTP_ACCEPT_ENCODING'], 'gzip')) {
                 if (extension_loaded ('zlib')) {
                     ob_start ();
                     ob_implicit_flush (0);
                     $mSession['do_gzip'] = true;
                     header ('Content-Encoding: gzip');
                 }
             }
     }
}


else
     ob_start ();
?>


下面是初始化模板类,这个模板类~~似乎没人见过,我也没拿出来给人看过,呵呵……



<?php
// Init the Template Object
$tpl = new BsmTpl ('../../templates/', $tpl_c_root);
$tpl->set_tpl_name ('seepic');     //seepic是我临时写的一个模板名字,实际使用时应该用的是$CONF里的值。
?>

初始化模板之后是确认语言,因为有可能会用到多语言的语言包文件。


<?php
// Confirm Accept Language
$langMeta = isset ($_COOKIE['langMeta']) ? $_COOKIE['langMeta'] : $_SERVER['HTTP_ACCEPT_LANGUAGE'];


if ($_GET['lang']) {
     $langMeta = trim ($_GET['lang']);
     setcookie ('langMeta', $langMeta, time() + (60 * 60 * 24 * 365), $cookie_path);
}
?>


把最终确认的语言赋值给模板对象,common文件的任务完成了。


<?php
include $tpl->set_language ($langMeta);
?>

下面来看看config文件里有什么,我会逐段解释:



<?php
// BSM Configuration File.


// Para_DB_Global:
     $global_db_dbms                             = 'mysql4';                     //全局数据库连接类型(支持mysql/mysql4/oracle/mssql/odbc/access/pqsql/sqlite等)
    
     $global_db_host                             = 'localhost';             //数据库服务器地址
     $global_db_port                             = '';                                     //端口
     $global_db_name                             = 'g';                                 //数据库名
     $global_db_user                             = 'root';                             //数据库用户名
     $global_db_pass                             = 'root';                             //连接密码
    
     $global_db_prefix                     = 'bsg_';                             //数据表前缀(我都不知道我写它干什么&hellip;&hellip;整个服务器都是我的)
    
// Para_DB_Member:
     $member_db_dbms                             = 'sqlite';                     //可以再定义一组数据库连接,用了sqlite,比如保存个人信息什么的&hellip;&hellip;呵呵
    
     $member_db_host                             = 'member.db';             //sqlite的服务器地址就是数据库文件名了
     $member_db_port                             = '';
     $member_db_name                             = '';
     $member_db_user                             = '';
     $member_db_pass                             = '';
    
     $member_db_prefix                     = 'bsg_';
    
// Para_DB_Session;
     $session_save_handle                 = 'system';                 //也许更多人还是喜欢用mysql的HEAP表保存session
     $session_life_time                     = '60*15';
    
     $session_db_dbms                     = 'mysql4';
    
     $session_db_host                     = '';
     $session_db_port                     = '';
     $session_db_name                     = '';
     $session_db_user                     = '';
     $session_db_pass                     = '';
    
     $session_db_table                     = $global_db_prefix . 'sess';


// Para_Local_Sites:                                                                 //这是定义的一些绝对路径变量,你可以随便写点什么
     $global_site_root                     = '/www/mine/site/global/';
     $member_site_root                     = '/www/mine/site/member/';
     $admin_site_root                     = '/www/mine/site/admin/';


// Para_Global_Local_Dir
     $tmpfs_root                                 = '/opt/tmp/';                 //tmpfs的挂载根,具体要看你在mount的时候或者fstab中把它挂到什么地方了
     $data_root                                 = $tmpfs_root . 'data/';
                                                                                                                     //临时数据保存目录
     $tpl_c_root                                 = $data_root . 'template/';
                                                                                                                     //模板编译文件保存目录,可以看见它位于/opt/tmp/data/template/下
     $cache_root                                 = $data_root . 'cache/';
                                                                                                                     //缓存目录
     $includes_root                             = $tmpfs_root . 'includes/';
                                                                                                                     //include文件保存目录
     $tmpfs_pkg_data_filename             = '/www/g/dev_tools/start.data.tmpfs.pkg';
     $tmpfs_pkg_includes_filename     = '/www/g/dev_tools/start.includes.tmpfs.pkg';
                                                                                                                     //这两个是预先做好的压缩包,包含data和includes需要的文件和目录结构,你可以把它们放在web目录外面


// Para_Runtime_Environment:
     $phpEx                                     = 'php';                                     //这就是$phpEx,哈哈


// Para_Template


// Para_BSM_Session:
     $user_sess_base_dir                     = $data_root . 'user_sess/';
                                                                                                                     //Session文件保存根目录(如果你用了我的Session类),Session文件在保存时会自己Hash的
     $cookie_sess_id_varname                 = 'BSG_sid';
                                                                                                                     //在Cookie中的SessionID变量名
     $sess_lifetime                             = 60 * 15;                     //Session存活时间


// Para_Log:
     $log_record_method                     = 'file';                     //日志保存方式
     $log_base_dir                             = $data_root . '/log/';
                                                                                                                     //日志保存点。虽然它在tmpfs下,不过我们可以有一个守护进程每隔一段时间把它写到磁盘上。或者干脆就把它直接写到磁盘上。因为系统崩溃前夕的Log才最有价值


// Para_Cookie                                                                                 //Cookie设置(如果你需要用它)
     $cookie_domain                             = '';
     $cookie_path                             = '/';

?>


这两个文件基本确立了系统的运行环境,可以看见,里面使用了一些在通用系统中不会有的技术,包括共享内存、tmpfs等。当然还可以根据需要加入memcached支持等。
    提及一下我所用到的数据库封装类包,它源自PHPBB3,很遗憾我没有和作者打招呼。使用它并不是因为它比Adodb和PEAR DB好很多,只是因为我熟悉它。其实它的结构很清晰,功能也很简单,不过简单的才是我想要的,我不希望一个类中藏着一大堆我不知道在干什么的代码(比如Smarty……)
    下面举一个例子,来说明tmpfs和shm是如何被系统用到并做缓存来用的。这里是cache.inc.php中的obtain_cache函数:



function obtain_config ()
{
     global $db, $global_db_prefix;
    
     $config_cache_file = '../../data/cache/config.cache';
    
     if (defined ('SHM_SUPPORT')) {
             global $shm;
             @$CONF = $shm->get_var (SHM_VAR_PARA_CACHE);
            
             if ($CONF['cfg_end'] !== 0) {
                 $sql = "SELECT * FROM {$global_db_prefix}config";
                 $res = $db->sql_query ($sql);
                
                 $CONF = array ();
                
                 while ($r = $db->sql_fetchrow ($res)) {
                     $CONF[$r['config_varname']] = $r['config_value'];
                 }
                
                 $CONF['cfg_end'] = 0;
                
                 $shm->put_var (SHM_VAR_PARA_CACHE, $CONF);
             }
     }
    
     else {
             if (!@file_exists ($config_cache_file)) {
                 $str = "<?phprn// Config Cache File...DO NOT MODIFY THIS FILE PLEASE!!!rnif (!defined('IN_BSG')) {rntexit;rn}rn" . '$CONF = array (' . "rn";
                
                 $sql = "SELECT * FROM {$global_db_prefix}config";
                 $query = $db->sql_query ($sql);
                
                 while ($r = $db->sql_fetchrow ($query)) {
                     $value = "'" . addslashes ($r['config_value']) . "'";
                     $str .= "t'" . $r['config_varname'] . "'ttt" . '=> ' . $value . " ,rn";
                 }
                
                 $str .= "t'cfg_end'ttt=> 0rn";
                 $str .= ");rn?>


";
           
            if (!@$fp = fopen ($config_cache_file, 'w'))
                return false;
           
            fwrite ($fp, $str);
            fclose ($fp);
        }
        include ($config_cache_file);
    }
   
    return $CONF;
}


这个函数分为两个部分,分别对应于shm和文件。首先它会检查系统是否支持共享内存(这个常量是在common.inc.php中已经设置过的),如果支持,函数会在config表中(具体应用中我也不知道你会把它放在哪里)读取所有的变量,并把它们放到一个数组中直接保存到shm里(当然实际操作不是这样简单的),如果系统不支持shm,函数会试图生成一个php文件。当再次调用这个函数时,如果shm里已经有了这个数组存在,或者已经有了这个文件存在的话(前面已经规定这个文件会被保存在tmpfs上),函数会直接返回它们的内容,不必再去读取数据库。
    这就是一个简单的cache概念。究竟什么样的数据可以并且适合被cache?这和cache的更新方式有关。cache有定时间隔更新的,有不定时更新的。定时更新的指cache存在若干时间后再次重新生成cache,通常用于统计数据,比如在线人数等。不定时更新的是指生成后就一直保持不变,直到再次检测到不存在或已过期、已损坏等情况出现,通常见于参数调用、模板编译结果等。这些数据的特点是它们都是临时的,可以被丢弃的,比如没人会在乎一个模板是否被重新编译过,除了在编译的那次执行中多占用一点时间。这批可丢弃的数据就可以被放心地保存在内存或者tmpfs中,因为它们不怕丢失,并且随时可以被重建。
    早期版本的PHPWIND论坛的cache机制是很差的,虽然它很快,但是很脆弱,一旦cache文件损坏或丢失,它不会自己去创建它,而是直接导致程序无法运行,这种只能叫做临时文件,而不能叫cache。我不知道现在的PHPWIND什么样,因为我一直没兴趣去看它……
   
    下面是shm.inc.php的源码,我不想对它加太多的注释,因为它很机械,没什么好注释的。唯一需要注意的是php的两种支持shm的方式。一种是shmop,一种是sysv的shm,不同的是sysv只在UNIX/LINUX系统中存在,shmop更底层,只接受字符串数据。



<?php
class BsmShm
{
     var $shm_id;
     var $shm_mod;
    
     function BsmShm ()
     {
             // Init Shared Memory Support...
             // Both SysV Shm and Shmop are support under *NIX Operating System
             // But Only Shmop can be used in Windows.
            
             if (get_sys() == SYSTEM_WIN) {
                 if (function_exists ('shmop_open')) {
                     $this->shm_mod = 'shmop';
                 }
                 else {
                     $this->shm_mod = 'none';
                     $this->shm_id = false;
                 }
             }
            
             else {
                 if (function_exists ('shm_attach')) {
                     $this->shm_mod = 'sysv';
                 }
                 elseif (function_exists ('shmop_open')) {
                     $this->shm_mod = 'shmop';
                 }
                 else {
                     // No Module installed
                     $this->shm_mod = 'none';
                     $this->shm_id = false;
                 }
             }
            
             if ($this->shm_mod == 'sysv') {
                 $this->shm_id = shm_attach (ftok (__FILE__, 'g'), SHM_SIZE, 0600);
             }
            
             elseif ($this->shm_mod == 'shmod') {
                 // if no "sysv" module installed, function "ftok())" is unavailiable.
                 $this->shm_id = shmop_open (SHM_KEY, 'n', 0600, SHM_SIZE);
             }
            
             return;
     }
    
     function put_var ($varkey, $varval)
     {
             // Write a value into shm
             if ($this->shm_mod == 'sysv')
                 return shm_put_var ($this->shm_id, $varkey, $varval);
            
             elseif ($this->shm_mod == 'shmod') {
                 // shmop is much more low-level than sysv, you need to operate every byte yourself!
                 $curr = shmop_read ($this->shm_id, 0, shmop_size ($this->shm_id));
                 $curr = base64_decode ($curr);
                 $curr = substr ($curr, 0, strpos ($curr, "
                

这个class同时支持sysv和shmop,对于shmop,它把数据做了序列化,并用一个\0做为数据的结束。因为序列化本身并不是很快,所以有可能的话,还是sysv的shm稳定一些。
     共享内存的原本用途并不是做缓存,也不是做buffer,它是用来做进程间通信的。它可以保存临时队列,传递信号量等。我们在这里变通地用它来保存点东西,只是因为它的速度快得实在不是文件和数据库存取能比的。而且因为它的共享性,它在一段web脚本结束之后不会消失,所以它特别适合用来实现Application变量(不用再羡慕ASP了)。
    
     下面的部分是mSession的实现,它只是模拟了session的存取过程,并对系统session进行了改进。它用了Hash目录。它的缺点是在程序结束部分还要Rewrite一下,把数据更新到session文件里,当然这个很容易被改进。

 


<?php
class BsmSession
{
     var $sid;
     var $sess_file;
    
     function mSession_Start ()
     {
             // Special Function...session_start()
             global $cookie_sess_id_varname, $cookie_path, $sess_liftime, $mSession;
            
             $sid = $_COOKIE[$cookie_sess_id_varname] ? $_COOKIE[$cookie_sess_id_varname] : $this->_Gen_Sid();
             setcookie ($cookie_sess_id_varname, $sid, $sess_liftime, $cookie_path);
            
             $sess_file = $this->_Hash_Dir($sid) . 'sess_' . $sid;
            
             if (file_exists ($sess_file)) {
                 if (!@$fp = fopen ($sess_file, 'rb')) {
                     // Debug Info...No Log.
                     fatal_error ('Session Error...');
                 }
                
                 if (0 == ($fl = filesize ($sess_file)))
                     $sess_content = '';
                
                 else
                     $sess_content = fread ($fp, $fl);
             }
             else {
                 if (!@$fp = fopen ($sess_file, 'wb')) {
                     // Debug Info...No Log.
                     fatal_error ('Session Error...');
                 }
                
                 $sess_content = '';
             }
            
             fclose ($fp);
            
             $this->sid = $sid;
             $this->sess_file = $sess_file;
            
             $mSession = unserialize($sess_content) or $mSession = array ();
     }
    
     function mSession_Destroy ()
     {
             global $mSession;
            
             $mSession = array ();
             return @unlink ($this->sess_file);
     }
    
     function mSession_Rewrite ()
     {
             // Restore Session Data into Session File
             global $mSession;
            
             $sess_content = serialize($mSession);
            
             if (!@$fp = fopen ($this->sess_file, 'wb')) {
                 // Debug Info...No Log.
                 fatal_error ('Session Error...');
             }
            
             fwrite ($fp, $sess_content);
             fclose ($fp);
            
             return;
     }
    
     function _Hash_Dir ($sid)
     {
             // Hash the Session file Dir
            
             global $user_sess_base_dir;
            
             $sess_dir = $user_sess_base_dir . substr ($sid, 0, 1) . '/' . substr ($sid, 16, 1) . '/';
             return $sess_dir;
     }
    
     function _Gen_Sid ()
     {
             // Gen an Unique Session ID
            
             $key_1 = rand (32768, 65535);
             $key_2 = microtime ();
             $key_3 = sha1 (time ());
            
             $sid = md5 ($key_1 . $key_3 . $key_2);
            
             return $sid;
     }
    
     function _Get_Sid ()
     {
             // Get Current Session ID
             global $cookie_sess_id_varname;
            
             $sid = $_COOKIE[$cookie_sess_id_varname] ? $_COOKIE[$cookie_sess_id_varname] : FALSE;
             return $sid;
     }
}
?>


Hash目录是一种优化文件存储性能的方法。无论是Windows还是Linux,无论是NTFS还是ext3,每个目录下所能容纳的项目数是有限的。并不是不能保存,而是当项目数量过大的时候,会降低文件索引速度,所以权衡一个目录下应该保存多少文件是很必要的。保存得多了会影响性能,保存得少了会造成目录太多和空间浪费。所以当保存大批文件的时候,需要有一种算法能将文件比较均匀地“打散”在不同的子目录下以提高每一级的索引速度,这种算法就是Hash。通常用的MD5、sha1等都可以用来做Hash目录,我的mSession里也同样使用了MD5,取得sessionID的第一位和第九位,这就构成了两级Hash路径,也就是说,系统把所有的Session文件分散到了16×16=256个子目录下。假设Linux每个目录下保存1000个文件可以获得最好的空间性能比,那么系统在理想情况下可以同时有256000个session文件在被使用。
    Hash目录还被广泛应用在备份、图库、电子邮件、静态页生成等文件密集型应用上。
   
    再来点一下我的模板类,我很懒地保留了Discuz模板函数的所有标签。一方面是我确实很懒,另一方面是我曾经试图修改Discuz,把它改成一个专用的版本,不过这是一个类,它的使用方法和Discuz函数没什么两样,都是include一个parse结果返回的文件名。
    所不同的是在处理{template}标签的时候。Discuz的处理方式是把{template}替换成再次调用模板解析函数去解析另一个模板文件,这样,模板函数可能会被调用多次,编译的结果里也会有很多include另一个模板文件Parse结果的地方。这里涉及另一个优化点——尽量少地include文件。过多地include会带来更多的IO开销和CPU处理开销,所以我把{template}改成直接读入文件内容,然后再parse。这样一个模板文件即使有1000个{template},编译的结果也只有一个文件。
    这个模板类用起来是如此地简单方便,更重要的是,它确实很快~~呵呵,我从来不否认我有时候也会做一些比较有用的事,哈哈:

 


<?php


// BSM Template Class v1.03
// By Dr.NP
// Create data: 11-26-2005


class BsmTpl {


     var $classname                     = 'BsmTpl';
    
     var $tpl_root_dir                 = 'templates/';
     var $tpl_cache_root_dir             = 'sitedata/template/';
     var $tpl_dir                     = '';
    
     var $tpl_file_ext                 = 'htm';
     var $tpl_cache_file_ext             = 'php';
     var $tpl_lang_file_ext             = 'lang';
     var $tpl_static_file_ext     = 'html';
    
     var $tpl_name                     = 'default';
     var $default_tpl_name             = 'default';
    
     var $default_db_handle             = '$db';
     var $default_lang                 = 'zh-cn';
    
     function BsmTpl ($root_dir = '', $cache_root_dir = '')
     {
             if ($root_dir != '')
                 $this->tpl_root_dir = $root_dir;
            
             if ($cache_root_dir != '')
                 $this->tpl_cache_root_dir = $cache_root_dir;
     }
    
     function parse_template ($tplfile, $objfile)
     {
             $nest = 5;
            
             if (!@$fp = fopen ($tplfile, 'r')) {
                 die ("Current template file '" . $tplfile. " ' not found or have no access!");
             }
            
             $template = fread ($fp, filesize ($tplfile));
             fclose ($fp);
            
             $var_regexp = "((\$[a-zA-Z_x7f-xff][a-zA-Z0-9_x7f-xff]*)([[a-zA-Z0-9_\"'$x7f-xff]+])*)";
             $const_regexp = "([a-zA-Z_x7f-xff][a-zA-Z0-9_x7f-xff]*)";
            
             $template = preg_replace ("/s*{templates+(.+?)}s*/ies", "file_get_contents('{$this->tpl_dir}\1.{$this->tpl_file_ext}')", $template);
            
             $template = preg_replace ("/([nr]+)t+/s", "\1", $template);
             $template = preg_replace ("/<!--{(.+?)}-->/s", "{\1}", $template);
             //$template = preg_replace ("/{langs+(.+?)}/ies", "languagevar('\1')", $template);
             $template = str_replace     ("{LF}", "<?=\"\n\"?>
", $template);                         //  ** = p
        
        $template = preg_replace ("/{(\$[a-zA-Z0-9_[]'\"$x7f-xff]+)}/s", "<?=\1?>[/ph**]", $template);
        $template = preg_replace ("/$var_regexp/es", "addquote('<?=\1?>[/ph**]')", $template);
        $template = preg_replace ("/<?=<?=$var_regexp?>[/ph**]?>[/ph**]/es", "addquote('<?=\1?>[/ph**]')", $template);
       
        $template = preg_replace ("/s*{evals+(.+?)}s*/ies", "stripvtags('n<?php \1 ?>[/ph**]n', '')", $template);
        $template = preg_replace ("/s*{elseifs+(.+?)}s*/ies", "stripvtags('n<?php } elseif(\1) { ?>[/ph**]n', '')", $template);
        $template = preg_replace ("/s*{else}s*/is", "n<?php } else { ?>[/ph**]n", $template);
        $template = preg_replace ("/s*{dates+(.+?)s+(.+?)}s*/ies", "stripvtags('n<?php echo date (\1, \2 ) ?>[/ph**]n', '')", $template);
       
        for($i = 0; $i < $nest; $i++) {
            $template = preg_replace ("/s*{loops+(S+)s+(S+)}s*(.+?)s*{/loop}s*/ies", "stripvtags('n<? if(is_array(\1)) { foreach(\1 as \2) { ?>[/ph**]','n\3n<? } } ?>[/ph**]n')", $template);
            $template = preg_replace ("/s*{loops+(S+)s+(S+)s+(S+)}s*(.+?)s*{/loop}s*/ies", "stripvtags('n<? if(is_array(\1)) { foreach(\1 as \2 => \3) { ?>[/ph**]','n\4n<? } } ?>[/ph**]n')", $template);
            $template = preg_replace ("/s*{ifs+(.+?)}s*(.+?)s*{/if}s*/ies", "stripvtags('n<? if(\1) { ?>[/ph**]','n\2n<? } ?>[/ph**]n')", $template);
        }
       
        $template = preg_replace ("/{$const_regexp}/s", "<?=\1?>[/ph**]", $template);
        $template = preg_replace ("/ ?>[/ph**][nr]*<? /s", " ", $template);
       
        /*
        $int = preg_match_all ("/->parse('<?=(.+?)?>[/ph**]')/ies", $template, $arr);
       
        for ($i = 0; $i < sizeof ($arr[0]); $i++) {
            $template = str_replace ($arr[0][$i], '->parse(' . $arr[1][$i] . ')', $template);
        }
        */
       
        $template = str_replace ('<!--NOW_TIMESTAMP!-->', '<?php echo time(); ?>[/ph**]', $template);
        $template = str_replace ('<!--NUM_QUERIES!-->', '<?php echo ' . $this->default_db_handle . '->num_queries; ?>[/ph**]', $template);
       
        /*
        if (!@$fp = fopen($objfile, 'w')) {
            die ("Directory '" . $this->tpl_root . '/' . $this->tpl_cache_root . "' not found or have no access!");
        }
       
        flock ($fp, 3);
        fwrite ($fp, $template);
        fclose ($fp);
        */
       
        return $template;
    }
   
    function parse ($file)
    {
        global $page_time, $page_time_start;
       
        $tplfile = $this->tpl_dir . $file . '.' . $this->tpl_file_ext;
        $objfile = $this->tpl_cache_root_dir . '/' . $this->tpl_name . '_' . $file . '.tpl.' . $this->tpl_cache_file_ext;
       
        if (!file_exists($tplfile)) {
            $tplfile = $this->tpl_root_dir . '/' . $this->default_tpl_name . '/' . $file.'.'.$this->tpl_file_ext;
            $objfile = $this->tpl_cache_root_dir . '/' . $this->default_tpl_name . '_' . $file . '.tpl.' . $this->tpl_cache_file_ext;
        }
       
        if ((@filemtime($tplfile) > @filemtime($objfile)) || (!@file_exists($objfile))) {
            //$this->parse_template ($tplfile, $objfile);
            if (!@$fp = fopen($objfile, 'w')) {
                die ("Directory '" . $this->tpl_root . '/' . $this->tpl_cache_root . "' not found or have no access!");
            }
           
            flock ($fp, 3);
            fwrite ($fp, $this->parse_template ($tplfile, $objfile));
            fclose ($fp);
        }
        list ($usec, $sec) = explode (" ", microtime ());
        $page_time_end = $usec + $sec;
        $page_time = sprintf ("%0.6f", $page_time_end - $page_time_start);
       
        return $objfile;
    }
   
    function set_tpl_db_handle ($dh)
    {
        $this->defalt_db_handle = '$' . $dh;
    }
   
    function set_tpl_name ($name)
    {
        $tpl_dir = $this->tpl_root_dir . '/' . $name . '/';
        if ($name != '' && is_dir ($tpl_dir)) {
            $this->tpl_name = $name;
            $this->tpl_dir = str_replace ('//', '/', $tpl_dir);
        }
        else {
            $this->tpl_name = $this->default_tpl_name;
            $this->tpl_dir = str_replace ('//', '/', $this->tpl_root_dir . '/' . $this->tpl_name . '/');
        }
       
    }
   
    function set_language ($langMeta)
    {
        $langFile = $this->tpl_dir . $langMeta . '.lang';
        clearstatcache ();
       
        if (@is_readable ($langFile)) {
            return $langFile;
        }
       
        elseif (@is_readable ($this->tpl_dir . $this->default_lang . '.' . $this->tpl_lang_file_ext)) {
            $langFile = $this->tpl_dir . $this->default_lang . '.' . $this->tpl_lang_file_ext;
            $langMeta = $this->default_lang;
            return $langFile;
        }
       
        elseif (@is_readable ($this->tpl_root_dir . '/' . $this->default_tpl_name . '/' . $langMeta . '.lang')) {
            $langFile = $this->tpl_root_dir . '/' . $this->default_tpl_name . '/' . $langMeta . '.lang';
            return $langFile;
        }
       
        elseif (@is_readable ($this->tpl_root_dir . '/' . $this->default_tpl_name . '/' . $this->default_lang . '.' . $this->tpl_lang_file_ext)) {
            $langFile = $this->tpl_root_dir . '/' . $this->default_tpl_name . '/' . $this->default_lang . '.' . $this->tpl_lang_file_ext;
            $langMeta = $this->default_lang;
            return $langFile;
        }
       
        else
            die ('Accept Langfile:' . $langFile . ' did not exist or has no access!');
    }
   
    function dsp ()
    {
        global $mSession;
        if ($mSession['do_gzip']) {
            $gzip_contents = ob_get_contents ();
            ob_end_clean ();
           
            $gzip_size = strlen ($gzip_contents);
            $gzip_crc = crc32 ($gzip_contents);
           
            $gzip_contents = gzcompress ($gzip_contents, 9);
            $gzip_contents = substr ($gzip_contents, 0, strlen ($gzip_contents) - 4);
           
            echo "x1fx8bx08x00x00x00x00x00";
            echo $gzip_contents;
            echo pack ('V', $gzip_crc);
            echo pack ('V', $gzip_size);
        }
        else
            ob_end_flush ();
    }
   
    function get_static_html ($file)
    {
        // Just for Test...
       
        $static_file = '../../data/static/' . $file . '.' . $this->tpl_static_file_ext;
        if (@$fp = fopen ($static_file, 'wb')) {
            fwrite ($fp, ob_get_contents ());
            fclose ($fp);
        }
       
        ob_end_clean ();
        return;
    }
}

function addquote ($var)
{
    return str_replace("\\"", """, preg_replace("/[([a-zA-Z_x7f-xff][a-zA-Z0-9_x7f-xff]*)]/s", "['\1']", $var));
}
   
function stripvtags ($expr, $statement)
{
    $expr = str_replace("\\"", """, preg_replace("/<?=(\$[a-zA-Z_x7f-xff][a-zA-Z0-9_[]\"'x7f-xff]*)?>[/ph**]/s", "\1", $expr));
    $statement = str_replace("\\"", """, $statement);
    return $expr . $statement;
}
   
?>


后面附了一个简单的获取静态页的方法,其实也没什么用,大家都有更好的方法来生成静态页。
   
    *        *        *        *        *        *
   
    主要就是这些东西支撑起一个系统运行的必要部分。我从来不强调MVC层次,也不去讲究OOP,虽然偶尔也写一些很蹩脚的类。多年以来Pascal、C和汇编养成的习惯使我相比注意OO结构之外更注意执行效率。这次只是罗列了一些基于共享内存和tmpfs的优化方法。
    至于把什么样的数据放在tmpfs上,各位自己看着办。我把include文件、session、模板的编译结果、cache文件放在了上面。 在提升IO性能的同时,它带来的另一个好处是不需要把这些文件放在web目录里,也提高了不少安全性。即使有一些文件需要放在web目录下,比如程序执行文件(废话……),也不要用奇怪的扩展名。对于config.inc.php这样的文件尤其要注意,不要使用config.inc这种文件名,很有可能你的系统忘了配置对.inc的支持,访问者可以直接在浏览器里访问config.inc就可以把这个文件下载走了,而这个文件里保存着你的数据库密码……
    走到这里,我们已经逐渐地跟上了优化的步伐,在后面的时间里,优化程序结构的同时,已经可以做好更输入地挖掘系统潜力的的准备了。将优化进行到底,挑战一下一台服务器到底能撑住多少个访问者是我近期的变态目标。不过再走下去,可能已经走出了PHP的领地,各位一定要有心理准备,因为我的C程序写得有时候比天书还乱…………hoho
    附上那个压缩/解压的类:



<?php
class BsmPkg
{
     //This class operates with PKG archive format...Haha
     //By Dr.NP 02-15-2006
    
     var $classname                             = 'BsmPkg';
     var $source_dir                             = '';
     var $target_dir                             = '';
     var $filename                             = '';
     var $max_filesize                     = 1048576;
    
     var $error_msg                             = '';
     var $line_ret                             = "n";
    
     function pack_from_dir ()
     {
             ini_set ('memory_limit', '32M');
             global $content;
            
             $source_dir = $this->source_dir ? $this->source_dir : './';
            
             $content = '<==PACKAGE_START==>' . $this->line_ret;
             $this->_GetDirs ($source_dir);
             $content .= '<==PACKAGE_END==>';
             $zfp = gzopen ($this->filename, 'wb9');
             gzwrite ($zfp, $content);
             gzclose ($zfp);
             return;
     }
    
     function unpack_into_dir ()
     {
             ini_set ('memory_limit', '32M');
            
             $target_dir = $this->target_dir ? $this->target_dir : './';
             $zfp = gzopen ($this->filename, 'rb');
             $content = gzread ($zfp, $this->max_filesize);
             gzclose ($zfp);
             $lines = explode ($this->line_ret, $content);
             while (list ($key, $line) = each ($lines)) {
                 if (preg_match ("/<==Directory:([0-7]+)==>(S+)<==/Directory==>/is", $line, $march)) {
                     $access_str = $march[1];
                     $item_dir = $march[2];
                     if (!is_dir ($target_dir . $item_dir)) {
                             mkdir ($target_dir . $item_dir);
                             @chmod ($target_dir . $item_dir, intval ($access_str, 8));
                     }
                 }
                
                 if (preg_match ("/<==File:(d+)-([0-9a-f]+)-([0-7]+)==>(S+)<==/File==>/is", $line, $march)) {
                     $target_file = $march[4];
                     $access_str = $march[3];
                     $target_file_checksum = $march[2];
                     $target_filesize = $march[1];
                    
                     if (!@$fp = fopen ($target_dir . $target_file, 'wb')) {
                             continue;
                     }
                    
                     if (false === (list ($key, $content) = each ($lines))) {    
                             continue;
                     }
                    
                     $file_content = base64_decode (trim ($content));
                    
                     if (!@fwrite ($fp, $file_content)) {
                             continue;
                     }
                    
                     fclose ($fp);
                    
                     if (!@md5_file ($target_file) == $target_file_checksum) {
                             $this->error_msg = 'File : ' . $target_dir . $target_file . 'CheckSum Failed...';
                     }
            
                     @chmod ($target_dir . $target_file, intval ($access_str, 8));
                
                 }
             }
             return;
     }
    
     function _GetDirs ($dirname)
     {
             global $content;
            
             $dh = opendir ($dirname);
            
             while (false !== ($item = readdir ($dh))) {
                 $full_itemname = str_replace ('//', '/', $dirname . '/' . $item);
                 if (strpos ($full_itemname, $this->source_dir) === 0)
                     $pkg_itemname = substr ($full_itemname, strlen ($this->source_dir));
                 else
                     continue;
                
                 if ($item != '.' && $item != '..' && $item != $this->filename) {
                     if (is_dir ($full_itemname)) {
                             $access_str = substr (decoct (fileperms ($full_itemname)), -4);
                             $content .= "<==Directory:{$access_str}==>$pkg_itemname<==/Directory==>{$this->line_ret}";
                             $this->_GetDirs ($full_itemname);
                     }
                    
                     elseif (is_file ($full_itemname) && is_readable ($full_itemname)) {
                             $filesize = filesize ($full_itemname);
                             $checksum = md5_file ($full_itemname);
                             $access_str = substr (decoct (fileperms ($full_itemname)), -4);
                             $content .= "<==File:{$filesize}-{$checksum}-{$access_str}==>$pkg_itemname<==/File==>{$this->line_ret}";
                            
                             @$fp = fopen ($full_itemname, 'rb');
                             if ($filesize > 0)
                                 $source_file_str = fread ($fp, $filesize);
                             else
                                 $source_file_str = '';
                             $base64_str = base64_encode ($source_file_str);
                             fclose ($fp);
                            
                             $content .= $base64_str . $this->line_ret;
                     }
                 }
             }
             return;
     }
}
?>


昨天实在太困了,写的什么自己也不是很清楚~这是(一),不保证什么时候写(二)、(三)、(四)……(如果有的话)。希望大家的老板可以把省掉的买服务器的钱中的一部分发下来做奖金,嘿嘿……

Session就是Session。我懒得不行,距离上次的长贴有个把月了,我也连着跳票,这几天突发奇想,也算是和朋友赌点饭,在Session上做了点手脚。

以下文字可以算做是Session扫盲,其实本文主要内容集中在创建一个服务上,每次喝酒之后都会弄些变态的东西。

PHP的Session是PHP4开始系统级提供的,PHP4之前在PHPLIB中有对Session的模拟实现,Session的作用是维持一个较长时间的会话,以保存一些跨页面的变量。通常Session有一个唯一的ID,PHP的SessionID保存在Cookie里,如果客户端不支持Cookie,也可以由URL显式传递。
SessionID的生成方式可以有很多种,主要的是尽量保证唯一性,可以由当前服务器时间、客户端地址、随机变量、服务器pid等参数通过哈稀算法生成一个唯一的id,PHP默认的SessionID生成算法如下:(session.c)



PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS)
{
     PHP_MD5_CTX md5_context;
     PHP_SHA1_CTX sha1_context;
     unsigned char digest[21];
     int digest_len;
     int j;
     char *buf;
     struct timeval tv;
     zval **array;
     zval **token;
     char *remote_addr = NULL;
                    
     gettimeofday(&tv, NULL);


     if (zend_hash_find(&EG(symbol_table), "_SERVER",
                     sizeof("_SERVER"), (void **) &array) == SUCCESS &&
                 Z_TYPE_PP(array) == IS_ARRAY &&
                 zend_hash_find(Z_ARRVAL_PP(array), "REMOTE_ADDR",
                     sizeof("REMOTE_ADDR"), (void **) &token) == SUCCESS) {
             remote_addr = Z_STRVAL_PP(token);
     }


     buf = emalloc(100);


     /* maximum 15+19+19+10 bytes */
     sprintf(buf, "%.15s%ld%ld%0.8f", remote_addr ? remote_addr : "",
                 tv.tv_sec, tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);


     switch (PS(hash_func)) {
     case PS_HASH_FUNC_MD5:
             PHP_MD5Init(&md5_context);
             PHP_MD5Update(&md5_context, buf, strlen(buf));
             digest_len = 16;
             break;
     case PS_HASH_FUNC_SHA1:
             PHP_SHA1Init(&sha1_context);
             PHP_SHA1Update(&sha1_context, buf, strlen(buf));
             digest_len = 20;
             break;
             efree(buf);
             return NULL;
     }


     if (PS(entropy_length) > 0) {
             int fd;


             fd = VCWD_OPEN(PS(entropy_file), O_RDONLY);
             if (fd >= 0) {
                 unsigned char rbuf[2048];
                 int n;
                 int to_read = PS(entropy_length);


                 while (to_read > 0) {
                     n = read(fd, rbuf, MIN(to_read, sizeof(rbuf)));
                     if (n <= 0) break;


                     switch (PS(hash_func)) {
                     case PS_HASH_FUNC_MD5:
                             PHP_MD5Update(&md5_context, rbuf, n);
                             break;
                     case PS_HASH_FUNC_SHA1:
                             PHP_SHA1Update(&sha1_context, rbuf, n);
                             break;
                     }
                     to_read -= n;
                 }
                 close(fd);
             }
     }


     switch (PS(hash_func)) {
     case PS_HASH_FUNC_MD5:
             PHP_MD5Final(digest, &md5_context);
             break;
     case PS_HASH_FUNC_SHA1:
             PHP_SHA1Final(digest, &sha1_context);
             break;
     }


     if (PS(hash_bits_per_character) < 4
                 || PS(hash_bits_per_character) > 6) {
             PS(hash_bits_per_character) = 4;


             php_error_docref(NULL TSRMLS_CC, E_WARNING, "The ini setting hash_bits_per_character is out of range (should be 4, 5,
or 6) - using 4 for now");
     }
     j = (int) (bin_to_readable(digest, digest_len, buf, PS(hash_bits_per_character)) - buf);


     if (newlen)
             *newlen = j;
     return buf;
}

可以看出它包含客户端地址、服务器时间(秒&微秒),通过SHA1或MD5来构造哈稀。
Session的工作原理描述如下:
1、        提出Session请求,服务器生成一个唯一的SessionID,发送给客户端保存
2、        服务器根据Session的保存方式,创建数据保存空间(如文件)
3、        初始化特殊变量$_SESSION
4、        对Session操作,改变数据
5、        将结构化数据整理成可逆向的字符串(如序列化,具体可参考php.ini)
6、        保存到空间(如文件)
7、        换页后,客户端保存这个SessionID(Cookie是随HTTP请求发送的,所以要跨页后才生效)
8、        之后每次都由已保存的SessionID初始化Session。Session有一个指定的生存周期,超时之后Session会失效,数据也会丢失,需要重新创建。
以上是Session的工作原理(不是过程),PHP默认的Session保存方式是文件形式。每一个Session一个文件,文件名为sess_[SessionID],可能保存在/tmp中或者其它地方。PHP默认所有Session文件是保存在一起的,这样文件多了的话会有问题,还可以修改参数,改成Hash目录的方式。上一次贴过一个模拟Session的class,有兴趣的可以去翻翻。

PHP提供一个自定义Session的很方便的方法:session_set_save_handle函数,可以用用户自定义的函数来代替系统的保存方式,包括open、close、read、write、destroy、gc六种操作,操作者可以很方便地用这种方式修改Session,比如修改成将Session保存进数据库等。

有人说过,Session就是给懒人用的。在大型的应用中,默认的Session工作方式是不够的,在性能、效率和安全性上都有问题,所以出现了很多特殊的Session方式甚至专用的Session产品。比如很多通用系统,都不使用系统Session,类似PHPBB、Discuz!这样的论坛,它们会用Cookie来模拟一些Session动作,同时也会用数据库来完成其它的部分。一方面可以提高性能,另一方面也可以方便跟踪和统计,比如获取某用户当前在哪个版块哪个帖子,在干什么,统计在线用户情况等,而且也可以使通用系统少受服务器尤其是虚拟主机的限制。

Session在大型系统里所面对的问题主要集中在1、效率;2、共享;3跨域支持这三个方面。效率自不用说——世界上本没有Session,有了人登陆,自然就出现了Session……登陆的人多了,自然就出现了很多Session,几百万人登陆,自然就出现了几百万的Session,呵呵。共享问题出现在多服务器、负载均衡等体系中,而且Session需要集中管理。跨域支持其实和Session关系并不是很大,主要要解决的是如何在客户端保存合法的ID。因为Cookie是有域属性的,所以必须解决这个跳转关系,才能实现网络通行证等单点集中登陆的效果(我一直觉得这玩意挺变态的,哈哈)。

在《大型》(一)中我提到了一些cache的实现和保存方法,比较集中的热点是tmpfs,其实tmpfs可以做很多事,它比共享内存更适合做大量数据的交换。其它的像memcached等内存数据管理方式也很不错,不过memcached有一个很讨厌的问题是不能很方便地取得变量列表,也就是说过了一段时间之后你可能自己都不知道曾经在里面存过什么……
如果我们需要用一个另类的方式保存Session,根据上面的结论,我们要找一个可以提供效率和性能,并且可以提供共享的方式。以前尝试过在memcached里保存Session,因为memcached是可以通过网络访问的,但是它的那个讨厌的毛病使得不好做统计,而且保存得也是乱乱的。此外也尝试过以文件的方式保存,然后通过NFS来做网络共享,但是NFS在这种小文件密集操作情况下的效率是比较差的,而且也不稳定。比较“正常的”内存表方式也有它致命的弱点——内存表字段太短(HEAP里不支持TEXT类字段)。所以以上的方式都不“另类”
所以我需要的是一个可以提供TCP访问的透明的Session保存方式。我选择了SQLite,(当然也可以用文件),SQLite本身是一个轻型的文件数据库系统,它可以提供很方便很标准的SQL操作方式和相对不错的负载能力,更重要的是它很快,而且是万全基于磁盘文件的。但是它本身不支持网络访问。在解决IO的问题上,可以把数据库文件创建在tmpfs上来解决物理磁盘带来的性能瓶颈,反正Session也是易失数据,没有必要长期保存的。在解决共享的问题上,我创建了一个TCP Server:
TCP Server由对外的端口监听方式提供服务,像Apache等守护类程序一样。MySQL也可以对外提供网络访问(那个3306),但是MySQL的性质和Apache不同,MySQL虽然和Web开发关系比较密切,但是MySQL在更多的情况下需要提供长连接和持续连接,而且它不能处理非常大的并发,而且对于本地访问,MySQL是通过sock方式连接的(UNIX)而不是通过TCP端口。Apache则不同,首先Apache没有必要提供特殊的对本地优化的连接方式,因为没人在服务器上浏览自己(调试除外),其次Apache处理的连接绝大部分是HTTP请求,是瞬时的,它可能会面对一段时间内非常大的访问高峰,所以它需要提供非常好的连接响应。UNIX上的Apache默认是工作在“预创建进程”的方式下,它会在服务启动后,生成一个pid文件,里面保存了本次启动的主进程的进程号,然后每隔一个比较短的时间(比如1秒)fork出一个子进程直到满足一定数量(比如5个),这个时候我们会在ps的时候看见若干个httpd进程,其中一个是主进程,它可能是由root身份启动并运行,其它的是它fork出的子进程,它们由apache用户身份执行(比如nobody,httpd.conf中可调整)。这些子进程来处理连接请求,当它们收到连接请求后,Apache会再次fork出一个新的子进程来保证有足够多的空闲子进程来处理连接,因为本身fork是需要时间的,所以预选创建好这些等待服务请求的子进程可以提高连接性能。而且HTTP的请求从开始到完成的时间通常很短,所以子进程会再次闲下来阻塞。我们经常会看见一个运行了一段时间的服务器上有一大堆的httpd进程,即使当前没几个人在访问。当然子进程不是无休止地创建,它也有一个上限,httpd.conf中可以调整最大进程数,最小空闲进程数等参数。
Apache2.0之后提供了特殊的thread方式,它避免了每个进入的连接都要fork的开销,它由在一个进程内由多个线程来循环处理accept(),它叫做MPM,它是由多线程和预创建进程混合的服务方式,提供比单纯多进程方式更好的连接性能。

我的Session服务器就需要使用类似Web服务器的这种处理短连接的方式。开发这样的一个TCP Server可以使用很多种语言中都比较通用的sock类函数,PHP中也有很好的socket支持,可以在加载sockets模块之后做一些比较复杂的网络开发。但是PHP本身是不支持多线程的,它的socket实现级别也不够底层,同时PHP对多进程的支持也不够好(有些人说PHP不能fork,其实还是可以由变通的方法实现的,后面会附上一些这样的内容)。我们可以选择C、Perl一类的语言来完成这个工作,这次我选择了Perl。Perl的网络性能和IO性能其实是很好的,使用起来也非常方便,Perl目前在程序开发方面不够流行我总觉得更多地“归功”于它怪异的程序结构和语法(它确实很怪……)。
本来可以完全从0开始实现一个预创建进程/线程的TCP Server,但是懒人就要会偷懒,CPAN中有一个包,可以非常方便地创建一个TCP Server,同时它有各种可调参数,这个包是NetServer::Generic,可以通过CPAN安装。
接下来需要定义一些参数,首先是默认端口,我随便写了一个34343,为了和Sky同学的SessionD保持兼容。然后是命令格式,根据Session的使用特性,定义了五种操作:
+:表示创建/更新一个Session
-:表示删除一个Session
?:表示取得一个Session的内容
!:表示标识一个Session为过期
*:表示清理Session表,即删除已过期的Session数据
这其中只有?操作要返回字符串类数据。命令格式包括三段,第一个字节为操作符号,就是以上的五种字符,然后是分隔符,我写的是两个冒号,后面是32位的SessionID,然后再是分割附,最后是变长的数据。这样,一条完整的创建指令可能是:
+::12341234123412341234123412341234::Hello World!!!
取得这个Session值的指令可能是:
?::12341234123412341234123412341234::0
发送成功这条指令的结果是服务器返回“Hello World!!!”
在这五种合法的指令之外,定义如果接收到其它的指令就表示操作结束,断开连接。

在服务开始之前,需要创建一个SQLite数据库来保存Session数据,可以根据需要创建一些相应的字段,同时每次服务启动的时候都要检查并清空它(如果是tmpfs上的,重启服务器之后文件就丢掉了)。我定义了如下的一个表:


SQL代码:


"CREATE TABLE `sess` (
                `sid` VARCHAR(32) NOT NULL UNIQUE,
                `expire` INT NOT NULL,
                `sess_data` TEXT NOT NULL)";


然后在开始服务之前要确定一个绑定地址,可以是一个IP,可以是一个域名,或者绑定本机所有(0.0.0.0)

整个服务程序如下:



#!/usr/local/bin/perl                
                                            
use NetServer::Generic;            
use DBI;                                
use Switch;
                                
my $server = sub {
             while (defined ($tcp_input = <STDIN>)) {
                     chomp $tcp_input;
                                            
                     my ($sess_cmd, $sess_sid, $sess_data) = split (/::/, $tcp_input, 3);
                     my $sql;            
                     my $sth;
                     my $now = time ();
                     my $expire = $now + 900;
                     chomp $sess_sid;
                     chomp $sess_data;
                    
                     switch ($sess_cmd) {
                                 case "+" {
                                             #Create a Session
                                             $sth = $db->prepare ($sql);
                                             $sth->execute ();
                                 }


                                 case "-" {
                                             #Delete a Session
                                             $sql = "DELETE FROM `sess` WHERE `sid` = '$sess_sid'";
                                             $sth = $db->prepare ($sql);
                                             $sth->execute ();
                                 }
                        
                                 case "!" {
                                             #Mark a Session as EXPIRED!!!
                                             $sql = "UPDATE INTO `sess` SET `expire` = $now WHERE `sid` = '$sess_sid'";
                                             $sth = $db->prepare ($sql);
                                             $sth->execute ();
                                 }
                                
                                 case "?" {
                                             #Get a Session from SQLite
                                             $sql = "SELECT `sess_data` FROM `sess` WHERE `sid` = '$sess_sid' AND `expire` >= $now";
                                             $sth = $db->prepare ($sql);
                                             $sth->execute ();
                                             my @row = $sth->fetchrow_array();
                                             print "$row[0]\n";


                                 }


                                 case "*" {
                                             #Clear all Session which expired
                                             $sql = "DELETE FROM `sess` WHERE `expire` < $expire";
                                             $sth = $db->prepare ($sql);
                                             $sth->execute ();
                                 }


                                 else {
                                             return 0;
                                 }
                     }
             }
};


my $create_sql = "CREATE TABLE `sess` (
                     `sid` VARCHAR(32) NOT NULL UNIQUE,
                     `expire` INT NOT NULL,
                     `sess_data` TEXT NOT NULL)";
$db = DBI->connect('dbi:SQLite:/www/htdocs/sess.db');


my $sth = $db->prepare ("DROP TABLE IF EXISTS `sess`");
$sth->execute () || print $db->get_error . "\n";


$sth = $db->prepare ($create_sql);
$sth->execute () || print $db->get_error . "\n";


my $port = 34343;
my $hostname = 'admin.qiaoqiao.org';


my (%config) = ('port' => $port, 'callback' => $server,
                     'mode' => 'prefork',
                     'start_servers' => 5,
                     'max_servers' => 255,
                     'min_spare_servers' => 2,
                     'hostname' => $hostname);


my ($TCP_Server) = new NetServer::Generic (%config);
print "BSM_Session Daemon 1.01 Alpha\nBy Dr.NP 2006 <bssoft\@263.net>\nServer Started...Bind on $hostname\n";
$TCP_Server->run();


上面使用DBI来操作SQLite数据库,为了看起来比较清晰,使用了Switch包,Perl本身是没有明确的Switch-case结构的,因为它不需要。NetServer::Generic具体使用方式可以查阅相关文档,这里创建的是一个prefork方式(预创建进程)的服务器程序,绑定在admin.qiaoqiao.org的34343端口上。启动这个程序,它就可以开始提供对外的网络服务了。它会接受并处理上面提到的五种指令,然后在?的时候反馈一个字符串接一个换行符,但并不马上断开连接。
具体的程序不做太多解释,因为这次不是以讲述perl为主。
服务器端大概就是这个样子,只需要mount或者fstab一个tmpfs,同时修改这个脚本把SQLite的库文件创建在tmpfs上可以了。客户端的情况要比这个清晰一些,只要连接到服务器上,发送相应的指令并读取服务器反馈的数据就可以了。根据这样的一个情况,我写了一个class,这次是PHP的……



<?php
/* BSM_Session Client
*     By Dr.NP 07-19-2006
*     BSM_Session Daemon is a very BT session handle...Hahahahahaha
*/


class Bsm_Session
{
     var $hostname                 = 'localhost';
     var $port                     = 34343;
     var $sock_fp                 = false;
     var $sess_id                 = '';
     var $sess_data                 = array ();
     var $sid_cookie_var             = 'BSM_SESS_ID';
     var $sess_expire             = '3600';
     var $cookie_domain             = 'localhost';
     var $cookie_path             = '/';
    
     function BSM_Session ($hostname = '', $port = '', $sid_cookie_var = '', $sess_expire = '', $cookie_domain = '')
     {
             if (trim ($hostname))
                 $this->hostname = trim ($hostname);
            
             if (intval (trim ($port)))
                 $this->port = intval (trim ($port));
            
             if (trim ($sid_cookie_var))
                 $this->sid_cookie_var = trim ($sid_cookie_var);
            
             if (intval (trim ($sess_expire)))
                 $this->sess_expire = intval (trim ($sess_expire));
            
             if (trim ($cookie_domain))
                 $this->cookie_domain = trim ($cookie_domain);
            
             $socket = socket_create (AF_INET, SOCK_STREAM, SOL_TCP);
             if (!$socket)
                 return false;
            
             $result = socket_connect($socket, $this->hostname, $this->port);
            
             if ($result) {
                 $this->sock_fp = $socket;
                 $this->_gen_sid ();
                
                 $data = $this->_read_daemon ();
                 $this->sess_data = is_array (@unserialize ($data)) ? @unserialize ($data) : array ();
                
                 return true;
             }
            
             else
                 return false;
     }
    
     function put ($key, $value)
     {
             $this->sess_data[$key] = $value;
             $data = serialize ($this->sess_data);
             return $this->_write_daemon ($data);
     }
    
     function get ($key)
     {
             return $this->sess_data[$key];
     }
    
     function destroy ()
     {
             $this->sess_data = array ();
             return $this->_del_daemon ();
     }
    
     function _gen_sid ()
     {
             if ($_COOKIE[$this->sid_cookie_var])
                 $this->sess_id = $_COOKIE[$this->sid_cookie_var];
            
             else {
                 $client_ip = $_SERVER['REMOTE_ADDR'];
                 $this->sess_id = md5 (uniqid (microtime () . $client_ip));
                 @setcookie ($this->sid_cookie_var, $this->sess_id, $this->sess_expire + time(), $this->cookie_path, $this->cookie_domain);
                 $_COOKIE[$this->sid_cookie_var] = $this->sess_id;
             }
             return;
     }
    
     function _read_daemon ()
     {
             $cmd = '?::' . $this->sess_id . "::0rn";
             socket_write ($this->sock_fp, $cmd);
             $ret = socket_read ($this->sock_fp, 16384, PHP_NORMAL_READ);
             if (!$ret)
                 $ret = '';
            
             return $ret;
     }
    
     function _write_daemon ($data)
     {
             $cmd = '+::' . $this->sess_id . '::' . $data . "rn";
             $bytes = socket_write ($this->sock_fp, $cmd);
            
             return;
     }
    
     function _del_daemon ()
     {
             $cmd = '-::' . $this->sess_id . "::0n";
             socket_write ($this->sock_fp, $cmd);
            
             return;
     }
    
     function _set_expire_daemon ()
     {
             $cmd = '!::' . $this->sess_id . "::0n";
             socket_write ($this->sock_fp, $cmd);
            
             return;
     }
    
     function _flush_daemon ()
     {
             $cmd = '*::' . $this->sess_id . "::0n";
             socket_write ($this->sock_fp, $cmd);
            
             return;
     }
    
     function _disconnect ()
     {
             $cmd = 'QUIT';
             socket_write ($this->sock_fp, $cmd);
     }
}


?>


我还是一如既往地懒得写注释,因为它比较好理解,需要提及的是在做_read_daemon()的时候,也就是在发送?指令之后,socket_read要使用PHP_NORMAL_READ方式。在php4.1之后,它就不再是默认的了。因为服务器端接受?指令并返回Session结果之后并不主动关闭连接,而只是返回一个换行符,所以客户端不能傻傻地等待它送出所有数据。
这个class怎么用,其实一看就知道……呵呵,唯一要注意的就是cookie在下一个HTTP请求的时候才生效。
之后的事情就是要测试它的性能和负载能力,不太方便得到具体的数值,但是可以使用类似ab的测试工具或采用“DOS自己”的变态办法来观察它的性能。NetServer::Generic的性能是非常好的,至于SQLite,如果你信不过它,完全可以换成别的,或者是用文件的方式实现保存,对于客户端来说,用什么都没关系,鬼才会在乎服务器上用什么保存数据,就像鬼才会在意浏览网页时候Apache在做什么一样(在专门研究这个的人群中,不排除鬼的存在)。
OK了,我创建了一个另类的Session实现结构,呵呵,如果聪明的话,完全可以用它来做别的,session只不过是个特殊一点的变量而已……

附一个PHP创建子进程的例子(说话要算数):

 


<?php


require_once ('init.php');


if ($pid != $shm->get_var (SHM_VAR_PID)) {
             // Child Process
             $fh = fopen ('temp.pid', 'wb');
             fwrite ($fh, $pid);
             fclose ($fh);
}


else {
             // Master Process
             $descriptorspec = array(
                     0 => array("pipe", "r"),
                     1 => array("pipe", "w"),
                     2 => array("file", "error-output.txt", "a")
             );


             $res = proc_open ('php ' . basename (__FILE__), $descriptorspec, $pipe);
             $ret = proc_close ($res);


             $shm->put_var (SHM_VAR_SYS_RUN, false);
             $shm->put_var (SHM_VAR_PID, 0);
}


exit (0);
?>


这个程序使用了我上次给出的一个SHM类,用来保存pid,或者你可以把它保存在一个文件里。这个程序模拟了fork(),fork()实际上就是一个完全的自我复制,所不同的是有不同的pid和ppid,程序可以根据它们来区分当前的是父进程还是子进程,以完成不同的操作。上面这个程序中,如果pid不等于SHM里保存的pid,说明它是子进程,它要完成在temp.pid里写入它自己的进程号这样一个动作。如果当前进程是主进程,表明这个程序是由shell运行的,它要保存自己的进程号在SHM中,同时用proc_open创建一个“自身”,在子进程结束动作之后,父进程结束。Init.php内容如下:

 



<?php
require_once ('constants.inc.php');
require_once ('basic.function.php');
require_once ('shm.inc.php');


// Fetch Self PID
$pid = posix_getpid ();


$shm = new BsmShm ();
if (!$shm->get_var (SHM_VAR_SYS_RUN) || !$shm->get_var (SHM_VAR_PID)) {
             // Master Process Startup...


             $shm->put_var (SHM_VAR_SYS_RUN, true);
             $shm->put_var (SHM_VAR_PID, $pid);
}


?>


NP博士

关键词: 代码收藏 , php

上一篇: 哦,明白了
下一篇: asp木马的一个漏洞和利用

相关文章
目前还没有人评论,您发表点看法?
发表评论

评论内容 (必填):