CodeIgniter框架源码笔记(13)——SESSION之文件File驱动实现

来源:互联网 发布:mac 翻墙 知乎 编辑:程序博客网 时间:2024/06/05 02:36

CI的文件驱动要满足以下三个条件:
1、驱动要实现open ,read ,write ,close ,destory ,gc六个方法。
session_start()时,调用了open(),read()方法。并有一定机率触发gc()方法。
session_commit()或session_write_close()时,触发write(),close()方法。
session_destory()会触发desotry()方法。

这六个方法实现的功能如下:
open:获取并创建文件保存的路径
read: 根据session_id读取或创建session_id对应的文件,获取文件操作指针并加锁flock
write:session内容变更时,将内容写入session_id对应的文件。该写入是全量写。session_data的内容包括新增的session键值对以及已经存在的键值对。
close:释放(flock)文件锁,并关闭(fclose)文件指针。
destory:删除服务端session_id对应的文件,并清除客户端对应session_id的cookie信息
gc:判断文件是否过期:根据文件最后一次修改时间(filemtime())和当前时间对比判断文件对应的session是否过期。该方法在session_start时一定机率调用。

2、驱动要支持session_regenerate_id()。
该方法重新生成一个新的session_id并创建以此session_id命名的文件。然后将原来老的session_id文件中保存的内容拷入新的文件中。最后删除老session_id所在的文件。

3、驱动要实现session锁:这里采用文件锁方式。

实现:文件驱动

class CI_Session_files_driver extends CI_Session_driver implements SessionHandlerInterface {    //文件保存路径    protected $_save_path;    //文件操作句柄    protected $_file_handle;    //文件名    protected $_file_path;    //是否新文件标识    protected $_file_new;    // ------------------------------------------------------------------------    //构造函数    public function __construct(&$params)    {        parent::__construct($params);        //初始化文件保存路径,根据配置文件设置php.ini中的session.save_path选项        if (isset($this->_config['save_path']))        {            $this->_config['save_path'] = rtrim($this->_config['save_path'], '/\\');            //根据配置文件设置php.ini中的session.save_path选项            ini_set('session.save_path', $this->_config['save_path']);        }        else        {            //如果配置文件中的$config['sess_save_path']不存在,则使用当前ini中默认的路径            $this->_config['save_path'] = rtrim(ini_get('session.save_path'), '/\\');        }    }    // ------------------------------------------------------------------------    //open方法    //第一个参数$save_path对应的是ini_get('session.save_path')    //第二个参数$name对应的是ini_get('session.name')    public function open($save_path, $name)    {        //如果文件中径不存在,尝试创建        if ( ! is_dir($save_path))        {            if ( ! mkdir($save_path, 0700, TRUE))            {                //如果无法创建目录,                throw new Exception("Session: Configured save path '".$this->_config['save_path']."' is not a directory, doesn't exist or cannot be created.");            }        }        //如果目录不可写,抛出异常        elseif ( ! is_writable($save_path))        {            throw new Exception("Session: Configured save path '".$this->_config['save_path']."' is not writable by the PHP process.");        }        //生成文件保存的路径,这里给文件保存目录加上一点料,避免冲突        $this->_config['save_path'] = $save_path;        $this->_file_path = $this->_config['save_path'].DIRECTORY_SEPARATOR            .$name // we'll use the session cookie name as a prefix to avoid collisions            .($this->_config['match_ip'] ? md5($_SERVER['REMOTE_ADDR']) : '');        return $this->_success;    }    // ------------------------------------------------------------------------    //read    //参数$session_id对应的是session_id()的值    public function read($session_id)    {        //如果session_id()对应的文件操作指针为空        if ($this->_file_handle === NULL)        {            // Just using fopen() with 'c+b' mode would be perfect, but it is only            // available since PHP 5.2.6 and we have to set permissions for new files,            // so we'd have to hack around this ...            //建议打开文件时使用c+模式,因为该模式当文件存在时不会删除文件原有内容 (w+模式下会清空原文件内容)            //但是该模式只有PHP 5.2.6后有效,所以我们不得不根据文件是否存在而做不同的操作。            //文件不存在就用'w+'模式,文件存在就用'r+'模式            //当请求的Session文件不存在,则采用'w+b'读写模式创建文件,获取操作句柄            if (($this->_file_new = ! file_exists($this->_file_path.$session_id)) === TRUE)            {                //采用'w+b'模式打开文件,获取操作句柄                if (($this->_file_handle = fopen($this->_file_path.$session_id, 'w+b')) === FALSE)                {                    //如果未能创建成功,则返回失败                    log_message('error', "Session: File '".$this->_file_path.$session_id."' doesn't exist and cannot be created.");                    return $this->_failure;                }            }            //如果请求的Session文件存在,则采用'r+b'读写模式打开文件,获取操作句柄            elseif (($this->_file_handle = fopen($this->_file_path.$session_id, 'r+b')) === FALSE)            {                //如果未能读取成功,则返回失败                log_message('error', "Session: Unable to open file '".$this->_file_path.$session_id."'.");                return $this->_failure;            }            //至此已成功获取文件指针,并赋给$this->_file_handle            //锁定文件指针对像$this->_file_handle(LOCK_EX是独占锁定)            //注意:释放锁(LOCK_UN)放在了close()函数中            if (flock($this->_file_handle, LOCK_EX) === FALSE)            {                //没锁定成功,记录日志,释放文件指针,然后返回失败.                log_message('error', "Session: Unable to obtain lock for file '".$this->_file_path.$session_id."'.");                fclose($this->_file_handle);                $this->_file_handle = NULL;                return $this->_failure;            }            // Needed by write() to detect session_regenerate_id() calls            //将$session_id赋给对像的属性$this->_session_id            //在session_regenerate_id()更改sessionid后,在write方法中用得到,这里保存了老的sessionid            $this->_session_id = $session_id;            //如果是新生成的文件,则设置文件权限600            if ($this->_file_new)            {                //只给读写权限,没有执行权限                chmod($this->_file_path.$session_id, 0600);                $this->_fingerprint = md5('');//摘要是空字符的md5                return '';            }        }        // We shouldn't need this, but apparently we do ...        // See https://github.com/bcit-ci/CodeIgniter/issues/4039        //如果$this->_file_handle === FALSE,则返回失败。        //这是git上一叫aanbar的小哥发现的,然后补上了$this->_file_handle === FALSE这个判断,        //因为fopen成功时返回文件指针,如果打开失败返回 FALSE        elseif ($this->_file_handle === FALSE)        {            return $this->_failure;        }        else        {            //如果指针不为空            //将文件内部offset指针重新指向开头            rewind($this->_file_handle);        }        $session_data = '';        //读取内容        for ($read = 0, $length = filesize($this->_file_path.$session_id); $read < $length; $read += strlen($buffer))        {            if (($buffer = fread($this->_file_handle, $length - $read)) === FALSE)            {                break;            }            $session_data .= $buffer;        }        //根据内容生成文件摘要        $this->_fingerprint = md5($session_data);        return $session_data;    }    // Write 注意:Session的写入都是全量写,不是增量写    //参数$session_id对应的是session_id()的值    //参数$session_data不只是当前待写入的数据,它包含整个SESSION已保存的数据+当前要写入的数据    public function write($session_id, $session_data)    {        /***************** session_regenerate_id()处理 开始 *****************/        //如果程序调用了session_regenerate_id(),就会造成函数调用之后的$session_id(参数$session_id)和函数调用之前的$session_id( $this->_session_id)不一致。        //这时我们需要关闭旧的文件指针,打开新的文件获取操作指针        //这里的if条件语句其实就是个短路操作,分解开就是        /*if ($session_id !== $this->_session_id){            $close_flag=$this->close();//调用close关闭旧的指针            $read_flag=$this->read($session_id);//read函数参数为新的$session_id,从而创建新的文件            //上述两步中有一步出错,则返回失败。            //实际上 OR 也是短路操作,第一个$close_flag===$this->_failure的话,就不会再往后面执行$this->read($session_id)            //为说明思路,先忽略这点            if($close_flag=== $this->_failure OR $read_flag===$this->_failure)                return $this->_failure;        }*/        if ($session_id !== $this->_session_id && ($this->close() === $this->_failure OR $this->read($session_id) === $this->_failure))        {            return $this->_failure;        }        //如果$this->_file_handle)不是资源类型,则返回错误        if ( ! is_resource($this->_file_handle))        {            return $this->_failure;        }        //如果当前请求的session内容摘要和$session_data是一样的,那么说明生成新的sessionid文件成功        //并用用touch函数测试一下文件是否存在,同时检测$this->_file_new标记(该标记在read中文件不存在需要新创建时会被设置为true)        //如果条件都满足,就返回成功了        elseif ($this->_fingerprint === md5($session_data))        {            return ( ! $this->_file_new && ! touch($this->_file_path.$session_id))                ? $this->_failure                : $this->_success;        }        /***************** session_regenerate_id()处理 结束 *****************/        //如果是现成的文件,那么先清空内容        if ( ! $this->_file_new)        {            //清空文件内容            ftruncate($this->_file_handle, 0);            //将文件内部offset指针重新指向开头            rewind($this->_file_handle);        }        //Session的写入都是全量写,不是增量写        //把$session_data内容写入文件。        if (($length = strlen($session_data)) > 0)        {            for ($written = 0; $written < $length; $written += $result)            {                if (($result = fwrite($this->_file_handle, substr($session_data, $written))) === FALSE)                {                    break;                }            }            if ( ! is_int($result))            {                $this->_fingerprint = md5(substr($session_data, 0, $written));                log_message('error', 'Session: Unable to write data.');                return $this->_failure;            }        }        //获取SESSION内容摘要        $this->_fingerprint = md5($session_data);        return $this->_success;    }    // Close    //close()在当前请求的程序执行完毕后执行,或 在调用session_commit(),session_write_close()时执行    public function close()    {        if (is_resource($this->_file_handle))        {            //释放文件锁            flock($this->_file_handle, LOCK_UN);            //释放文件指针            fclose($this->_file_handle);            //清空变量            $this->_file_handle = $this->_file_new = $this->_session_id = NULL;        }        return $this->_success;    }    // Destroy    public function destory($session_id)    {        //调用close()方法        if ($this->close() === $this->_success)        {            if (file_exists($this->_file_path.$session_id))            {                //删除对应的客户端cookie                $this->_cookie_destroy();                //删除服务端文件                return unlink($this->_file_path.$session_id)                    ? $this->_success                    : $this->_failure;            }            return $this->_success;        }        //调用close()方法失败        elseif ($this->_file_path !== NULL)        {            //清除 PHP 缓存的该文件信息, is_file(),is_dir(), file_exists()都有影响            clearstatcache();            //重复上面的语句,再删除一次            if (file_exists($this->_file_path.$session_id))            {                $this->_cookie_destroy();                return unlink($this->_file_path.$session_id)                    ? $this->_success                    : $this->_failure;            }            return $this->_success;        }        return $this->_failure;    }    // ------------------------------------------------------------------------    //gc方法。当session_start()时有机率调用,删除过期文件    public function gc($maxlifetime)    {        if ( ! is_dir($this->_config['save_path']) OR ($directory = opendir($this->_config['save_path'])) === FALSE)        {            log_message('debug', "Session: Garbage collector couldn't list files under directory '".$this->_config['save_path']."'.");            return $this->_failure;        }        $ts = time() - $maxlifetime;        //确定session文件名的正则规定,免得误删文件        $pattern = sprintf('/^%s[0-9a-f]{%d}$/', preg_quote($this->_config['cookie_name'], '/'),            ($this->_config['match_ip'] === TRUE ? 72 : 40)        );        while (($file = readdir($directory)) !== FALSE)        {            // If the filename doesn't match this pattern, it's either not a session file or is not ours            //根据创建时间判断是否过期            if ( ! preg_match($pattern, $file)                OR ! is_file($this->_config['save_path'].DIRECTORY_SEPARATOR.$file)                OR ($mtime = filemtime($this->_config['save_path'].DIRECTORY_SEPARATOR.$file)) === FALSE                OR $mtime > $ts)            {                continue;            }            unlink($this->_config['save_path'].DIRECTORY_SEPARATOR.$file);        }        closedir($directory);        return $this->_success;    }}
0 0
原创粉丝点击