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('');//摘要是Null 字元的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; }}