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

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;
    }

}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值