PHP沉思录

jowin 贡献于2015-02-13

作者 User  创建于2011-01-02 06:25:00   修改者jowin  修改于2014-10-16 05:35:00字数31658

文档摘要:PHP沉思录(一)工作模型  PHP的工作模型非常特殊。从某种程度上说,PHP和ASP、ASP.NET、JSP/Servlet等流行的Web技术,有着本质上的区别。  以Java为例,Java在Web应用领域,有两种技术:Java Servlet和JSP(Java Server Page)。Java Servlet是一种特殊类型的Java程序,它通过实现相关接口,处理Web服务器发送过来的请求,完成相应的工作。JSP在形式上是一种类似于PHP的脚本,但是事实上,它最后也被编译成Servlet。也就是说,在Java解决方案中,JSP和Servlet是作为独立的Java应用程序执行的,它们在初始化之后就驻留内存,通过特定的接口和Web服务器通信,完成相应工作。除非被显式地重启,否则它们不会终止
关键词:

本文发表于《程序员》5月号      PHP沉思录(一) 工作模型   PHP的工作模型非常特殊。从某种程度上说,PHP和ASP、ASP.NET、JSP/Servlet等流行的Web技术,有着本质上的区别。   以Java为例,Java在Web应用领域,有两种技术:Java Servlet和JSP(Java Server Page)。Java Servlet是一种特殊类型的Java程序,它通过实现相关接口,处理Web服务器发送过来的请求,完成相应的工作。JSP在形式上是一种类似于PHP的脚本,但是事实上,它最后也被编译成Servlet。也就是说,在Java解决方案中,JSP和Servlet是作为独立的Java应用程序执行的,它们在初始化之后就驻留内存,通过特定的接口和Web服务器通信,完成相应工作。除非被显式地重启,否则它们不会终止。因此,可以在JSP和Servlet中使用各种缓存技术,例如数据库连接池。   ASP.NET的机制与此类似。至于ASP,虽然也是一种解释型语言,但是仍然提供了Application对象来存放应用程序级的全局变量,它依托于ASP解释器在IIS中驻留的进程,在整个应用程序的生命期有效。   PHP却完全不是这样。作为一种纯解释型语言,PHP脚本在每次被解释时进行初始化,在解释完毕后终止运行。这种运行是互相独立的,每一次请求都会创建一个单独的进程或线程,来解释相应的页面文件。页面创建的变量和其他对象,都只在当前的页面内部可见,无法跨越页面访问。在终止运行后,页面中申请的、没有被代码显式释放的外部资源,包括内存、数据库连接、文件句柄、Socket连接等,都会被强行释放。   也就是说,PHP无法在语言级别直接访问跨越页面的变量,也无法创建驻留内存的对象。见下例:      ";   TestStaticVar();   ?>       在这个例子中,定义了一个名为StaticVarTester的类,它仅有一个公共的静态成员$StaticVar,并被初始化为0。然后,在TestStaticVar()函数中,对StaticVarTester :: $StaticVar进行累加操作,并将它打印输出。    熟悉Java或C++的开发者对这个例子应该并不陌生。$StaticVar作为StaticVarTester类的一个静态成员,只在类被装载时进行初始化,无论StaticVarTester类被实例化多少次,$StaticVar都只存在一个实例,而且不会被多次初始化。因此,当第一次调用TestStaticVar()函数时,$StaticVar进行了累加操作,值为1,并被保存。第二次调用TestStaticVar()函数,$StaticVar的值为2。    打印出来的结果和我们预料的一样:      StaticVarTester :: StaticVar = 1   StaticVarTester :: StaticVar = 2       但是,当浏览器刷新页面,再次执行这段代码时,不同的情况出现了。在Java或C++里面,$StaticVar的值会被保存并一直累加下去,我们将会看到如下的结果:      StaticVarTester :: StaticVar = 3   StaticVarTester :: StaticVar = 4   …       但是在PHP中,由于上文叙及的机制,当前页面每次都解释时,都会执行一次程序初始化和终止的过程。也就是说,每次访问时,StaticVarTester都会被重新装载,而下列这行语句       public static $StaticVar = 0;       也会被重复执行。当页面执行完成后,所有的内存空间都会被回收,$StaticVar这个变量(连同整个StaticVarTester类)也就不复存在。因此,无论刷新页面多少次,$StaticVar变量都会回到起点:先被初始化为0,然后在TestStaticVar()函数调用中被累加。所以,我们看到的结果永远是这个:      StaticVarTester :: StaticVar = 1   StaticVarTester :: StaticVar = 2      PHP这种独特的工作模型的优势在于,基本上解决了令人头疼的资源泄漏问题。Web应用的特点是大量的、短时间的并发处理,对各种资源的申请和释放工作非常频繁,很容易导致泄漏。同时,大量的动态html脚本的存在,使得追踪和调试的工作都非常困难。PHP的运行机制,以一种非常简单的方式避免了这个问题,同时也避免了将程序员带入到繁琐的缓冲池和同步等问题中去。在实践中,基于PHP的应用往往比基于Java或.NET的应用更加稳定,不会出现由于某个页面的BUG而导致整个站点崩溃的问题。(相比之下,Java或.NET应用可能因为缓冲池崩溃或其他的非法操作,而导致整个站点崩溃。)后果是,即使PHP程序员水平不高,也无法写出使整个应用崩溃的代码。PHP脚本可以一次调用极多的资源,从而导致页面执行极为缓慢,但是执行完毕后所有的资源都会被释放,应用仍然不会崩溃。甚至即使执行了一个死循环,也会在30秒(默认时间)后因为超时而中止。从理论上来说,基于PHP的应用是不太可能崩溃的,因为它的运行机制决定它不存在常规的崩溃这个问题。在实践中,很多开发者也认为PHP是最稳定的Web应用。   但是,这种机制的缺点也非常明显。最直接的后果是,PHP在语言级别无法实现跨页面的缓冲机制。这种缓冲机制缺失造成的影响,可以分成两个方面:   一是对象的缓冲。如我们所知,很多设计模式都依赖于对象的缓冲机制,对于需要频繁应付大量并发的服务端软件更是如此。因此,对象缓冲的缺失,理论上会极大地降低速度。但是,由于PHP本身的定位和工作机制等原因,它在实际工作中的速度非常快。就作者自己的经验来看,在小型的Web应用中,PHP至少不比Java慢。在大型的应用中,为了榨干每一分硬件资源,即使PHP本身足够快,一个优秀的对象缓冲机制仍然是必要的。在这种情况下,可以使用第三方的内存缓冲软件,如Memcached。由于Memcached本身的优异特性(高性能,支持跨服务器的分布式存储,和PHP的无缝集成等),在大型的PHP应用中,Memcached几乎已经成为不可或缺的基础设施了。比起使用PHP语言自己实现对象缓冲来,这种第三方解决方案似乎更好一些。   二是数据库连接的缓冲。对MySQL,PHP提供了一种内置的数据库缓冲机制,使用起来非常简单,程序员需要做的只是用mysql_pconnect()代替mysql_connect()来打开数据库而已。PHP会自动回收被废弃的数据库连接,以供重复使用。具有讽刺意味的是,在实际应用中,这种持久性数据库连接往往会导致数据库连接的伪泄漏现象:在某个时间,并发的数据库连接过多,超过了MySQL的最大连接数,从而导致新的进程无法连接数据库。但是过一段时间,当并发数减少时,PHP会释放掉一些连接,网站又会恢复正常。出现这种现象的原因是,当使用pconnect时,Apache的httpd进程会不释放connect,而当Apache的httpd进程数超过了mysql的最大连接数时,就会出现无法连接的情况。因此,需要小心地调整Apache和Mysql的配置,以使Apache的httpd进程数不会超出MySQL的最大连接数。在某些情况下,一些有经验的PHP程序员宁可继续使用mysql_connect(),而不是mysql_pconnect()。   就作者所知,在PHP未来的roadmap中,对于工作模型这一部分,没有根本性的变动。这是PHP的缺点,也是PHP的优势,从本质上说,这就是PHP的独特之处。因此,我们很难期待PHP在近期内会对这一问题做出重大的改变。但是,在对待这个问题带来的一系列后果时,我们必须谨慎应对。      数据库访问接口    长期以来,PHP都缺乏一个象ADO或JDBC那样的统一的数据库访问接口。PHP在访问不同的数据库时,使用不同的专门API。例如,使用mysql_connect函数连接MySQL,使用ora_logon函数连接Oracle。平心而论,这种方式并没有象我们想象的那样麻烦。在真实项目中,把系统从一种数据库完全迁移到另一种数据库的要求是比较少见的,特别是对于LAMP这样的小型项目而言。而且,只要将访问数据库的代码进行了良好的封装,迁移的工作量也会较少。另外,使用专门API,在效率上多少会有一些优势。    虽然如此,PHP的开发人员仍然在努力构建PHP的统一的数据库访问接口。从PHP 5.1开始,PHP的发行包内置了PDO(PHP Data Objects,PHP数据对象)。PDO具有如下特性:    l   统一的数据库访问接口。PDO为访问不同的数据库提供了统一的接口,并且能够通过切换数据库驱动程序,方便地支持各种流行的数据库。 l   面向对象。PDO完全基于PHP 5的对象机制,因此区别于基于过程的专用API。 l   高性能。PDO的底层用C编写,比起用纯PHP开发的其他类似解决方案,有更高的性能。      一个典型的PDO应用如下例:      $pdo = new PDO("mysql:host=localhost;dbname=justtest", " mysql_user ", " mysql_password");   $query = "SELECT id, username FROM userinfo ORDER BY ID";   foreach ($pdo->query($query) as $row) {    echo $row['id']." | ".$row['username']."
";   }      但是,PDO还有一个更重要的问题没有解决,那就是对数据集的抽象。   无论是ADO还是JDBC,除了提供统一的数据库访问接口以外,也提供了对数据集的抽象。也就是说,在通过ADO/JDBC取回数据集结果以后,这些数据集以统一的格式被存放在RecordSet/RowSet对象中,业务逻辑代码只需要与数据集对象进行交互即可。对数据集进行抽象的直接后果,是彻底地分离了业务逻辑层和数据库访问层的代码,并且也在某种程度上起到了OR Mapping的效果。   自从ADO.NET出现后,数据集的抽象又有了一次不小的进步。和ADO相比,ADO.NET中的DataTable/DataSet类的主要新特性如下:    l   非连接性。在传统的ADO模型中,数据集需要占用一个数据库连接,直到所有工作完成。一旦连接被关闭,数据集的内容也就失效了。ADO.NET中的数据集是非连接的,也就是说,当连接被关闭后,数据集中的内容仍然保存。这种非连接性带来的直接后果是,数据库连接可以被最大限度地利用,因为一旦工作完成就可以将连接返回到数据库连接池中。(ADO也支持非连接的数据集,但是需要程序员自己实现,而ADO.NET的数据集在本质上就是非连接的。) l   自描述性。ADO.NET中的数据集是完全自我描述的,而且具有完备的metadata,其内容不但可以从任何特定的数据库生成,而且可以由代码动态生成。DataSet可以跟踪数据的变化,并完成相应的操作。 l   互操作性。由于非连接性和自描述性,ADO.NET中的数据集可以非常方便地在网络之间进行传输。DataSet可以序列化/反序列化为XML或其他特定的格式。这样,DataSet不但可以用于同一平台的分布式网络环境,而且可以用于异构网络环境。      Java从J2SE 5.0开始内置了CachedRowSet,其原理和ADO.NET的数据集类似。Borland开发的用于取代BDE的新一代数据库引擎dbExpress,其改进也与此类似。   与之对比,PHP中对数据集的支持显得非常原始。无论是传统的API还是PDO,取回的数据仅仅表现为数组,并且没有任何缓存机制。这意味着,在所有需要访问数据集的地方,都必须频繁地使用直接访问数据库的API。下面是一个使用mysql API的例子:      $link = mysql_connect('localhost', 'mysql_user', 'mysql_password');   if (!$link) {    die('Could not connect: ' . mysql_error());   }   mysql_select_db("justtest");   $result = mysql_query("SELECT id, username FROM userinfo ORDER BY ID");   while ($row = mysql_fetch_array($result)) {    echo $row['id']." | ".$row['username']."
";   }   mysql_close($link);       这样做的后果是业务逻辑和访问数据库的代码无法分离,在规模较大的系统中尤其严重。    就作者所知,PHP官方没有提供支持抽象数据集的计划。但是,自己实现这样一个数据集并不是一件难事。作者参照ADO.NET的架构,使用纯PHP代码编写了一个规模非常小的数据抽象类库,姑且称之为MyPDO。MyPDO大约有1300行代码,在几个真实项目中工作得很好。由于篇幅所限,本文不列出它的所有代码,仅仅给出几个最主要的类的描述:    DataAdapter接口:定义了所有与数据库操作相关的方法。 l   l   ConceptDataAdapter类:实现了DataAdapter,封装了访问特定数据库的代码。如MySqlDataAdapter。 l   DataSet类:实现了自描述的抽象数据集,与具体数据库无关。可以自我跟踪数据的变化。 l   SqlCommandBuilder类:可以跟据DataSet类的内容,自动实现Insert、Update、Delete等操作。      下面是MyPDO的一个典型应用:       $Conn = new MySqlDataConnection(new ConnectionInfo("localhost", "mysql_username", "mysql_password", "justtest", "gbk"));    $Da = $Conn->GetDataAdapter();    $Da->SetSqlString("SELECT id, username FROM userinfo ORDER BY ID");    $Ds = new DataSet();    $Da->Fill($Ds);    $Conn->Disconnect(); // 关闭数据库连接,但$Ds仍然保存数据内容    echo $Ds; // 调用DataSet的__tostring()方法,格式化输出内容      通过替换MySqlDataConnection,可以以最小的成本实现不同数据库之间的切换。   由于上文中讨论过的PHP的工作模型,通过MyPDO实现的缓存在性能上获得的好处有限。但是,在采用Memcached的解决方案中,MyPDO还是能够带来很大的便利。因为只有基于非连接方式的数据集,才可能在Memcached这样的内存池中被缓存。   另外,由于MyPDO中的DataSet是自描述的,内置了WriteToXml和ReadFromXml方式,它无需程序员编码就可以保存为XML或从XML中还原,在网络上甚至异构平台之间进行传输和识别。在电子商务领域中,这个特性是非常有用的。 本文发表在《程序员》第7期 PHP沉思录之二 PME模型 在大规模的程序设计中,组件(component)已经成为一种非常流行的技术。常见的组件技术都基于PME模型,即属性(Property)、方法(Method)和事件(Event)。基于PME的组件技术可以方便地实现IoC(Inversion of Control,控制反转),是从IDE的plugin到应用服务器的“热发布”等许多技术的基础。 PHP从版本5开始,大大完善了对OO的支持,以前不能被应用的许多pattern现在都可以在PHP5中实现。因此,是否能够实现基于PHP的组件技术,也就成了一个值得讨论的问题。 下面对PHP对于PME模型的支持,逐一进行讨论: 属性(Property) PHP并不支持类似Delphi或者C#的property语法,但这并不是问题。Java也不支持property语法,但是通过getXXX()和setXXX()的命名约定,同样可以支持属性。PHP也可以通过这一方式来支持属性。但是,PHP提供了另一种也许更好的方法,那就是__set()和__get()方法。 在PHP中,每一个class都会自动继承__set()和__get()方法。它们的定义如下: void __set ( string name, mixed value ) mixed __get ( string name ) 这两个方法将在下列情况下被触发:当程序访问一个当前类没有显式定义的属性时。在这个时候,被访问的属性名称作为参数被传入相应的方法。任何类都可以重载__set()和__get()方法,以实现自己的功能。 如下例: class PropertyTester { public function __get($PropName) { echo "Getting Property $PropName\n"; } public function __set($PropName, $Value) { echo "Setting Property $PropName to '$Value'\n"; } } $Prop = new PropertyTester(); $Prop->Name; $Prop->Name = "some string"; 类PropertyTester重载了__set()和__get()方法,为了测试,仅仅将参数打印输出,没有做更多的工作。测试代码创建了PropertyTester类的实例,并试图读写它并不存在的一个属性Name。此时,__set()和__get()相继被调用,并打印出相关参数。它的输出结果如下: Getting Property Name Setting Property Name to 'some string' 基于这种机制,我们可以将属性的值放在一个private的List中,在读写属性时,通过重载__set()和__get()方法,读写List中的属性值。 但是,__set()和__get()方法的有趣之处远不止及。通过这两个方法,可以实现动态属性,也就是不在程序中显式定义,而是在运行时动态生成的属性。只要想想这种技术在OR Mapping中的作用就能够明白它的重要性了。配合__call()方法(用于实现动态方法,在下一节中详述),它能够取代丑陋的代码生成器(code generator)的大部分功能。 方法(Method) PHP对方法的支持比较简单,没有太多可以讨论的。值得一提的是,PHP从版本5开始支持类的静态方法(static method),这使得程序员再也不用无谓地增加许多全局函数了。 事件(Event) 事件也许是PHP遇到的最复杂的问题。PHP并没有在语法层面提供对事件的支持,我们只能考虑通过别的途径来实现。因此,我们需要先对事件的概念和其他语言对事件的实现方式进行讨论。 事件模型可以简述如下:充当事件触发者的代码本身并不处理事件,而仅仅是在事件发生时,把程序控制权转交给事件的处理者,在事件处理完成后,再收回控制权。事件触发者本身并不知道事件将会被如何处理,在大多数情况下,事件触发者的代码要先于事件处理者的代码被完成。 在传统的面向过程的语言(例如C或者PASCAL)中,事件可以通过函数指针来实现。具体来说,事件触发者定义一个函数指针,这个函数指针可以在以后被指向某个处理事件的函数。在事件发生时,调用该函数指针指向的处理函数,并将事件的上下文作为参数传入。处理完成后,控制权再回到事件触发者。 在面向对象的语言中,方法指针(指向某个类的方法的指针)取代了函数指针。以Delphi为例,事件处理的例子如下: type TNotifyEvent = procedure(Sender: TObject) of object; TMainForm = class(TForm) procedure ButtonClick(Sender: TObject); … End; Var MainForm: TMainForm; OnClick: TNotifyEvent; … 可以看出,TNotifyEvent被定义为所谓的过程类型(Procedural Type),事实上就是一个方法指针。TMainForm的ButtonClick方法是一个事件处理者,符合TNotifyEvent的签名。OnClick是一个事件触发者。在实际使用时,通过如下代码: OnClick := MainForm.ButtonClick; 将MainForm.ButtonClick方法绑定到了OnClick事件。当OnClick事件触发时,MainForm.ButtonClick方法将被调用,并且将Sender(触发事件的组件对象)作为参数传入。 回到PHP,由于PHP不支持指针,因此无法使用函数指针这一技术。但是,PHP支持所谓的“函数变量”,可以把函数赋予某个变量,其作用类似于函数指针。如下例: function EventHandler($Sender) { echo "Calling EventHandler(), arv = $Sender\n"; } $Func = 'EventHandler'; $Func('Sender Name'); 由于PHP是一种动态语言,变量可以为任何类型,所以无须先定义函数指针的类型作为事件的签名。直接定义了一个函数EventHandler作为事件处理者,然后将它赋予变量$Func(注意直接使用了字符串形式的函数名),最后触发该事件,并将一个字符串“Sender Name”传给它作为参数。输出的结果是: Calling EventHandler(), arv = Sender Name 同样地,PHP也提供了类似方法指针的机制。如下例: Class EventHandler { public function DoEvent($Sender) { echo "Calling EventHandler.DoEvent(), arg = $Sender\n"; } } $EventHanler = new EventHandler(); $HandlerObject = $EventHanler; $Method = 'DoEvent'; $HandlerObject->$Method('Sender Name'); 由于PHP中没有能够直接引用对象方法的变量,因此需要使用两个变量来间接实现:$HandlerObject指向对象,$Method指向对象方法。通过$HandlerObject->$Method方式的调用,可以动态地指向任何对象方法。 为了使代码更加优雅和更适合复用,可以定义一个专门的类NotifyEvent,并使用一段新的调用代码: final class NotifyEvent { private $HandlerObject; private $Method; public function __construct($HandlerObject, $Method) { $this->HandlerObject = $HandlerObject; $this->Method = $Method; } public function Call($Sender) { $Method = $this->Method; $this->HandlerObject->$Method($Sender); } } $EventHanler = new EventHandler(); $NotifyEvent = new NotifyEvent($EventHanler, 'DoEvent'); $NotifyEvent->Call('Sender Name'); NotifyEvent类定义了两个私有变量$HandlerObject和$Method,分别指向事件处理者对象和处理方法。在构造函数中对这两个变量赋值,再通过Call方法来调用。 熟悉C#的读者可以发现,NotifyEvent类与C#中的Delegate十分类似。Delegate超过NotifyEvent的地方在于支持多播(Multicast),也就是一个事件可以绑定多个事件处理者。只要事件触发者自己维护一个NotifyEvent对象数组,支持多播也不是一件难事。 至此,PHP对事件的支持已经得到了比较圆满的解决。但是,人的求知欲是无穷无尽的。还有没有可能通过其他的方式来实现事件呢? 除了方法指针,接口(interface)也可以用于实现事件。在Java中,这种技术被广泛应用。其核心思想是,将事件处理者的处理函数定义抽象为一个接口(相当于函数指针的签名),事件触发者针对这个接口编程,事件处理者则实现这个接口。这种方式的好处在于,不需要语言支持函数指针或方法指针,让代码显得更加清晰和优雅,缺陷在于,实现同一种功能,要使用更多的代码。如下例: interface IEventHandler { public function DoEvent($Sender, $Arg); } cla ss EventHanlerAdapter implements IEventHandler { public function DoEvent($Sender, $Arg) { echo "Calling EventHanlerAdapter.DoEvent(), Sender = $Sender, arg = $Arg\n"; } } class EventRaiser { private $EventHanlerVar; public function __construct($EventHanlerAdapter) { $this->EventHanlerVar = $EventHanlerAdapter; } public function RaiseEvent() { if ($this->EventHanlerVar != null) { $this->EventHanlerVar->DoEvent($this, 'some string'); } } public function __tostring() { return 'Object EventRaier'; } } $EventHanlerAdapter = new EventHanlerAdapter(); $EventRaiser = new EventRaiser($EventHanlerAdapter); $EventRaiser->RaiseEvent(); 首先定义了一个接口IEventHandler,它包含了方法的签名。EventHanlerAdapter类作为事件处理者,实现了这个接口,并提供了相应的处理方法。EventRaiser类作为事件触发者,针对$EventHanlerVar变量(它应该是IEventHandler接口类型,但是在PHP中不用显式定义)编码。在实际应用中,将EventHanlerAdapter的实例作为参数赋予传给EventRaiser类的构造函数,当事件发生时,相应的处理方法将被调用。输出结果如下: Calling EventHanlerAdapter.DoEvent(), Sender = Object EventRaier, arg = some string 最后,让我们回到现实世界中来。虽然我们用PHP完整地实现了PME模型,但是这到底有什么用呢?毕竟,我们不会用PHP去编写IDE,也不会用它编写应用服务器。回答是,基于PME模型的组件技术可以实现更加方便和更大规模的代码复用。在基于PHP的应用系统中,虽然插件已经被广泛使用,但是通过组件技术可以实现功能更强大、更加规范和更容易维护的插件。此外,组件技术在实现一些大的Framework(例如,针对Web UI的Framework)时,也是不可或缺的。 PHP沉思录之三:Smarty   左轻侯 2007.8.11    在任何Web应用中,如何将程序代码和界面设计,或者说,将逻辑层和表现层分离开来,都会是一个问题。对于PHP这种类型的嵌入网页的脚本语言,这一问题尤其突出。在新手编写的代码中,把访问数据库的代码和操纵HTML元素的代码写在同一个页面里,是很常见的情况。为了避免这一问题,开发者倾向于将涉及业务逻辑的代码封装在某些单独的库文件中,再在负责显示界面的文件中将它们include进来。但是,这仍然无法避免在显示界面的文件中包含大量的PHP代码。究其所以然,是因为除了涉及业务逻辑的代码以外,即使仅仅在显示层,也往往涉及到复杂的显示逻辑。在一个典型的显示页面中,程序需要先包含所有必需的库文件,初始化上下文环境,创建相关业务逻辑对象(假如数据库访问代码已经被业务逻辑对象封装,可以节省数据库相关的代码),最后在HTML的空隙中把对象格式化为HTML元素进行显示。于是我们看到了无数这样的页面,在第一行HTML开始之前,就已经包含了数十行甚至更多的PHP代码,在HTML的内部,仍然充满了各种各样的PHP代码。因此,PHP代码和HTML代码搅和在一块的问题仍然无法解决,对HTML的修改仍然可能导致整个PHP程序崩溃。更加麻烦的是,这种不清晰的结构妨碍了PHP应用在规模上的进一步扩张。    为了解决这一问题,模板(template)技术应运而生。模板技术的基本原理是,通过一个解析器(parser),读取指定的模板文件(包含了某些特定标签的HTML文件),将这些标签替换为相关的PHP变量,再输出为标准的HTML。通过这种方式,不但分离了业务逻辑层和表现层,而且也尽可能地分离了显示逻辑和HTML代码。通过替换不同的模板文件,可以方便地生成各种格式的输出,例如HTML,XML,WML,后期稍加处理甚至可以生成PDF和Flash。早期较为著名的PHP模板引擎有PHPLib中的Template和FastTemplate。    但是,模板技术也有其先天的缺陷。    l   无法彻底分离逻辑。显示逻辑和HTML代码很难通过简单的标签替换,实现彻底的分离。例如,遍历并显示一个数组,在PHP中可以用简单的foreach语句实现,但是使用模板时,就需要进行对整个模板文件进行多次替换操作,造成效率的极大降低;或者根据不同的数据值显示不同的格式,如果模板文件完全不包含PHP代码,那么将很难做到这一点。 l   解析导致的性能损失。由于每次PHP页面被访问时,解析器都必须对模板文件进行替换操作,无疑会降低PHP应用的性能。尤其在多次的替换操作时更是如此。因此,不使用模板比使用模板往往更加快速,这也是许多PHP程序员摒弃模板技术的原因之一。      在经过数年的发展之后,“编译型”的模板技术渐渐占据了主流。所谓“编译型”,是指解析器读取模板文件以后,并不直接生成静态HTML,而是“编译”成一个新的PHP文件,并将它保存起来。以后访问该页面时,模板引擎会直接执行“编译”后的PHP文件。Smarty是这种模板引擎的代表。    针对以上的两个问题,Smarty作了如下处理:    l   独立语法。Smarty实现了一套自己的语法,这套语法不但支持变量替换和简单的判断,而且支持循环,修饰符(modifier),内置了很多功能强大的函数,而且还支持自定义函数。这套系统保证Smarty能够完全独立地处理显示输出,无须再和PHP有什么瓜葛。事实上,在Smarty模板中,是不可以直接使用PHP代码的(通过显式定义可以使用),这也是一种强制分离逻辑层和表现层的方式。(理论上来说,Smarty的模板文件也可以应用于其它语言。)但是,这种解决方式也受到了指责,因为Smarty的语法过于强大,几乎变成了一门新的语言,指责者认为,这反而增加了复杂性。但是,根据作者的实际经验,Smarty的语法不但非常简单直观,而且只需要掌握一些最初级的语法,就足可以应付绝大多数的应用。即使是不懂编程的网页设计师,也很容易就能够掌握。 l   编译机制。Smarty的“编译”机制,节省了用于反复解析模板文件的时间,极大地提高了速度。由于“编译”后生成的是标准的PHP文件,因此从理论上来说,执行的速度不会低于没有模板的PHP应用的速度。在一些和解析型模板引擎进行的对比测试中,Smarty在第一次访问时落后,但是在以后的访问中速度远远超出竞争对手。而且,这种编译过程是智能的,在模板文件的内容被改变后,Smarty会自动重新编译,因此对于开发者来说,编译过程完全无需人工干预。另外,如果你愿意的话,生成的PHP文件还可以方便地应用于Zend Accelerator这样的工具,进行二次编译。      除此之外,Smarty还拥有其他一些优秀的特性:    l   缓存机制。由于实现了编译机制,在接收到对某个模板文件的访问请求时,Smarty会自动将它重定向到编译后的PHP文件。但是,这也意味着,Smarty也可以将它重定向到任何其他的文件——例如静态的HTML文件。在此基础之上,Smarty实现了自己的基于页面的缓存机制。Smarty能够将编译后的PHP文件产生的结果——静态HTML——保存起来,将重复发送的请求直接重定向给它,这意味着对于第一次之后的请求,不需要执行任何PHP代码(Smarty本身的代码当然除外)。对于不需要频繁更新的页面(我们知道这种网页往往在整个网站中占大多数),通过这种缓存机制获取的性能提升是惊人的。而且,由于它是在页面级实现的,因此完全无须涉及到复杂的对象级缓存问题,保持了逻辑上的简单性。 l   可配置性。Smarty在开发之初就将高度的可配置性作为自己的一个设计目标。它本身以100%的PHP编写,以源代码的方式发行,只需要将Smarty简单地拷贝到你的文件路径中,就可以使用了。Smarty的各项配置变量,都可以通过修改config文件或者手动编码进行定制。例如,Smarty默认的定界符是花括号({}),但是这往往和Javascript以及CSS中的花括号冲突。为了解决这一问题,可以简单地将默认定界符修改为其他的字符(例如ASP风格的“<%”和“%>”)。 l   可扩展性。Smart的实现基于面向对象的架构,并且提供了插件机制,非常便于用户修改和扩展其默认的认为。当然,你也可以直接修改它的源代码来达到目的。(对于基于脚本语言的开源应用来说,这是非常惬意的,因为你甚至不需要重新编译。)      让我们来看一个最简单的Smarty应用。这个应用包括两个文件:      TestSmarty.php 调用Smarty类库,初始化变量,并解析相应的模板文件   TestSmarty.tpl 模板文件,其实就是包含了Smarty标签的HTML,放在指定的模板目录下,默认是./templates       TestSmarty.php的内容如下:      assign("HelloStr", "Hello, world");   $Smarty->display("TestSmarty.tpl");   ?>       这个文件的内容非常简单,任何有过PHP经验的程序员都应该能够理解:首先将Smarty类库所在的文件include进来,然后创建一个新的Smarty对象,并对HelloStr变量进行赋值,最后解析TestSmarty.tpl文件。    TestSmarty.php的内容如下:      This is a string from Smarty: {$HelloStr}       解析的结果为:      This is a string from Smarty: Hello, world       此时检查存放编译后的PHP文件的子目录(默认是./templates_c),可以找到一个名叫%%65^650^65099D8B%%TestSmarty.tpl.php的文件,内容如下:         This is a string from Smarty: _tpl_vars['HelloStr']; ?>       这就是Smarty引擎编译生成的结果。    为了启用缓存,可以在TestSmarty.php文件中加入这么一行(当然必须在display方法之前):      $Smarty->caching = 1;       重新访问该页面,然后检查存放缓存文件的子目录(默认是./cache),可以找到一个名叫%%65^650^65099D8B%%TestSmarty.tpl的文件,内容如下:      136   a:4:{s:8:"template";a:1:{s:14:"TestSmarty.tpl";b:1;}s:9:"times*****p";i:1186888266;s:7:"expires";i:1186891866;s:13:"cache_serials";a:0:{}}This is a string from Smarty: Hello, world       这就是生成的缓存文件,在静态的HTML文件之前,包含了已经序列化的PHP信息。虽然这些信息无法被直接阅读,但是多少还是能够猜测出来:模板的子目录,模板文件名,时间戳,生存期(过期时间),等等。如果读者有兴趣研究它们的详细定义,可以阅读Smarty的源代码。    注意,上述信息中包含了一项:生存期,即当前缓存在多长时间以后过期。Smarty默认的生存期是1小时,即3600秒。可以通过修改Smarty属性来设置生命期,代码如下:      $Smarty->cache_lifetime = 1800;       时间单位是秒,设置为1800表示当前缓存半小时后过期。    Smarty还支持为同一个模板创建多个缓存实例,这在实际应用中是非常常见的。举例来说,假设某个博客系统中,显示article的页面为Article.php,对应的模板文件为Article.tpl。但是,article页面的内容根据不同的article ID而不同,因此,必须为同一个页面创建不同的缓存实例。Smarty可以轻松做到这一点:      $Smarty->display("Article.tpl", $ArticleId);       只要将一个唯一标识符(在这个例子中是article的ID)作为第二个参数传给display方法,Smarty就会自动完成一切。       Smarty出现的时间虽然较老牌的PHPLib Template和FastTemplate为晚,但是发展非常迅速,而且已经成为PHP的官方子项目,拥有二级域名 http://smarty.php.net/。正如它的官方站点上所说,与其说Smarty是一个模板引擎,不如说它是一个表现层的Framework。这句话极为重要。    作者个人认为,Smarty诞生和逐渐取得主流地位的意义,不仅仅是提供了一个优秀的模板引擎,而是表示PHP在解决更大规模的应用上迈出了坚实的一步。如果说Java是一开始就将解决大规模应用作为自己的设计目标的话,那么PHP就是在开源社区的推动下,在不断的改进中逐渐接近这一目标。当一个PHP应用采用了Smarty这样优秀和成熟的表现层解决方案,以及其他一些类似的技术(例如好的ORM解决方案)以后,它能够解决的问题的规模就比过去大得多了。通过这些现象,我们可以看到,PHP,或者说LAMP,正在以稳健而持续的步伐,向企业级应用迈进。 本文发表于《程序员》杂志2007年第12期,略有删节。      PHP沉思录之四:Zend Framework   左轻侯   2007.11.11       从理论上来说,PHP是一种通用的动态语言,它可以替代Perl实现通用的脚本,甚至可以创建客户端GUI程序(通过GTK+)。但是,在实际应用中,PHP在绝大多数情况下都被用来开发Web应用。即使在Java和.NET这样有软件业界巨头支持的重量级竞争对手面前,出身草莽的PHP也毫不逊色,尤其是在应用最为广泛的轻量级web开发领域,PHP一直牢牢占据着领先的位置。在这一领域参与竞争的其他语言,例如Python和Perl,虽然各具特色,但是仍然无法撼动PHP的地位。PHP是当之无愧的“Web开发第一语言”,而且似乎没有什么能对它构成挑战。随着web应用在软件界的地位越来越重要,PHP也逐渐从脚本小子手中的玩具,转变成重要的工业语言,并获得了IBM这样的巨人的支持。    但是,就在近两三年,一种新的Web开发解决方案的迅速崛起,震动了整个业界,让PHP开始感到王座不稳。这个解决方案,当然就是Ruby on Rails。    本文无意对PHP和Ruby这两种语言进行全面的比较(虽然这的确是一个非常有意思的话题)。无论读者对Ruby和ROR持什么看法,有一点却是不争的事实:Ruby作为一种新的语言,虽然极具特色,但是并没有得到业界的普遍接受;只有在ROR诞生以后,以其惊人的生产力征服了无数开发者,Ruby这才一飞冲天。也就是说,Ruby至于在很大程度上是依靠ROR这个框架,才能起迅速崛起。    对于感到严重威胁的PHP社区,如果要对Ruby进行有效的反击,这似乎是一条可行的道路:基于PHP语言,实现一个新的web开发框架,能够达到甚至超过ROR那样的生产力。这既是对Ruby的反击,其实也是PHP自身发展的必然结果。因为PHP在其发展过程中,早已经出现了数十个各种各样的框架,只是尚没有一个能具有ROR那样的生产力和影响力。    但是,实现这样一个框架,也面临着不少的问题:    l   PHP语言本身能否完成这一任务?PHP原本只是用于快速解决web应用的简单脚本,虽然经过不断的发展,已经具有了许多高级的语言特征,但是是否能够实现象ROR那样精巧和强大的框架,能够达到ROR那样的生产力? PHP社群能否接受这样一个框架?PHP的使用者,大多数是脚本小子出身,习惯于快速、敏捷、直观的开发方式,对于重量级的框架,未必能够普遍认同。l         无论这些问题的答案如何,重要的是,有人这么做了。Zend Framework就是这一思路的结果。    如上所述,Zend Framework就是这样一个完全基于PHP语言的、向ROR学习的、针对web应用开发的框架。与其它同类的框架相比,Zend Framework有两个特点让它显得与众不同:    Zend Framework由Zend公司开发,因此它是一个“官方的”框架。众所周知,Zend公司是PHP编译器的维护者,也是Zend Optimizer、Zend Studio等一系列PHP相关产品的拥有者。由于这一关系,Zend Framework虽然没有被内置到PHP发行包中,但也算得上PHP的官方解决方案了。(顺便说句题外话,Zend公司最近发布了Zend Studio for Eclipse的beta版本,有兴趣的读者不妨尝试一下。) Zend Framework跟ROR实在太象了。和ROR一样,Zend Framework使用了同样的Front Controller模式,同样的Model/Controller/View模型,同样的ORM思路,甚至连命名规则都十分相似。当然了,Zend Framework跟ROR的不同之处还是很多的,至于底层的实现细节,肯定区别更大。但是,不管是有意还是无意(好吧,我承认“无意”绝对不可能做成这样),Zend Framework的实现思路和ROR是非常相似的。      这是一个典型的Zend Framework应用的目录:      /root   /application   /controllers   /models   /views   /filters   /helpers   /scripts   /library   /public   /images   /scripts   /styles       其中,library目录下存放的是Zend Framework所有的库文件,如果还有其他第三方的库文件,也可以存放在这里。Public目录是唯一一个可以通过web方式直接访问的目录,但它并不存放php文件,只存放CSS、JavaScript脚本、图片等静态文件。Application目录中存放着实现应用逻辑的所有PHP文件,它又拥有三个子目录:controllers、models、views。顾名思义,它们分别用于存放控制器/模型/视图的相应PHP文件。    Zend Framework实现了Front Controller模式,这意味着,所有的http请求都会被转发到同一个入口,然后再路由到相应的控制器。这个入口就是root根目录下的index.php文件。Zend Framework通过URL Rewrite技术来实现这一点。在root根目录下,存在一个.htaccess文件,内容如下:      RewriteEngine on   RewriteRule .* index.php   php_flag magic_quotes_gpc off   php_flag register_globals off       熟悉apache的URL Rewrite技术的读者,很容易理解它的意义:将所有的请求转发到index.php。当然,对于图片之类的静态文件的请求不需要被转发,这可以通过在public目录下放置另一个.htaccess来覆盖上级目录的Rewrite规则。    (习惯在IIS下运行PHP的读者可能会问,Zend Framework是否可以在IIS下运行呢?由于IIS不支持基于. htaccess的URL Rewrite,因此Zend Framework无法简单地在IIS下运行。但是,由于IIS支持基于HttpModule的URL Rewrite,因此理论上经过某些修改,是有可能让Zend Framework在IIS下运行的。有兴趣的读者可以自行尝试。)    Index.php的功能是将http请求转发到相关的控制器,另外,它还需要完成一些初始化时期的配置工作。它的内容如下:      throwExceptions(true);   $frontController->setControllerDirectory('./application/controllers');   // run!   $frontController->dispatch();   ?>       开始的几行,分别用于配置报错等级、设置时区、设置包含文件路径,以及载入相关文件。Zend_Loader::loadClass是一个静态方法,用于装载指定的类。在载入Zend_Controller_Front以后,程序建立了这个类的实例,通知它抛出所有的异常,并且设置了控制器所在的目录。最后,调用dispatch()方法,将http请求转发到相应的控制器。   在application/controllers目录下,存放着所有的控制器类。那么路由器是根据什么规则寻找相应的控制器呢?很简单,它按照一定的命名规范来进行匹配。下面是一个控制器文件的内容:      root/application/controllers/IndexController.php      in IndexController::indexAction()

";   }   function addAction() {   echo "

in IndexController::addAction()

";   }   function editAction() {   echo "

in IndexController::editAction()

";   }   function deleteAction() {   echo "

in IndexController::deleteAction()

";   }   }       所有的控制器类都派生自Zend_Controller_Action。控制器名字必须大写字母开头,其他字母必须都为小写,最后加上Controller结尾,并且存放在同名的php文件中。本例中的控制器是IndexController。(需要指出的是,“其他字母必须都为小写”是一条很诡异的规则,这意味着CurrentUserController这样的名字是不合法的,无法被正确地路由。如果你这样做了,会是个很难发现的bug。)    控制器拥有一个或多个行为(action),体现在代码上,行为是控制器类拥有的public方法,以小写字母开头,以Action结尾。在本例中,indexAction、deleteAction、addAction和editAction是IndexController的四个行为。    在浏览器中,可以通过“控制器/行为”的URL,来访问相应的行为。如上例,URL和行为的对应关系是:      URL Action   http://localhost/root/    IndexController::indexAction()   http://localhost/root/index    IndexController::indexAction()   http://localhost/root/index/add IndexController::addAction()   http://localhost/root/index/edit IndexController::editAction()   http://localhost/root/index/delete IndexController::deleteAction()       可以看到,indexAction是一个控制器的缺省行为:如果没有指定行为,路由器将把请求转发到indexAction方法。同样,IndexController是缺省的控制器:如果没有指定控制器,路由器将把请求转发到IndexController控制器。    在上例中,行为方法直接输出一行代码作为响应。在规范的应用中,与显示相关的代码应该存放在视图中。一个规范的控制器行为方法内容如下:      function indexAction() {   $view = new Zend_View();   $view->setScriptPath('./application/views/scripts’);   $view->title = "Hello world";   echo $view->render();   }       首先,创建了一个视图类(Zend_View)的实例,然后设置其路径,并对它的title属性赋值,最后调用render()方法,渲染相应的模板文件,将结果返回给浏览器。    )在本例中,index.phtml的内容如下:J同样的,视图根据特定的命名规范来匹配相应的模板文件。对于IndexController::indexAction()方法,视图会查找如下模板文件:root/application/views/scripts/index/index.phtml。后缀名为.phtml的模板文件事实上就是一个标准的PHP文件。(熟悉.rhtml后缀名的读者有何感觉?            <?php echo $this->escape($this->title); ?>         

escape($this->title); ?>

            最后,我们终于又一次看到了亲切的“Hello world”。   Zend Framework使用Zend_Db_Table来实现类似于ActiveRecord的ORM功能。和任何ORM一样,首先需要为它配置数据库的相关信息(服务器,用户名,密码,数据库名,等等)。虽然这并不复杂,但本文不拟讨论这些细节问题。(Zend Framework在它的Roadmap中计划支持YAML!)一旦配置完成后,可以在/application/models子目录下创建和数据库表相对应的模型类。   举例来说,在数据库中存在表user,结构如下:      CREATE TABLE user (   id int(11) NOT NULL auto_increment,   name varchar(100) NOT NULL,   password varchar(100) NOT NULL,   PRIMARY KEY (id)   )       相应地,可以在models目录下创建一个模型:      root/application/models/User.php      user = $user->fetchAll();   echo $view->render();   }       User类的fetchAll()返回一个数组,包含该表中所有的内容。这个数组被存放在$view的user属性。接下来,看看新的index.phtml:      …                  user as $user) : ?>                  
IdName
escape($user->id);?>escape($user->name);?>
  …       这段代码遍历了$this->user,并将它的属性依次打印出来。    除了基本的MVC模型以外,Zend Framework还提供了一系列高级功能,下面是一个不全面的列表:    Zend_Acl(access control l   list):实现了非常灵活的权限控制机制,通过对特定的角色和资源进行规则定义,可以方便地实现各种复杂的权限控制规则,并且权限可以按照角色的树型结构实现继承。 Zend_Cache:提供了一种通用的数据缓存方式,可以将任何数据缓存到文件系统、数据库、内存,能够与Memcached和SQLite这样的第三方软件很好地集成。 Zend_Log:提供了一种通用的log解决方案,支持多个log后端,支持log信息格式化,支持信息过滤。l   l   Zend_Pdf:完全用PHP语言实现的PDF文件操作引擎。 Zend_Feed:封装了对RSS和ATOM的操作。 l   l   Zend_Gdata:封装了对Google Data API的操作。 l   Zend_Json:JSON是AJAX中必不可少的数据格式。Zend_Json封装了数据在PHP和JSON格式之间的转换操作。 l   Zend_Rest:REST是越来越流行的Web Service协议,有逐渐凌驾传统的SOAP协议之势。Zend_Rest封装了对REST的操作,可以方便地实现REST的客户端或服务端应用。 l   Zend_Search_Lucene:封装了对著名的全文检索引擎Lucene的操作。      Zend Framework在7月份发布1.0正式版,现在最新的版本为1.0.2。可以说,Zend Framework还是一个非常新的事物。但是在它漫长的测试期间,已经有不少人在尝试,甚至用于正式的项目,因为它的种种功能实在太让人馋涎欲滴了。   回到本文开头的两个问题,Zend Framework的前途将会如何呢?虽然作者深知揣测未来的风险,但是还是很愿意尝试回答这两个问题:第一,Zend Framework的出现,已经证明了PHP 5有能力实现大型的、复杂的框架,而由于PHP的编译器掌握在Zend公司手中,能够保证PHP语言会持续发展,以满足进一步的需求。第二,PHP本来就已经在逐步进入企业开发的领域,出现一个重量级的Framework是必然的事情。也许PHP未来的开发主力,将由经过短期培训的、偏向快速解决问题的初级程序员,变为经验丰富的、熟悉各种复杂的IT平台、侧重应用的安全性和健壮性的资深工程师。毕竟,只要经过不懈的努力,脚本小子也可能有成为大师的一天,对吗?   至于Zend Framework和ROR之间的竞争,现在没有人能够给出答案。也许到目前为止,Zend Framework还无法达到ROR 100%的生产力,但是它拥有更广大的用户基础,更成熟的语言,更短的学习曲线,类似C和Java的语法也更让人容易掌握。ROR中的DRY、“惯例重于配置”等原则,在Zend Framework中也得了很好的秉承和发扬。无论它是否能够有效地狙击ROR,毫无疑问的是,一旦Zend Framework被PHP社群广泛接受,PHP的生产力将会有极大的提高。 本文发表在《程序员》杂志第10期 PHP沉思录之五:Session有效期问题 左轻侯 2008.9.07    Session处理是所有的Web应用都必须面对的问题。PHP中对session有效期的处理,和其他的解决方案有着很大的不同,这是和PHP的工作机制相关的。 在传统的client/server应用中,对于session失效的情况,可以交给网络协议自己来处理。无论是client端主动关闭连接,还是因为网络异常而导致的连接中断,server端都能够得到通知,触发连接中断的事件。只要编程响应这一事件,执行指定的操作即可。但对于web应用来说,情况却完全不一样。HTTP协议本身是无状态的,也就是说,每当client/server完成一次请求/响应的过程后,连接就会被断开。在断开连接以后,server并不知道client是否继续“在线”,还会继续发送下一次请求。换句话说,无论client端的用户已经关闭了浏览器窗口,还是用户仅仅在阅读当前网页并准备在下一秒钟继续浏览,或者用户因为Windows崩溃/停电/硬盘坏掉/网线被拔/地球爆炸而彻底无法再发送下一个请求,server都一无所知。(在HTTP 1.1中,浏览器可以通过keep-alive参数,来通知server不要在响应请求后主动断开连接,从而实现物理上的长连接。但是,这只是为了提高网络传输的性能而采取的措施,HTTP在逻辑上仍然是无状态的。)因此,只能通过某种模拟的方式来判断当前session是否有效。如果某个session在超过一段时间后没有对server端发出请求,server都会判断用户已经“离线”,当前session失效,并触发连接中断的事件。要做到这一点,server需要运行一个后台线程,定时扫描所有的session信息,判断session是否已经超时。 PHP处理session的原理也不例外,但是在具体的实现方式上,却与众不同。这是因为,由于PHP的工作机制,它并没有一个后台线程,来定时地扫描session信息并判断其是否失效。它的解决之道是,当一个有效请求发生时,PHP会根据某个概率,来决定是否调用一个GC(Garbage Collector)。GC的工作,就是扫描所有的session信息,用当前时间减去session的最后修改时间(modified date),同配置参数(configuration option)session.gc_maxlifetime的值进行比较,如果生存时间已经超过gc_maxlifetime,就把该session删除。这是很容易理解的,因为如果每次请求都要调用GC代码,那么PHP的效率就会低得令人吃不消了。这个概率取决于配置参数session.gc_probability/session.gc_divisor的值(可以通过php.ini或者ini_set()函数来修改)。默认情况下,session.gc_probability = 1,session.gc_divisor=100,也就是说有1%的可能性会启动GC。 这三个参数,session.gc_maxlifetime/session.gc_probability/session.gc_divisor都可以通过php.ini或者ini_set()函数来修改。但要记得,如果使用ini_set()函数的话,必须在每一个页面的开始处都调用ini_set()。   这又导致了另外一个问题,gc_maxlifetime只能保证session生存的最短时间,并不能够保存在超过这一时间之后session信息立即会得到删除。因为GC是按概率启动的,可能在某一个长时间内都没有被启动,那么大量的session在超过gc_maxlifetime以后仍然会有效。当然,发生这种情况的概率很小,但是如果你的应用对session的失效期要求很精确的话,这会导致很严重的问题。解决这个问题的一个方法是,把session.gc_probability/session.gc_divisor的机率提高,如果提到100%,就会彻底解决这个问题,但显然会对性能造成严重的影响。另一个方法是放弃PHP的GC,自己在代码中判断当前session的生存时间,如果超出了 gc_maxlifetime,就清空当前session。 PHP中的session有效期默认是1440秒(24分钟),也就是说,客户端超过24分钟没有刷新,当前session就会失效。要修改这个默认值,正确的解决办法是修改配置参数session.gc_maxlifetime。 我曾经在网上搜索过这个问题的解决方式,找到的结果千奇百怪。有的说要设置“session_life_time”,据我知所,PHP中没有这个参数。有的说要调用session_set_cookie_params,或者设置session.cookie_lifetime,这仅仅用于设置client端cookie的生存时间,换言之,只当client端cookie的生存时间小于server端的session生存期时,修改这个值才有效,并且最长不能超过server端的session生存期,原因很简单,当server端的session已经失效时,client端cookie的生存时间再长也是没有意义的。还有的说要调用 session_cache_expire,这个参数用于通知浏览器和proxy,当前页面的内容应该被缓存多长时间,和session的生存期并没有直接关系。 听起来,这种解决方案很完美。但是,当你在实际中尝试修改session.gc_maxlifetime的值的时候,你很可能会发现,这个参数基本不起作用,session有效期仍然保持24分钟的默认值。甚至可能出现,在开发环境下工作正常,在服务器上却无效! 为了彻底解决这个问题,需要对PHP的工作细节进行进一步的分析。   在默认情况下,PHP 中的session信息会以文本文件的形式,被保存在系统的临时文件目录中。这个路径由配置参数session.save_path指定。在Linux下,这一路径通常为\\tmp,在 Windows下通常为C:\\Windows\\Temp。当服务器上有多个PHP应用时,它们会把自己的session文件都保存在同一个目录中(因为它们使用同一个session.save_path参数)。同样地,这些PHP应用也会按一定机率启动GC,扫描所有的session文件。   问题在于,GC在工作时,并不会区分不同站点的session。举例言之,站点A的gc_maxlifetime设置为2小时,站点B的 gc_maxlifetime设置为默认的24分钟。当站点B的GC启动时,它会扫描公用的临时文件目录,把所有超过24分钟的session文件全部删除掉,而不管它们来自于站点A或B。这样,站点A的gc_maxlifetime设置就形同虚设了。 找到问题所在,解决起来就很简单了。在页面的开始处调用session_save_path()函数,它能够修改session.save_path参数,把保存session的目录指向一个专用的目录,例如\\tmp\\myapp\\。这样,gc_maxlifetime参数就工作正常了。 使用公用的session.save_path还会导致安全性问题,因为这意味着,同一台服务器上的其它PHP程序也可以读取你的站点的session文件,这可能被用于黑客攻击。另一个问题是效率:在一个繁忙的站点中,可能存在成千上万个session文件,而把许多不同网站的session文件都放在同一个目录下,无论是对单个文件的读写,还是遍历所有文件进行GC,都无疑会导致性能的降低。因此,如果你的PHP应用和别的PHP应用运行在同一台服务器上的话,强烈建议你使用自己的session.save_path。 严格地来说,这算是PHP的一个bug。当PHP在进行GC时,它应该区别来自不同站点的session文件,并应用不同的gc_maxlifetime值。目前,最新的PHP 5.2.X仍然存在这个问题。 上文说到,在一个繁忙的站点中,可能存在成千上万个session文件,即使区分了不同站点的session.save_path目录,单个站点的session文件数目仍然可能导致效率问题。为了解决这一问题,可行的几种方法有: l 如果PHP运行在Linux系统下,使用ReiserFS文件系统取代默认的ext2/ext3文件系统。ReiserFS对于大量小文件的存取性能,比ext2/ext3有极大的提高。 将session.save_path指向一个内存路径。这意味着,session文件的读写只在内存中进行,而不执行磁盘操作。l l session.save_path接受一个额外的N参数,用于指定目录的级数。例如,“5;/tmp” 将导致创建类似这样的session文件:/tmp/4/b/1/e/3/sess_4b1e384ad74619bd212e236e52a5a174If。具体的说明,请参见:http://cn.php.net/manual/en/session.configuration.php#ini.session.save-path l 终极的解决方案,是放弃PHP的session处理机制,自己编码接管所有的session处理操作,通过session_set_save_handler()函数来实现。通过自己接管session处理,可以将所有的session保存在专门的数据库(往往使用内存表)中,从而彻底解决session文件带来的问题,并且可以方便地实现session的共享和复制。这也是大型的PHP应用一般会使用的方式。关于session_set_save_handler()函数的使用,网上和相关图书都有详细的说明,这里不再赘述。值得一提的是,即使在这种方式下,启动GC的概率仍然取决于session.gc_probability/session.gc_divisor。 本文发表在《程序员》杂志2008年第11期 PHP沉思录之六:Drupal的性能问题 左轻侯 Drupal是一个基于PHP的开源CMS系统,也是我认为技术上实现得最好的一个PHP应用。Drupal的架构非常优秀,通过微内核+plugin的方式,实现了极佳的扩展性,从而使Drupal远远超出一般的CMS这一范畴。从这个意义上来说,把Drupal称为Web OS似乎更加合适一些。关于Drupal,有太多的话可以说,也许我会在以后的时间里写一篇文章对它进行专门的讨论。但是在本文中,我想讨论的,是Drupal社区中的每一个人都会面对,但不是每一个人都对其有清晰认识的问题,即Drupal的性能问题。 因为客户需求,我曾经对Drupal做过比较全面的测试。当时的环境是双服务器(DB server+Web Server),硬件配置都是单CPU+4G。数据库里面有几千条Node记录。用JMeter对各种情况下(开/关各种cache模块,logged user/anonymous user)不同页面的读取和写入操作都进行过测试。 测试的结果可能和很多人印象中不一样。两个主要的结果如下: 1. Logged user和anonymous user的性能差距非常大。同一个页面,logged user的RPS(Requests per second)一般不超过20,而启用了cache的anonymous user的RPS在100多,当使用了file-based cache以后,甚至能超过300。 2. 数据库压力相对较小。由于Drupal把大量可配置的内容都放在数据库中,因此往往容易产生这样一种印象,即Drupal对数据库要求应该是很高的。但事实上,无论是cache还是非cache模式,DB server的压力都相当小(CPU在10%以下),而Web Server的CPU在80%以上。跟踪所有的db query的执行时间后,也证明了这一点(全部db query的执行时间只占页面生成时间的一小部分)。 经过反复的测试和思考,我得出了一些结论。很明显,Drupal在大量logged user并发情况下的瓶颈,在于执行Drupal代码的CPU时间,而不是在于数据库或者其他地方。之所以出现这样的情况,和PHP本身的执行机制和Drupal的实现方式有关。Drupal在生成一个非cached的页面时,不管这个页面多么简单,都要执行一个完整的bootstrap过程,即使只启用了最少的模块,这个过程也要调用几十个PHP文件,执行成千上万行PHP代码。而PHP的机制又决定了没有任何PHP代码或者对象能够驻留内存,每次响应请求都必须执行完整的初始化工作。而anonymous user之所以快,是因为Drupal在执行cached page的时候,不会执行完整的bootstrap过程,它先检查是否cached page,是的话就读取缓存,然后结束工作。这样当然就快了。 以这个结论为前提,可以解释一些事情: 1. 为什么Drupal的性能在各种环境下相差并不多。无论是双服务器,单服务器,甚至内存非常小的虚拟机,logged user的RPS值往往总是在10~20之间。数据库里面有几百条,或者几十万条记录,影响也不大。因为瓶颈并不在于DB或者内存,而是在于执行代码的过程。 2. 为什么使用APC/XCache这样的代码优化程序,能够得到极大的性能提升。在我自己的虚拟机环境上,RPS从3~4提升到了12。因为它提升的是PHP代码的执行时间。 从这个结论出发,列出一些对优化Drupal的logged user性能有明显作用和没有明显作用的措施: 没有明显作用的: 1. 加内存。在并发数只有10+的时候,即使每个请求占20M内存,也只有200M+内存而已。 2. DB server和Web server分开,或者增强DB server的配置。一台中等性能的mysql服务器,应付200~300的并发是很轻松的事情,在并发数只有10+的时候,db server实际上是很空闲的。 3. 基础软件的优化,例如从Windows转移到Linux,从apache转移到Lighttpd,从MySQL迁移到其他数据库,除了从Windows转移到 Linux会有比较明显的提升以外(因为PHP在Linux上的效率比在Windows上要好),其它的措施可能会快一些,但不会有大幅度的提高,因为瓶颈不在那里。 有明显作用的: 1. 使用APC/XCache这样的代码优化程序,速度会有几倍的提升。估计大家都已经这样做过了。 2. 增加web server的CPU数量。双核的肯定比单核的快,4个CPU肯定比2个CPU快得多。 3. 使用多web server+单db server的配置,把代码执行的压力分散到不同的web server上。上文说到,单台db server可以轻松应付200+的并发,这意味着理论上可以支持10台以上的web server。 4. 使用Quercus这样的引擎,把PHP代码编译成Java,再在Java VM中运行,理论上会有很大的提高。原因是,第一,Java的运行效率比PHP高,第二,Java代码是可以cache的,不需要每次都重新加载。这里有个测试结果: http://www.workhabit.org/resin-backed-php-drives-4x-performance-improvements-drupal 。Drupal在Quercus下有4倍的性能提高,但是这个数字跟Drupal在打开APC/eAccelerator下的提升差不多,所以可能没有太大的实用价值。 另外一种思路是代码本身的优化。 使用cache API基本上是没有意义的,因为对于logger user,Drupal不会调用cache API。Drupal.org上有人提出,即使是logged user,有很多页面也是不用定制化的,这意味着可以cache它们。但是Drupal没有提供这样一种机制。只要是logged user,Drupal就会执行完整的bootstrap过程,即使只打印出一个hello world,因此实际上你没有办法在logged user状态下cache单个页面。 到目前最新版本的Drupal(Drupal 6.4)为止,对于logger user,Drupal只提供了一种cache功能,就是可以将部分block设置为可cache的。在block占用大量服务器时间的情况下,block cache能够有效地提高效率。但是,由于block cache对于bootstrap过程并无影响,因此当瓶颈在于bootstrap本身时,Block cache是无能为力的。 在Drupal.org的社区,关于logger user的cache问题,一直处于热烈的讨论之中。基本的结论是,由于Drupal的架构就是这样,目前没有很好的解决方案,只能期待Drupal在以后的版本中进行改进了。 我研究了一下Drupal的bootstrap过程,发现也许这样是可行的:实现hook_boot函数,这是bootstrap中执行最靠前的一个函数,它被调用时,bootstrap的大部分过程还没有执行。在hook_boot中,检查当前页面是否需要cache,如果是,直接读 cache生成页面,然后调用exit()强行结束。这在理论上是可行的,但太过hack了一点。 Drupal的情况是这样,那么其他的PHP框架,尤其是半官方的Zend Framework,性能如何呢?通过搜索,我在网上找到了一份PHP framework comparison benchmarks,网址见: http://www.avnetlabs.com/php/php-framework-comparison-benchmarks。根据这份报告的数据,Zend Framework的性能只有原生PHP的10%,如果没有用APC,连3%都不到。当然,这份报告的数据不一定详尽,Zend Framework在不同环境下的表现应该也会有出入。但是,Zend Framework的性能大幅度落后于Baseline PHP,应该是确凿无疑的。 为什么PHP主流框架的性能都存在着这样的问题呢?其实这也不难理解。回顾PHP沉思录系列第一篇中对于PHP工作模型的讨论,由于PHP没有驻留内存的进程,所以每一个request发生时,都必须初始化所有的对象,这导致大量的时间被耗费在进程代码的执行过程中。当PHP程序仅仅是简单的脚本时,这无关紧要,但是在结构复杂的架构中,由于每次处理request都要重复调用成千上万行代码,这一问题就变得非常突出了。而且,除非PHP以后的版本对这一机制进行改进,否则这个问题无法得到彻底的解决。 那么,这是否意味着,PHP只能适用于小型的网站,而无法在高并发量的大型网站施展拳脚呢?当然不是这样。事实上,在Yahoo和其他许多知名的巨型网站上,都大量地使用了PHP。原因在于,PHP仅仅被用作一个内容生成器,生成的内容会被转化为静态文本,绝大多数用户浏览的都是被cache的静态文本。这就和PHP程序的性能毫无关系了。但是,当用户并不是仅仅进行浏览,而是需要频繁地和网站进行互动时,PHP的性能不但无法比拟C和Java,甚至无法与同为脚本语言的Python和Ruby相比。也就是说,PHP更适合于新闻门户这样的内容发布站点,而不是web 2.0应用的首选。 在本系列文章告一段落的时候,我们看到的是PHP的局限性。热爱PHP的人们可能会对此觉得沮丧。但是,这并无损于PHP作为一门优秀语言的声誉。尺有所短,寸有所长,对于我们熟悉和喜爱的工具,我们更应该了解它们的局限,这也有利于我们更有效地使用它们。

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档