前面的話
Session技術和Cookie相似,都是用來儲存使用者的相關資料。但最大的不同之處在於Cookie是將數據存放在客戶端的計算機之中,而Session則是將數據存放於服務器系統之下。Session的中文意思是會話,在Web系統中,通常是指用戶與Web系統的對話過程。本文將詳細介紹Session的內容
Session ID
在Web技術發展史上,雖然Cookie技術的出現是一個重大的變革,但Cookie是在客戶端的計算機中保存資料,所以引起了一個爭議。用戶有權阻止Cookie的使用,使Web服務器無法通過Cookie來跟蹤用戶信息。而Session技術是將使用者相關的資料存放在服務器的系統之下,所以使用者無法停止Session的使用
Session在客戶端僅需要保存由服務器為用戶創建的一個Session標識符,稱為Session ID,而在服務器端(文件或數據庫或MemCache中)保存Session變量的值。Session ID是一個既不會重復,又不容易被找到規律的,由32位16進制數組成的字符串

Session ID會保存在客戶端的Cookie中,如果用戶阻止Cookie的使用,則可以將Session ID保存在用戶瀏覽器地址欄的URL中。當用戶請求Web服務器時,就會把Session ID發送給服務器,再通過Session ID提取保存在服務器中的Session變量。可以把Session中保存的變量,當做是這個用戶的全局變量,同一個用戶對每個腳本的訪問都共享這些變量
當某個用戶向Web服務器發出請求時,服務器首先會檢查這個客戶端的請求里是否已經包含了一個Session ID。如果包含,說明之前已經為此用戶創建過Session,服務器則按該Session ID把Session檢索出來使用。如果客戶端請求不包含Session ID,則為該用戶創建一個Session,並且生成一個與此Session相關聯的Session ID,在本次響應中被傳送給客戶端保存
【session_start()】
用戶向Web服務器發出請求時,必須首先使用session_start()函數來啟動新會話或者重用現有會話,成功開始會話返回TRUE,反之返回FALSE
bool session_start ([ array $options = [] ] )
因為基於Cookie的Session是在開啟的時候,調用session_start()函數會生成一個唯一的SessionID,需要保存在客戶端電腦的Cookie中,和setCookie()函數一樣,調用之前不能有任何的輸出,空格或空行也不行
如果已經開啟過Session,再次調用Session_start()函數時,不會再創建個新的Session ID。因為當用戶再次訪問服務器時,該函數會通過從客戶端攜帶過來的Session ID,返回已經存在的Session。所以在會話期間,同一個用戶在訪問服務器上任何一個頁面時,都是使用同一個 Session ID
session_start();

而且,使用session_start()方法會在服務器端建立一個同名的Session文件(文本文件)

如果不想在每個腳本都使用Session_start()函數來開啟Session,可以在php.ini里設置"session.auto_start=1",則無須每次使用Session之前都要調用session_start()函數。但啟用該選項也有一些限制,則不能將對象放入Session中,因為類定義必須在啟動Session之前加載。所以不建議使用php.ini中的session.auto_start屬性來開啟Session
讀寫session
使用session_start()方法啟動session會話后,要通過訪問$_SESSION數組來讀寫session。和$_POST、$_GET、$_COOKIE類似,$_SESSION也是超全局數組
使用$_SESSION數組將數據存入同名Session文件中
<?php session_start(); $_SESSION['username'] = 'huochai'; $_SESSION['age'] = 28; ?>
同名Session文件可以直接使用文本編輯器打開,該文件的內容結構如下所示:
變量名|類型:長度:值;
<?php session_start(); print_r ($_SESSION);//Array ( [username] => huochai [age] => 28 ) ?>
Session變量會被保存在服務器端的某個文件中,該文件的位置是通過php.ini文件,在session.save_path屬性指定的目錄下
配置Session
在PHP配置文件php.ini中,有一組和Session相關的配置選項。通過對一些選項重新設置新值,就可以對Session進行配置,否則使用默認的Session配置
phpinfo();

session.auto_start=0;在請求啟動時初始化session session.cache_expire=180;設置緩存中的會話文檔在n分鍾后過時 session.cookie_lifetime=0;設置cookie保存時間(s),相當於設置Session過期時間,為0時表示直到瀏覽器被重啟 session.cookie_path=/;cookie的有效路徑 session.cookie_domain=;cookie的有效域 session.name=PHPSESSID;用在cookie里的session的名字 session.save_handler=files;用於保存/取回數據的控制方式 session.save_path=/tmp;在save_handler設為文件時傳給控制器的參數,這是數據文件將保存的路徑. session.use_cookies=1;是否使用cookies
銷毀Session
當使用完一個Session變量后,可以將其刪除,當完成一個會話后,也可以將其銷毀。如果用戶想退出Web系統,就需要提供一個注銷的功能,把所有信息在服務器中銷毀。銷毀和當前Session有關的所有的資料,可以調用session_destroy()函數結束當前的會話,並清空會話中的所有資源
【session_destroy()】
bool session_destroy ( void )
session_destroy()銷毀當前會話中的全部數據,刪除同名Session文件,但是不會重置當前會話所關聯的全局變量,也不會重置會話cookie。如果需要再次使用會話變量,必須重新調用session_start()函數
<?php session_start(); session_destroy(); ?>

可以使用unset()函數來釋放在Session中注冊的單個變量
print_r ($_SESSION);//'Array ( [username] => huochai [age] => 28 )' unset($_SESSION['username']); unset($_SESSION['age']); print_r ($_SESSION);//'Array()'
[注意]不要使用unset($_SESSION)刪除整個$_SESSION數組,這樣將不能再通過$_SESSION超全局數組注冊變量了
如果想把某個用戶在Session中注冊的所有變量都刪除,可以直接將數組變量$_SESSION賦值為一個空數組
$_SESSION=array();
PHP默認的Session是基於Cookie的,Session ID被服務器存儲在客戶端的Cookie中,所以在注銷Session時也需要清除Cookie中保存的SessionID,而這就必須借助setCookie()函數完成。在Cookie中,保存Session ID的Cookie標識名稱就是Session的名稱,這個名稱是在php.ini中,通過session.name屬性指定的值。在PHP腳本中,可以通過調用session_name()函數獲取Session名稱。刪除保存在客戶端Cookie中的Session ID
if(isset($_COOKIE[session_name()])) { setCookie(session_name(),'',time()-3600); }
通過前面的介紹可以總結出來,Session的注銷過程共需要四個步驟
<?php //第一步:開啟Session並初始化 session_start(); //第二步:刪除所有Session的變量,也可用unset($_SESSION[xxx])逐個刪除 $_SESSION = array(); //第三步:如果使用基於Cookie的Session,使用setCooike()刪除包含Session Id的Cookie if (isset($_COOKIE[session_name()])) { setcookie(session_name(),'', time()-42000); } //第四步:最后徹底銷毀Session,刪除服務器端保留session信息的文件 session_destroy(); ?>
自動回收
如果沒有通過上述步驟銷毀Session,而是直接關閉瀏覽器,或斷網等情況,在服務器端保存的Session文件是不會被刪除的。因為在php.ini配置文件中,默認的session.cookie_lifetime=0,表示Session ID在客戶端Cookie的有效期限為直到關閉瀏覽器。Session ID消失了,但服務器端保存的Session文件並沒有被刪除。所以,沒有被Session ID關聯的服務器端Session文件成為了垃圾,而系統則提供了自動清理的機制
服務器保存的Session文件是普通文本文件,都有文件修改時間。通過在php.ini配置文件中設置session.gc_maxlifetime選項來設置一個到期時間(默認為1440秒,即24分鍾)。垃圾回收程序在所有Session文件中排查出大於24分鍾的文件。如果用戶還在使用該文件,那么這個Session文件的修改時間就會被更新,將不會被排查到
排除出來后,並不會立刻清理垃圾,而是根據配置文件php.info中session.gc_probability/session.gc_divisor這兩個值的比例來決定何時清理,默認值是1/100。表示排查100次,才有一次可能會啟動垃圾回收機制,來自動回收垃圾。當然,這個值是可以修改的,但還是要兼顧服務器的運行性能和存儲空間
傳遞session
使用Session跟蹤一個用戶,是通過在各個頁面之間傳遞唯一的Session ID,並通過Session ID提取這個用戶在服務器中保存的Session變量。常見的Session ID傳送方法有以下兩種
1、基於Cookie的方式傳遞Session ID。這種方法更優化,但由於不總是可用,因為用戶在客戶端可以屏蔽Cookie
2、通過URL參數進行傳遞,直接將會話ID嵌入到URL中去
在Session的實現中通常都是采用基於Cookie的方式,客戶端保存的Session ID就是一個Cookie。當客戶禁用Cookie時,Session ID就不能再在Cookie中保存,也就不能在頁面之間傳遞,此時Session失效。不過PHP5在Linux平台可以自動檢查Cookie狀態,如果客戶端將它禁用,則系統自動把Session ID附加到URL上傳送。而使用Windows系統作為Web服務器則無此功能
【通過Cookie傳遞Session ID】
如果客戶端沒有禁用Cookie,則在PHP腳本中通過session_start()函數進行初始化后,服務器會自動發送HTTP標頭將Session ID保存到客戶端電腦的Cookie中
類似於下面的設置方式
//虛擬向Cookie中設置Session ID的過程 setCookie(session_name(),session_id(),0,'/')
第一個參數中調用session_name()函數,返回當前Session的名稱作為Cookie的標識名稱。Session名稱的默認值為PHPSESSID,是在php.ini文件中由session.name選項指定的值。也可以在調用session_name()函數時提供參數改變當前Session的名稱
echo session_name();//PHPSESSID
第二個參數中調用session_id()函數,返回當前Session ID作為Cookie的值。也可以通過調用session_id()函數時提供參數設定當前Session ID
echo session_id();//kstvdmae177qqk6jgvg6td12l1
第三個參數的值0,是通過在php.ini文件中由session.cookiejifetime選項設置的值。默認值為0,表示SessIon ID將在客戶機的Cookie中延續到瀏覽器關閉
最后一個參數'/',也是通過PHP配置文件指定的值,在php.ini中由session.cookie.path選項設置的值。默認值為'/',表示在Cookie中要設置的路徑在整個域內都有效
如果服務器成功將Session ID保存在客戶端的Cookie中,當用戶再次請求服務器時,就會把Session ID發送回來。所以當在腳本中再次使用session_start()函數時,就會根據Cookie中的Session ID返回已經存在的Session
【通過URL傳遞Session ID】
如果客戶瀏覽器支持Cookie,就把Session ID作為Cookie保存在瀏覽器中。但如果客戶端禁止Cookie的使用,瀏覽器中就不存在作為Cookie的Session ID,因此在客戶請求中不包含Cookie信息。如果調用session_start()函數時,無法從客戶端瀏覽器中取得作為Cookie的Session ID,則又創建了一個新的Session ID,也就無法跟蹤客戶狀態。因此,每次客戶請求支持Session的PHP腳本,session_start()函數在開啟Session時都會創建一個新的Session,這樣就失去了跟蹤用戶狀態的功能
如果客戶瀏覽器不支持Cookie,PHP則可以重寫客戶請求的URL,把Session ID添加到URL信息中。可以手動地在每個超鏈接的URL中都添加一個Session ID,但工作量比較大,不建議使用這種方式。如下所示:
<?php session_start(); echo '<a href="demo.php?'.session_name().'='.session_id() .'">鏈接演示</a>'; ?>
在使用Linux系統做服務器時,並且選用PHP4.2以后的版本,則在編輯PHP時如果使用了-enable-trans-sid配置選項,和運行時選項session.use_trans_sid都被激活,在客戶端禁用Cookie時,相對URL將被自動修改為包含會話ID。如果沒有這么配置,或者使用Windows系統作為服務器時,可以使用常量SID。該常量在會話啟動時被定義,如果客戶端沒有發送適當的會話Cookie,則SID的格式為session_name=session_id,否則就為一個空字符串。因此可以無條件地將其嵌入到URL中去。如下所示
//當阻止cookie時,SID返回'PHPSESSID=p2qouo8hjarul0a0ii5jmocmc0',否則返回一個空字符串 echo SID;
<?php session_start(); $_SESSION["usemame"]="admin"; echo "Session ID:".session_id()."<br>"; ?> <a href="test2.php?<?php echo SID ?>">通過URL傳遞Session ID</a>
如果使用Linux系統作為服務器,並配置好相應的選項,就不用手動在每個URL后面附加SID,相對URL將被自動修改為包含Session ID。但要注意,非相對的URL被假定為指向外部站點,因此不能附加SID。因為這可能是個安全隱患,會將SID泄露給不同的服務器
自定義Session
在系統中使用Session技術跟蹤用戶時,Session默認的處理方式是使用Web服務器中的文件來記錄每個用戶的會話信息,通過php.ini中的session_save_path創建會話數據文件的路徑。這種默認的處理方式雖然很方便,但也有一些缺陷。例如,登錄用戶如果非常大,文件操作的I/O開銷就會很大,會嚴重影響系統的執行效率。另外,最主要的是本身的session機制不能跨機,因為對於訪問量比較大的系統,通常都是采用多台web服務器進行並發處理,如果每台web服務器都各自獨立地處理Session,就不可能達到跟蹤用戶的目的。這時就需要改變session的處理方式,常見的跨機方法就是通過自定義session的存儲方式,可以將session信息使用NFS或SAMBA等共享技術保存到其他服務器中,或使用數據庫來保存session信息,最優的方式是使用memcached來進行session存儲
無論是用memcached、數據庫、還是通過NFS或SAMBA共享session信息,其原理是一樣的,都是通過PHP中的session_set_save_handler()函數來改變默認的處理方式,指定回調函數來自定義處理
Session_set_save_hander(callback open,callback close,call read,callback write,callback destro,callback gc);
該函數共需要6個回調函數作為必選參數,分別代表了Session生命周期中的6個過程,用戶通過自定義每個函數,來設置Session生命周期中每個環節的信息處理
回調函數的執行時機如下所示
回調函數 描述 open 運行session_start()時執行,該函數需要聲明兩個參數,系統自動將php.ini中的session_save_path選項值傳遞給該函數的第一個參數,將Session名自動傳遞給第二個參數中,返回true則可以繼續向下執行 close 該函數不需要參數,在腳本執行完成或調用session_write_close()、session_destroy()時被執行,即在所有session操作完成后被執行。如果不需要處理,則直接返回true即可 read 在運行session_start()時執行,因為在開啟會話時,會read當前session數據並寫入$_SESSION變量。需要聲明一個參數,系統會自動將Session ID傳遞給該函數,用於通過Session ID獲取對應的用戶數據,返回當前用戶的會話信息寫入$_SESSION變量 write 該函數在腳本結束和對$_SESSION變量賦值數據時執行。需要聲明兩個參數,分別是Session ID和串行化后Session信息字符串。在對$_SESSION變量賦值時,就可以通過Session ID找到存儲的位置,並將信息寫入。存儲成功可以返回true繼續向下執行 destroy 在運行session_destroy()時執行,需要聲明一個參數,系統會自動將Session ID傳遞給該函數,去刪除對應的會話信息 gc 垃圾回收程序啟動時執行。需要聲明一個參數,系統自動將php.ini中的session_gc_maxlifetime選項值傳給該函數,用於刪除超過這個時間的Session信息,返回true則可以繼續向下執行
在運行session_start()時分別執行了open(啟動會話)、read(讀取session數據至$_SESSION)和gc(清理垃圾),腳本中所有對$_SESSION的操作均不會調用這些回調函數。在調用session_destroy()函數時,執行destroy銷毀當前session(一般是刪除相應的記錄或文件),但此回調函數銷毀的只是Session的數據,此時如果輸出$_SESSION變量,仍然有值,但此值不會再close后被寫回去。在調用session_write_close()函數時執行write和close,保存$_SESSION至存儲,如果不手工使用此方法,則會在腳本結束時被自動執行
[注意]session_set_save_hander()函數必須在php.ini中設置session_save_hander選項的值為”user”時(用戶自定義處理器),才會被系統調用
<?php $sess_save_path =""; function open($save_path,$session_name){ global $sess_save_path; $sess_save_path = $save_path; return true; } function close(){ return true; } function read($id){ global $sess_save_path; $sess_file ="{$sess_save_path}/sess_{$id}"; return (string) @file_get_contents($sess_file); } function write($id,$sess_data){ global $sess_save_path; $sess_file ="{$sess_save_path}/sess_{$id}"; if($fp=@fopen($sess_file,"w")){ $return = fwrite($fp,$sess_data); fclose($fp); return $return; }else{ return false; } } function destroy($id){ global $sess_save_path; $sess_file ="{$sess_save_path}/sess_{$id}"; return (@unlink($sess_file)); } function gc($maxlifetime){ global $sess_save_path; foreach(glob("{$sess_save_path}/sess_*") as $filename){ if(filemtime($filename) + $maxlifetime <time() ){ @unlink($filename); } } return true; } session_set_save_hander(“open","close","read","write","destroy","gc"); session_start(); ?>
數據庫處理
如果網站訪問量非常大,需要采用負載均衡技術搭載多台Web服務器協同工作,就需要進行Session同步處理。使用數據庫處理Session會比使用NFS及SAMBA更占優勢,可以專門建立一個數據庫服務器存放Web服務器的Session信息,當用戶不管訪問集群中的哪個Web服務器,都會去這個專門的數據庫,訪問自己在服務器端保存的Session信息,以達到Session同步的目的。另外,使用數據庫處理Session還可以給我們帶來很多好處,比如統計在線人數等。如果mysql也做了集群,每個mysql節點都要有這張表,並且這張Session表的數據要實時同步
在使用默認的文件方式處理Session時,有3個比較重要的屬性,分別是文件名稱、文件內容及文件的修改時間:通過文件名稱中包含的Session ID,用戶可以找到自己在服務器端的Session文件;通過文件內容用戶可以在各個腳本中存取$_session變量;通過文件的修改時間則可以清除所有過期的Session文件。所以使用數據表處理Session信息,也最少要有這三個字段(Session ID、修改時間、Session內容信息),當然如果考慮更多的情況,例如,用戶改變了IP地址,用戶切換了瀏覽器等,還可以再自定義一些其他字段。下面為Session設計的數據表結構包含5個字段,創建保存Session信息表session的SQL語句如下所示:
CREATE TABLE session( sid CHAR(32) NOT NULL DEFAULT '', update INT NOT NULL DEFAULT 0, client_ip CHAR(15) NOT NULL DEFAULT '', user_agent CHAR(200) NOT NULL DEFAULT '', data TEXT, PRIMARY KEY(sid) );
數據表session創建成功后,再通過自定義的處理方式,將Session信息寫入到數據庫中
<?php class DBSession { public static $pdo; //pdo的對象 public static $ctime; //當前時間 public static $maxlifetime; //最大的生存時間 public static $uip; //用戶正在用的ip public static $uagent; //用戶正在用的瀏覽器 //開啟和初使化使用的, 參數需要一個路 public static function start(PDO $pdo) { self::$pdo = $pdo; self::$ctime = time(); self::$maxlifetime = ini_get("session.gc_maxlifetime"); self::$uip = !empty($_SERVER['HTTP_CLIENT_IP']) ? $_SERVER['HTTP_CLIENT_IP'] : (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : (!empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : "") ); filter_var(self::$uip, FILTER_VALIDATE_IP) && self::$uip = ''; self::$uagent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : "" ; //注冊過程, 讓PHP自己處理session時,找這個函數指定的幾個周期來完成 session_set_save_handler( array(__CLASS__, "open"), array(__CLASS__,"close"), array(__CLASS__, "read"), array(__CLASS__, "write"), array(__CLASS__, "destroy"), array(__CLASS__,"gc")); session_start(); //開啟會話 } // 開啟時, session_start() public static function open($path, $name) { return true; } //關閉 public static function close() { return true; } //讀取 echo $_SESSION['username'] public static function read($sid) { $sql = "select * from session where sid = ?"; $stmt = self::$pdo -> prepare($sql); $stmt -> execute(array($sid)); $result = $stmt -> fetch(PDO::FETCH_ASSOC); //如果還沒有會話信息,返回空字符串 if(!$result) { return ''; } //如果超出時間,銷毀session if($result['utime'] + self::$maxlifetime < self::$ctime) { self::destroy($sid); return ''; } //如果用戶換了ip或換了瀏覽器 if($result['uip'] != self::$uip || $result['uagent'] != self::$uagent) { self::destroy($sid); return ''; } return $result['sdata']; } //寫入 $_SESSION['username'] = "meizi"; public static function write($sid, $data) { //通過sid獲取已經有的數據 $sql = "select * from session where sid = ?"; $stmt = self::$pdo->prepare($sql); $stmt -> execute(array($sid)); $result = $stmt -> fetch(PDO::FETCH_ASSOC); //如果已經獲取到了數據,就不插入而更新 if($result) { //如果數據和原來的不同才更新 if($result['sdata'] != $data || $result['utime']+10 < self::$ctime) { $sql = "update session set sdata = ?, utime = ? where sid=?"; $stmt = self::$pdo->prepare($sql); $stmt -> execute(array($data, self::$ctime, $sid)); } //如果沒有數據,就新插入一條數據 } else { if(!empty($data)) { $sql = "insert into session(sid, sdata, utime, uip, uagent) values(?, ?, ?, ?, ?)"; $stmt = self::$pdo -> prepare($sql); $stmt -> execute(array($sid, $data, self::$ctime, self::$uip, self::$uagent)); } } } //銷毀 session_destroy() public static function destroy($sid) { $sql = "delete from session where sid=?"; $stmt = self::$pdo->prepare($sql); return $stmt -> execute(array($sid)); } //回收垃圾 public static function gc($maxlifetime) { // utime < ctime - self::$maxlifetime $sql = "delete from session where utime < ?"; $stmt = self::$pdo->prepare($sql); return $stmt -> execute(array(self::$ctime - self::$maxlifetime)); } } //開啟 DBSession::start($pdo);
memcached處理
用數據庫來同步Session會加大數據庫的負擔,因為數據庫本來就是容易產生瓶頸的地方,但如果采用MemCache來處理Session是非常合適的,因為MemCache的緩存機制和Session非常相似。另外,MemCach可以做分布式,能夠把Web服務器中的內存組合起來,成為一個”內存池”,不管是哪個服務器產生的Session,都可以放到這個“內存池”中,其他的Web服務器都可以使用。以這種方式來同步Session,不會加大數據庫的負擔,並且安全性也要比使用Cookie高。把session放到內存里面,讀取也要比其他處理方式快很多
自定義使用memcached處理session信息,和自定義數據庫的處理方式相同,但要簡單得多,因為MemCache的工作機制和Session技術很相似
<?php class MemSession { public static $mem; //pdo的對象 public static $maxlifetime; //最大的生存時間 public static function start(Memcache $mem) { self::$mem = $mem; self::$maxlifetime = ini_get("session.gc_maxlifetime"); //注冊過程, 讓PHP自己處理session時,按照這個函數指定的幾個周期來完成 session_set_save_handler( array(__CLASS__, "open"), array(__CLASS__,"close"), array(__CLASS__, "read"), array(__CLASS__, "write"), array(__CLASS__, "destroy"), array(__CLASS__,"gc")); session_start(); //開啟會話 } // 開啟時,session_start() public static function open($path, $name) { return true; } //關閉 public static function close() { return true; } //讀取 echo $_SESSION['username'] public static function read($sid) { $data = self::$mem -> get($sid); if(empty($data)) { return ''; } return $data; } //寫入 public static function write($sid, $data) { self::$mem -> set($sid, $data, MEMCACHE_COMPRESSED, self::$maxlifetime); } //銷毀 session_destroy() public static function destroy($sid) { self::$mem -> delete($sid, 0); } //回收垃圾 public static function gc($maxlifetime) { return true; } } //創建對象 $mem = new Memcache(); //添加兩台memcache服務器 $mem -> addServer("localhost", 11211); $mem -> addServer("192.168.1.3", 11211); //開啟 MemSession::start($mem); ?>