PHP大型Web应用入门(一)

这里所说的“大型”应用不是说像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的一些片断:


CODE:[Copy to clipboard]<?php
if (!defined(\'IN_BSG\')) {
exit;
}
?>
上面的代码保证它只能被合法的程序所调用,而不会被其它的文件include。如果正在执行的程序没有定义一个\'IN_BSG\'常量,它在include这个common.inc.php之后程序会终止。


CODE:[Copy to clipboard]<?php
list($usec, $sec) = explode(\" \", microtime());
$page_time_start = $usec + $sec;
?>
这两行大家可能都会比较熟悉,这是计算程序的开始执行时间的。在程序结束之前,还会再计算一下这个,为的是得出程序执行所耗费的时间。如果你不在意这些,可以放心地把它注释掉。


CODE:[Copy to clipboard]<?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(它常会带来危险并使人感到困惑),我们要把它随便设置的那些全局变量删掉。


CODE:[Copy to clipboard]<?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);
?>
上面的一陀,显而易见,它在做转义过滤所有来自客户端的输入。

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


CODE:[Copy to clipboard]<?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文件


CODE:[Copy to clipboard]<?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中设置。


CODE:[Copy to clipboard]<?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);
?>
创建成功后,记得把密码清空


CODE:[Copy to clipboard]<?php
// We do not need this any longer, unset for safety purposes
unset($global_db_pass);
?>
创建日志对象


CODE:[Copy to clipboard]<?php
// Init Log
$log = new BsmLog (\'bsg\');
?>
这又是一个精彩部分,创建一个共享内存对象


CODE:[Copy to clipboard]<?php
// Init the Shared Memory
$shm = new BsmShm;
if ($shm->shm_id) {
define (\'SHM_SUPPORT\', true);
}
?>
如果系统是第一次运行,则在共享内存中标记一个运行标志SHM_VAR_SYS_RUN = true(系统的constants.inc.php是常量表,里面定义了系统要使用的常量)


CODE:[Copy to clipboard]<?php
if (defined (\'SHM_SUPPORT\') && !@$shm->get_var (SHM_VAR_SYS_RUN)) {
$shm->put_var (SHM_VAR_SYS_RUN, true);
}
?>
我写了一个Session类,也许它的效率并不很好,我只是写来玩玩……如果你觉得它的效率不行,可以使用系统Session,或者使用Sky同学的SessionD,哈哈——做个广告……


CODE:[Copy to clipboard]<?php
// Init the User Defined Session
$mSession = array ();
$sess = new BsmSession;
$sess->mSession_Start ();
?>
一长串乱七八糟的代码,只是为了获得访问者IP:


CODE:[Copy to clipboard]<?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上的一个文件,优化它是非常有必要的,因为这批数据被使用得太频繁了。


CODE:[Copy to clipboard]<?php
// Grab global variables, re-cache if necessary
$CONF = obtain_config();
?>
然后是处理gz压缩。下面这些内容是明显通用化的,甚至包括检查php版本。前面我已经说过,我们是在控制自己的独立服务器,所以你完全可以把它简写成你自己需要的样子甚至完全删掉它。


CODE:[Copy to clipboard]<?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 ();
?>
下面是初始化模板类,这个模板类~~似乎没人见过,我也没拿出来给人看过,呵呵……


CODE:[Copy to clipboard]<?php
// Init the Template Object
$tpl = new BsmTpl (\'../../templates/\', $tpl_c_root);
$tpl->set_tpl_name (\'seepic\'); //seepic是我临时写的一个模板名字,实际使用时应该用的是$CONF里的值。
?>
初始化模板之后是确认语言,因为有可能会用到多语言的语言包文件。


CODE:[Copy to clipboard]<?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文件的任务完成了。


CODE:[Copy to clipboard]<?php
include $tpl->set_language ($langMeta);
?>
?>
下面来看看config文件里有什么,我会逐段解释:


CODE:[Copy to clipboard]<?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_\'; //数据表前缀(我都不知道我写它干什么……整个服务器都是我的)

// Para_DB_Member:
$member_db_dbms = \'sqlite\'; //可以再定义一组数据库连接,用了sqlite,比如保存个人信息什么的……呵呵

$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函数:


CODE:[Copy to clipboard]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更底层,只接受字符串数据。


CODE:[Copy to clipboard]<?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文件里,当然这个很容易被改进。


CODE:[Copy to clipboard]<?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},编译的结果也只有一个文件。
这个模板类用起来是如此地简单方便,更重要的是,它确实很快~~呵呵,我从来不否认我有时候也会做一些比较有用的事,哈哈:


CODE:[Copy to clipboard]<?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);

$template = preg_replace (\"/{(\\$[a-zA-Z0-9_[]\'\\\"$x7f-xff]+)}/s\", \"<?=\\1?>\", $template);
$template = preg_replace (\"/$var_regexp/es\", \"addquote(\'<?=\\1?>\')\", $template);
$template = preg_replace (\"/<?=<?=$var_regexp?>?>/es\", \"addquote(\'<?=\\1?>\')\", $template);

$template = preg_replace (\"/s*{evals+(.+?)}s*/ies\", \"stripvtags(\'n<?php \\1 ?>n\', \'\')\", $template);
$template = preg_replace (\"/s*{elseifs+(.+?)}s*/ies\", \"stripvtags(\'n<?php } elseif(\\1) { ?>n\', \'\')\", $template);
$template = preg_replace (\"/s*{else}s*/is\", \"n<?php } else { ?>n\", $template);
$template = preg_replace (\"/s*{dates+(.+?)s+(.+?)}s*/ies\", \"stripvtags(\'n<?php echo date (\\1, \\2 ) ?>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) { ?>\',\'n\\3n<? } } ?>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) { ?>\',\'n\\4n<? } } ?>n\')\", $template);
$template = preg_replace (\"/s*{ifs+(.+?)}s*(.+?)s*{/if}s*/ies\", \"stripvtags(\'n<? if(\\1) { ?>\',\'n\\2n<? } ?>n\')\", $template);
}

$template = preg_replace (\"/{$const_regexp}/s\", \"<?=\\1?>\", $template);
$template = preg_replace (\"/ ?>[nr]*<? /s\", \" \", $template);

/*
$int = preg_match_all (\"/->parse(\'<?=(.+?)?>\')/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(); ?>\', $template);
$template = str_replace (\'<!--NUM_QUERIES!-->\', \'<?php echo \' . $this->default_db_handle . \'->num_queries; ?>\', $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]*)?>/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
附上那个压缩/解压的类:


CODE:[Copy to clipboard]<?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;
}
}
?>
昨天实在太困了,写的什么自己也不是很清楚~这是(一),不保证什么时候写(二)、(三)、(四)……(如果有的话)。希望大家的老板可以把省掉的买服务器的钱中的一部分发下来做奖金,嘿嘿……

转自:http://www.ligf.cn/archives/200603/335.html

购物商城项目采用PHP+mysql有以及html+css jq以及layer.js datatables bootstorap等插件等开发,采用了MVC模式,建立一个完善的电商系统,通过不同用户的不同需求,进行相应的调配和处理,提高对购买用户进行配置….zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值