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