您当前的位置:首页 > 分类 > 技术资讯 > PHP > 正文

PHP Session原生及兼容双向技术方案阐述和实现

发布时间:2014-06-20 21:37:48      来源:51推一把
【摘要】本文首先表明PHP Session在植入式开发中所遇到的问题,接着依据PHP Session的运行特性,提出PHP Session原生及兼容双向技术方案,并在最后提供单独可用的整套代码,以及对应的解析。正文一、 PHP Session在植入式开发中所遇到的问题植入式开发,是指

    本文首先表明PHP Session在植入式开发中所遇到的问题,接着依据PHP Session的运行特性,提出PHP Session原生及兼容双向技术方案,并在最后提供单独可用的整套代码,以及对应的解析。

正文
一、 PHP Session在植入式开发中所遇到的问题
植入式开发,是指在原有系统本身上,利用原有系统的资源植入相关的代码,以达到增加或者改变系统功能的目的。目前较为常见的植入式开发模式为插件开发。
一般而言,植入式开发会受到三重因素的影响:A)服务器自身(比如PHP+MySQL);B)被植入系统本身;C)被植入系统中所正在运行的其它植入式代码(比如其它插件)。因此,植入式开发的不稳定性是相当的大、其变数也相当的多。PHP Session作为典型的服务器依赖方案,若应用于植入式开发,会经常遇到问题。以笔者所负责的Xweibo插件版项目来说,就出现过如下问题:
(1) Session互相干扰导致插件冲突
从2010年10月份开始,陆续有站长反映XWeibo插件版For Discuz!X 1.5 Ver 1.0.1(下称“XWB插件”)与第三方团队Discuz! Student Union(下称“DSU”)开发的插件“每日签到v2.2”(下称“签到插件”)存在冲突问题。经查证,为签到插件本身误用Session机制,出现了两者Session互相干扰,最终导致冲突产生[1]。
(2) session拖慢服务器问题
有站长反映,XWB插件安装后,网站运行速度缓慢,甚至有时网站报超时:
Fatal error: Maximum execution time of 30 seconds exceeded in plugin.env.php on line 32。
而plugin.env.php 的31行到34行的代码为:
session_start();
if(!isset($_SESSION[XWB_CLIENT_SESSION])){
 $_SESSION[XWB_CLIENT_SESSION]= array();
}
经询问,这些网站的php.ini默认为file session写入临时文件夹模式。很明显,要不是临时文件夹有问题(比如有服务器竟然拥有130万个session文件)、要不是权限问题(无法将过时Session文件垃圾回收,比如下面的E_NOTICE提示),导致file session读取、写入或者垃圾回收出现问题,最终引起插件超时退出,从而严重影响服务器的性能。
Notice: session_start() [function.session-start]: ps_files_cleanup_dir: opendir(H:WINDOWSTEMP) failed: No such file or directory (2) in xxx on line 127
为了解决此问题,故需要提供一个方案,能够既能保持现有的session应用方式(即跟随php.ini设置)、也能提供一种兼容的解决方案,以彻底解决这类session问题。

二、 PHP Session的运行特性
1、 Session操作和存储相分离
    php的session机制中,操作和存储是分离和委托调用(即A本身不直接实行逻辑代码,而是委托另外的B来完成[2])关系。具体如下:
1) 当调用函数session_start时,php委托session存储器进行读取操作,然后将得到的数据反序列化还原到$_SESSION数组中。session存储器默认根据php.ini中的session.save_handler设置来决定,但如果在session_start前调用了函数session_set_save_handler,则依赖于其注册的函数或者类相关方法。
2) 页面周期中,操作$_SESSION数组,存储器不会起任何作用。
3) 页面周期结束时(或者显著调用session终止函数,比如session_destroy、session_write_close等),php将$_SESSION数组序列化,然后委托session存储器进行写入。session存储器的决定请看第1)条。
2、 Session Cookie
php.ini的默认设置中, session依赖于一个Session Cookie值(php.ini的session.name默认为PHPSESSID)。故如果session中有关cookies部分设置错误,session机制则无法正常运行[3]。
 

我们经常听到一句话“浏览器关闭了,session就会失效了”,其实并不正确。有关错误的原因,请参考文章《cookie和session机制》[4]。
 
三、 方案原理
1、 拆分
从上面可以看出,要模拟Session机制,最重要的是能够实现Session操作和存储相分离,以及委托调用两大重要关系。将PHP操作Session和PHP存储Session进行原生和兼容拆分,可以出现4个方案:
a) Session原生操作方案
使用原生php session机制(即php有关session的任何函数),以及直接操作$_SESSION数组。
b) Session兼容操作方案
又称Session模拟操作方案。此方案下和php自身的session机制完全无关:首先Session的启动机制为自己编写,另外所有session存储在一个模拟的session数组(和$_SESSION数组无关)。
c) Session原生存储方案
使用php.ini中有关Session的存储设置,进行session数据的存储。
d) Session兼容存储方案
又称Session指定存储方案。不使用php.ini中有关Session的存储设置,改用自有的session数据存储设置和方法。
2、 组合
然后将上述4套方案进行组合,即形成一套完整的PHP Session原生及兼容双向技术方案:

a) 组合一:Session原生操作方案 +  Session原生存储方案
此组合下,完全使用原生php session机制,即session的操作和存储完全由php.ini决定。
此组合适用于可以自行设置php.ini的网站(或者使用默认php.ini设置也不出现问题的网站)。
b) 组合二:Session原生操作方案 +  Session兼容存储方案
此组合下,其操作模式和原来的php session一致;但在存储上,通过使用session_set_save_handler函数,设置其使用指定的存储方案。
此组合适用于不可以自行设置php.ini的网站。
c) 组合三:Session兼容操作方案 +  Session原生存储方案
目前尚无有效的方法覆盖读取php.ini的有关session存储设置,故此组合无效。
d) 组合四:Session兼容操作方案 +  Session兼容存储方案
此组合下,实际上模拟了一个独立的session机制,从而完全和其他原生session产生问题的因素隔离开,实现最大程度的兼容。
基于组合三的无效原因,故在此规定,使用Session兼容操作方案必须提供Session兼容存储方案。


四、 方案具体代码
为方便各位的开发,笔者已经将XWB插件版中的该套方案独立出来,形成一个单独的Inter_Session组件(仅用于PHP5及以上)。
下载地址(文件名为"Inter_Session_rxxx"):http://code.google.com/p/horseluke-code/downloads/list
该组件简单阐述如下:
1、 整体UML
该方案所有涉及的类都必须使用委托模式和单例模式;同时也可能使用了工厂模式。整体UML如下:


 
2、 Inter_Factory_Session

本类提供一个静态方法,通过工厂模式快速启动本session方案。用法:
$sess = Inter_Factory_Session::produce();
当调用Inter_Factory_Session::produce()时,即单例返回一个session操作器。初始化过程如下:
a) 根据配置,同时初始化一个session操作器和一个session存储器,并将session操作器存储到属性Inter_Factory_Session::_operator;
b) 按需要调用session操作器的方法setStorageHandler,将session存储器实例交与session操作器实例进行存储方案设置;
c) 假如设置需要,则调用session操作器的session_start,启动session机制。
详细请看相关代码。


3、 session操作方案类集合
 

    session操作方案有且仅有两个方案:Session原生操作方案(Inter_Session_Operator_Native)、Session兼容操作方案(又称“Session模拟操作方案”;Inter_Session_Operator_Simulator)。
除session_set_save_handler外,所有session操作方案类均必须实现Inter_Session_Operator_Abstract类里面的方法;其中,方法setStorageHandler用于替代session_set_save_handler,完成设置该操作方案所搭配的session存储方案,以便在session启动和结束后,完成对session的读取和写入。除非必要,一般最终用户无需调用该方法。
有关Session兼容操作方案中的Session id生成方案,经过数个程序的考察,最终在实现思路上与带有校验的ECMall靠近(相关方法名:_generateSessionid,_generateSessionHash,_checkSessionHash)。此Session id生成方案与浏览器的User Agent有关,因此若采取双核浏览器浏览,则在切换内核时,会导致session id重新计算,从而引发session丢失。
目前两个操作方案均已实现的session函数有:
class Inter_Session_Operator_Simulator extends Inter_Session_Operator_Abstract{
    public function session_start(){}
    public function session_id( $id = null ){}
    public function session_name($name = null){}
    public function session_regenerate_id( $delete_old_session = false ){}
    public function session_destroy(){}
    public function session_unset(){}
    public function session_encode(){}
    public function session_decode($data){}
    public function session_cache_limiter($cache_limiter = null){}
    public function session_cache_expire($new_cache_expire = null){}
    public function session_get_cookie_params(){    }
    public function session_set_cookie_params($lifetime, $path = null, $domain = null, $secure = null, $httponly = null){}
    public function session_save_path($path = null){}
    public function session_write_close(){}
}

使用方法示例:
$sess = Inter_Factory_Session::produce();
//如果你将Inter_Factory_Session的配置“AUTO_START”设置为false,就要自行session_start()
if(!Inter_Factory_Session::isAutoStart()){
    $sess->session_start();
}
$sess->session_regenerate_id();
//$sess->session_destroy();
//……
 
4、 session存储方案类集合
 

所有session存储方案,均必须实现session_storage_abstract类的方法,而这些方法的名称和参数,又必须强制和session_set_save_handler函数所提供的例子保持一致[5]。
本方案已经提供PDO_Sqlite和Memcache两个可以直接使用的Session存储方案;另外,提供两个Demo方案,以供开发者快速编写Session存储方案。其中Demo为编写像本方案中PDO_Sqlite之类的独立类别存储方案,Demo2为编写像XWB插件版中session_storage_db之类的和已有框架进行集合类别的存储方案。
要注意的是,在本方案中,由于Session存储方案还需要兼容Session兼容操作方案(该操作方案是在类被destruct之际,调用Session存储器实例进行session数据的写入、以及进行Session存储器的关闭),故在编写close方法时,还应该调用一次gc。另外也有php手册评论提到,使用session_set_save_handler后,Debian和Ubuntu的发行版不会自动调用gc(垃圾回收)机制,需要自己手工触发。所以部分程序,如phpcms、cmstop则在这里永远调用gc机制,虽然保证了gc但也导致资源浪费(以下为cmstop/framework/session/storage/db.php):
Class session_storage_db extends session_storage
{
    ……
 function close()
 {
  return $this->gc(ini_get("session.gc_maxlifetime"));
 }
    ……
 function gc($maxlifetime)
 {
  $expiretime = TIME - $maxlifetime;
  $sdb = $this->db->prepare("DELETE FROM $this->table WHERE `lastvisit`  return $sdb->execute(array($expiretime));
 }
 
}
 
那么如何减少这方面的消耗呢?一方面可以在close中调用gc的时候进行概率演算;另外一方面则使用一个属性标记是否运行过gc,防止重复运算。代码优化如下:
/**
 * Inter组件之Session存储器:Pdosqlite
 * 使用PDO连接SQLite 3
*/
class Inter_Session_Storage_Pdosqlite extends Inter_Session_Storage_Abstract{
  
    /**
     * 是否已经运行了GC?
     * @var bool
     */
    protected $_gcRunned = false;
  
    /**
     * 关闭一个session
     * php mannual的matt at openflows dot org (20-Sep-2006 08:02)提到
     * Debian和Ubuntu发行版不会调用gc,所以在这里最好手工以概率触发一下。
     * BUT IS THAT TRUE?
     * @return bool
     *
     */
    public function close(){
        if(rand($this->_config[session.gc_probability],$this->_config[session.gc_divisor]) <= $this->_config[session.gc_probability]){
            $this->gc($this->_config[session.gc_maxlifetime]);
        }
    }
  
  
    /**
     * session回收
     * @param integer $maxlifetime session存活时间
     */
    public function gc($maxlifetime){
        if( true == $this->_gcRunned ){
            return true;
        }
        if(false == $this->_connected){
            return false;
        }
        $expiretime = intval($this->_timestamp - $maxlifetime);
        $this->_PDO->exec( "DELETE FROM {$this->_config[TBL_NAME]} WHERE `lasttime` < {$expiretime}" );
        $this->_gcRunned = true;
}
}
 
五、 参考文献
[1]每日签到插件和微博插件冲突的解决方案(签到bug修复):http://bbs.x.weibo.com/viewthread.php?tid=254
[2]IBM:另外5个设计模式:http://www.ibm.com/developerworks/cn/opensource/os-php-designpatterns/
[3]php.ini中_session设置问题导致插件故障的问题和排查简略:http://bbs.x.weibo.com/viewthread.php?tid=45
[4]cookie和session机制:http://hi.baidu.com/jmtbai/blog/item/a3b7d5f3b76cd818b17ec51a.html
[5]PHP函数session_set_save_handler例子:http://cn2.php.net/manual/en/function.session-set-save-handler.php