http協議是WEB服務器與客戶端(瀏覽器)相互通訊的協議,它是一種無狀態協議。所謂無狀態,指的是不會維護http請求數據,http請求是獨立的,非持久的。而愈來愈複雜的WEB應用,須要保存一些用戶狀態信息。這時候,Session這種方案應需而生。PHP從4.1開始支持Session管理。php
session是很抽象的一個概念。咱們不妨先從與它幾個息息相關的有跡可尋的小切入點入手,而後逐漸地認識瞭解它。html
session存儲web
首先,咱們爲何須要Session,就是由於咱們須要存儲各個用戶的狀態數據。那麼試問,若是由你來設計解決這個需求的方案,那麼也許你會設置這樣一個數據表用與存儲各個用戶的狀態信息:apache
uid created data max_age數組
94c55770fdf044a7 1270802787 jtUsername=admin 14400瀏覽器
2c37df64277e4409 1270822787 jtUsername=Joe;jtBooks=8; 14400安全
… … … …服務器
uid : 用戶惟一標識符,區分其它用戶 cookie
created : 記錄產生時間session
data : 存放與用戶相關的數據
max_age : 記錄的有效時間
一樣地,PHP設計管理session方案也大體如此,它分別包含了如下信息:
1. session id
用戶session惟一標識符,隨機生成的一串字符串,具備惟一性,隨機性。主要用於區分其它用戶的session數據。用戶第一次訪問web頁面的時候,php的session初始化函數調用會分配給當前來訪用戶一個惟一的ID,也稱之爲session_id。
2. session data
咱們把須要經過session保存的用戶狀態信息,稱爲用戶session數據,也稱爲session數據。
3. session file
PHP默認將session數據存放在一個文件裏。咱們把存放session數據的文件稱爲session文件。它由特殊的php.ini設置session.save_path指定session文件的存放路徑,CentOS5.3操做系統,PHP5.1默認存放在/var/lib/php/session目錄中。用戶session文件的名稱,就是以sess_爲前綴,以session_id爲結尾命名,好比session id爲vp8lfqnskjvsiilcp1c4l484d3,那麼session文件名就是sess_vp8lfqnskjvsiilcp1c4l484d3
4. session lifetime
咱們把初始化session開始,直到註銷session這段期間,稱爲session生命週期,這樣有助於咱們理解session管理函數。
由此,咱們可見: 當每一個用戶訪問web, PHP的session初始化函數都會給當前來訪用戶分配一個惟一的session ID。而且在session生命週期結束的時候,將用戶在此週期產生的session數據持久到session文件中。用戶再次訪問的時候,session初始化函數,又會從session文件中讀取session數據,開始新的session生命週期。
與session存儲相關php.ini設置
1. session.save_handler = file
用於讀取/回寫session數據的方式,默認是files。它會讓PHP的session管理函數使用指定的文本文件存儲session數據
2. session.save_path =「/var/lib/php/session」
指定保存session文件的目錄,能夠指定到別的目錄,可是指定目錄必需要有httpd守護進程屬主(好比apache或www等)寫權限,不然沒法回存session數據。當指定目錄不存在時,php session環境初始化函數是不會幫你建立指定目錄的,因此須要你手工創建指定目錄。
它還能夠寫成這樣session.save_path =「N;/path」 其中N是整數。這樣使得不是全部的session文件都保存在同一個目錄中,而是分散在不一樣目錄。這對於服務器處理大量session文件是頗有幫助的。(注:目錄須要本身手工建立)
3. session.auto_start = 0
若是啓用該選項,用戶的每次請求都會初始化session。咱們推薦不啓用該設置,最好經過session_start()顯示地初始化session。
Session同步數據
一旦調用了session_start()初始化session,就意味着開始了一個session生命週期。也就是宣佈了,可使用相關函數操做$_SESSION來管理session數據。這個session生命週期產生的數據並無實時地寫入session文件,而是經過$_SESSION變量寄存在內存中。那麼,寄存在內存的數據何時會寫入到session文件?這也是咱們這一小節的主要測試內容。
在進行測試以前,先讓咱們介紹幾個影響session數據的PHP函數、或事件
1. session_start()
函數session_start會初始化session,也標識着session生命週期的開始。要使用session,必須初始化一個session環境。有點相似於OOP概念中調用構造函數構建立對象實例同樣。
session初始化操做,聲明一個全局數組$_SESSION,映射寄存在內存的session數據。若是session文件已經存在,而且保存有session數據,session_start()則會讀取session數據,填入$_SESSION中,開始一個新的session生命週期。
2. $_SESSION
它是一個全局變量,類型是Array,映射了session生命週期的session數據,寄存在內存中。在session初始化的時候,從session文件中讀取數據,填入該變量中。在session生命週期結束時,將$_SESSION數據寫回session文件。
3. session_register()
在session生命週期內,使用全局變量名稱將注全局變量註冊到當前session中。所謂註冊,就是將變量填入$_SESSION中,值爲NULL。它不會對session文件進行任何IO操做,只是影響$_SESSION變量。注意,它的正確寫法是session_register(‘varname’),而不是session_register($varname)
4. session_unregister()
與session_register操做正好相反,即在session生命週期,從當前session註銷指定變量。一樣隻影響$_SESSION,並不進行任何IO操做。
5. session_unset()
在session生命週期,從當前session中註銷所有session數據,讓$_SESSION成爲一個空數組。它與unset($_SESSION)的區別在於:unset直接刪除$_SESSION變量,釋放內存資源;另外一個區別在於,session_unset()僅在session生命週期可以操做$_SESSION數組,而unset()則在整個頁面(page)生命週期都能操做$_SESSION數組。session_unset()一樣不進行任何IO操做,隻影響$_SESSION數組。
6. session_destroy()
若是說session_start()初始化一個session的話,而它則註銷一個session。意味着session生命週期結束了。在session生命週期結整後,session_register, session_unset, session_register都將不能操做$_SESSION數組,而$_SESSION數組依然能夠被unset()等函數操做。這時,session意味着是未定義的,而$_SESSION依然是一個全局變量,他們脫離了關映射關係。
經過session_destroy()註銷session,除告終束session生命週期外,它還會刪除sesion文件,但不會影響當前$_SESSION變量。即它會產生一個IO操做。
7. session_regenerate_id()
調用它,會給當前用戶從新分配一個新的session id。而且在結束當前頁面生命週期的時候,將當前session數據寫入session文件。前提是,調用此函數以前,當前session生命週期沒有被終止(參考第9點)。它會產生一個IO操做,建立一個新的session文件,建立新的session文件的是在session結束以前,而不是調用此函數就當即建立新的session文件。
8. session_commit()
session_commit()函數是session_write_close()函數的別名。它會結束當前session的生命週期,而且將session數據當即強制寫入session文件。不推薦經過session_commit()來手工寫入session數據,由於PHP會在頁面生命週期結束的時候,自動結束當前沒有終止的session生命週期。它會產生一個IO寫操做
9. end session
結束session,默認是在頁面生命週期結束的以前,PHP會自動結束當前沒有終止的session。可是還能夠經過session_commit()與session_destroy()二個函數提早結束session。無論是哪一種方式,結束session都會產生IO操做,分別不同。默認狀況,產生一個IO寫操做,將當前session數據寫回session文件。session_commit()則是調用該函數那刻,產生一個IO寫操做,將session數據寫回session文件。而session_destroy()不同在於,它不會將數據寫回session文件,而是直接刪除當前session文件。有趣的是,無論是session_commit(),仍是session_destroy()都不會清空$_SESSION數組,更不會刪除$_SESSION數組,只是全部session_*函數不能再操做session數據,由於當前的session生命週期終止了,即不能操做一個未定義對象。
爲了驗證以上陳述,咱們能夠作如下測試
任務1: 觀察session初始化與默認結束session的時候,產生的IO操做
<?php
//@file test_session_2.php
session_start();
$pg_uuid = 'ac606826-9620-490b-b850-ea9dbce6cfd5';
//註冊全局變量pg_uuid到session,但$_SESSION['pg_uuid']值爲NULL,隻影響$_SESSION
session_register('pg_uuid');
var_dump($_SESSION);
fopen(__FILE__, "r");
[root@localhost ~]# strace -p `cat /var/run/httpd.pid`
Process 21819 attached - interrupt to quit
...
st_mode=S_IFREG|0644, st_size=72, ...}) = 0
open("/var/www/html/test_session.php", O_RDONLY) = 17
fstat64(17, {st_mode=S_IFREG|0644, st_size=72, ...}) = 0
lseek(17, 0, SEEK_CUR) = 0
read(17, "<p;?php\n//@file test_session.php\ns"..., 8192) = 72
read(17, "", 8192) = 0
read(17, "", 8192) = 0
close(17) = 0
gettimeofday({1270906664, 11602}, NULL) = 0
open("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5", O_RDWR|O_CREAT, 0600) = 17
flock(17, LOCK_EX) = 0
fcntl64(17, F_SETFD, FD_CLOEXEC) = 0
fstat64(17, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
time(NULL) = 1270906664
open("/var/www/html/test_session.php", O_RDONLY) = 18
fstat64(18, {st_mode=S_IFREG|0644, st_size=72, ...}) = 0
lseek(18, 0, SEEK_CUR) = 0
close(18) = 0
chdir("/var/lib/php/session") = 0
pwrite64(17, "", 0, 0) = 0
close(17) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
writev(16, [{"HTTP/1.1 200 OK\r\nDate: Sat, 10 A"..., 385}], 1) = 385
write(12, "192.168.0.98 - - [10/Apr/2010:21"..., 207) = 207
shutdown(16, 1 /* send */) = 0
epoll_wait(15,
藍色加粗,經過系統內核函數open調用打開session文件,這是由session_start()產生的調用,
注意這裏並無產生讀文件操做。紅色部分,將一個空字符串寫入session文件。
因而可知session初始化在頁面生命週期開始之時,手工調用session_start能夠初始化session文件,
而在頁面生命週期結束之時,會自動地註銷session,結束當前session生命週期,
同時在此週期產生的session數據寫回session文件,咱們把這種方式結束的session,稱爲session默認結束。
任務2: 觀察session_register()查看它是否會產生磁盤操做,仍是隻操做$_SESSION。
<?php
//@file test_session_2.php
session_start();
$pg_uuid = 'ac606826-9620-490b-b850-ea9dbce6cfd5';
session_register('pg_uuid'); //註冊全局變量pg_uuid到session,但值爲NULL,隻影響$_SESSION
var_dump($_SESSION);
fopen(__FILE__, "r");
[root@localhost ~]# strace -p `cat /var/run/httpd.pid`
Process 21819 attached - interrupt to quit
...
open("/var/www/html/test_session_2.php", O_RDONLY) = 17
fstat64(17, {st_mode=S_IFREG|0644, st_size=148, ...}) = 0
lseek(17, 0, SEEK_CUR) = 0
read(17, "<?php\nsession_start();\n$pg_uuid "..., 8192) = 148
read(17, "", 8192) = 0
read(17, "", 8192) = 0
close(17) = 0
open("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5", O_RDWR|O_CREAT, 0600) = 17
flock(17, LOCK_EX) = 0
fcntl64(17, F_SETFD, FD_CLOEXEC) = 0
fstat64(17, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
time(NULL) = 1270907613
open("/var/www/html/test_session_2.php", O_RDONLY) = 18
fstat64(18, {st_mode=S_IFREG|0644, st_size=148, ...}) = 0
lseek(18, 0, SEEK_CUR) = 0
close(18) = 0
chdir("/var/lib/php/session") = 0
pwrite64(17, "pg_uuid|N;", 10, 0) = 10
close(17) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
writev(16, [{"HTTP/1.1 200 OK\r\nDate: Sat, 10 A"..., 328}, {"array(1) {\n [\"pg_uuid\"]=>\n NUL"..., 36}], 2) = 364
write(12, "192.168.0.98 - - [10/Apr/2010:21"..., 210) = 210
shutdown(16, 1 /* send */) = 0
epoll_wait(15,
經過上面的觀察,藍色部分仍是由session初始化(session_start)產生,注意這裏依然沒讀文件操做,這是由於session文件爲空。紅色部分,依然是默認結束session產生的文件寫操做(pwrite)。由此,咱們能夠知道session_register()不會對session文件操做,即不會把$_SESSION中的數據寫回session文件,它沒有產生任何IO操做。而只在session生命週期是影響當前$_SESSION變量,即$_SESSION[‘pg_uuid’] = NULL。因此,推薦使用$_SESSION[‘pg_uuid’] = $pg_uuid;
任務3: 觀察session_destroy()與session_unset()的區別
<?php
session_start();
echo "<br/>---1--<br/>";
$pg_uid = 1;
//$_SESSION['pg_uid']; //該行會報一個Notice消息,即沒有初始化該變量
$_SESSION['pg_name'] = 'boys'; //填入到$_SESSION變量,但不當即寫入session文件,值爲boys
$pg_sex = 1;
$pg_theme = 'default';
session_register('pg_sex'); //填入到$_SESSION變量,但不當即寫入session文件,值爲NULL
session_register('pg_theme'); //填入到$_SESSION變量,但不當即寫入session文件,值爲NULL
var_dump($_SESSION);
//--
echo "<br/>---2--<br/>";
unset($_SESSION['pg_theme']); //從$_SESSION清除該元素,不當即同步到session文件
unset($_SESSION['pg_name']); //從$_SESSION清除該元素,不當即同步到session文件
session_unregister('pg_sex'); //從$_SESSION清除該元素,不當即同步到session文件
session_unregister('pg_uid'); //從$_SESSION清除該元素,不當即同步到session文件
var_dump($_SESSION);
echo "<br/>---3--<br/>";
$_SESSION['pg_members'] = 5; //填入$_SESSION數組,但不當即同步到session文件,值爲5
$pg_boy = 6;
session_register('pg_boy'); //填入$_SESSION數組,但不當即同步到session文件,值爲NULL
session_unset($_SESSION); //清空$_SESSION
var_dump($_SESSION);
echo "<br/>---4--<br/>";
$_SESSION['pg_boss'] = 3; //填入$_SESSION數組,但不當即同步到session文件,值爲3
$pg_girls = 6;
session_register('pg_girls'); //填入$_session數組,但不當即同步到session文件,值爲NULL
session_destroy(); //註銷session_destroy
var_dump($_SESSION);
echo "<br/>---5---<br/>";
session_unregister('pg_boss'); //pg_boss不會被清除,還爲NULL
session_unset(); //不會清空$_SESSION數組,由於session已被session_destroy註銷
var_dump($_SESSION);
fopen(__FILE__, "r");
//@這裏是頁面析構的時候-- 本應該將$_SESSION數據同步到session文件, 真的嗎???
//@事實,沒有發生任何IO操做,即沒有將$_SESSION數據回寫,怎麼回事???
//@由於被session_destroy()消毀了session...
程序輸出:
---1--
array(3) { ["pg_name"]=> string(4) "boys" ["pg_sex"]=> NULL ["pg_theme"]=> NULL }
---2--
array(0) { }
---3--
array(0) { }
---4--
array(2) { ["pg_boss"]=> int(3) ["pg_girls"]=> NULL }
---5---
array(2) { ["pg_boss"]=> int(3) ["pg_girls"]=> NULL }
[root@localhost ~]# strace -p `cat /var/run/httpd.pid`
Process 21819 attached - interrupt to quit
...
open("/var/www/html/test_session_3.php", O_RDONLY) = 17
fstat64(17, {st_mode=S_IFREG|0644, st_size=706, ...}) = 0
lseek(17, 0, SEEK_CUR) = 0
read(17, "<?php\nsession_start();\necho \"<br"..., 8192) = 706
read(17, "", 8192) = 0
read(17, "", 8192) = 0
close(17) = 0
open("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5", O_RDWR|O_CREAT, 0600) = 17
flock(17, LOCK_EX) = 0
fcntl64(17, F_SETFD, FD_CLOEXEC) = 0
fstat64(17, {st_mode=S_IFREG|0600, st_size=10, ...}) = 0
pread64(17, "pg_uuid|N;", 10, 0) = 10
close(17) = 0
unlink("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5") = 0
time(NULL) = 1270910665
open("/var/www/html/test_session_3.php", O_RDONLY) = 17
fstat64(17, {st_mode=S_IFREG|0644, st_size=706, ...}) = 0
lseek(17, 0, SEEK_CUR) = 0
close(17) = 0
chdir("/var/lib/php/session") = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
...
write(12, "192.168.0.98 - - [10/Apr/2010:22"..., 211) = 211
shutdown(16, 1 /* send */) = 0
藍色部分是咱們熟悉的session初始化的時候產生的open系統內核調用。綠色部分,是一個IO讀操做,由於上一次訪問頁面的時候,產生了session數據,因此這一次會將上次的session填入$_SESSION中。紅色部分,能夠看出,這裏調用unlink刪除session文件,並且後面(頁面生命週期結束時),一直沒有看到前兩例看到的任何與session文件有關的IO寫操做,即沒有將$_SESSION中的數據寫回session文件。咱們也沒有在session.save_path找到相應的session文件
[root@localhost html]# ls /var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5 ls: /var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5: No such file or directory
注意: 雖然刪除了session文件,但用戶再次訪問web的時候,並不會給用戶從新分配一個新的session id,而是依然用該session id,而且會從新建立文件名相同的session文件,即sess_SESSION-ID
任務4: 測試並觀察session_regenerate_id行爲,以及$_SESSION的變化
<?php
session_start();
$_SESSION['pfid'] = 123;
var_dump($_SESSION);
session_regenerate_id();
var_dump($_SESSION);
fopen(__FILE__, "r");
[root@localhost ~]# strace -p `cat /var/run/httpd.pid`
Process 22641 attached - interrupt to quit
...
open("/var/www/html/test_session_4.php", O_RDONLY) = 17
fstat64(17, {st_mode=S_IFREG|0644, st_size=141, ...}) = 0
lseek(17, 0, SEEK_CUR) = 0
read(17, "<?php\nsession_start();\n$_SESSION"..., 8192) = 141
read(17, "", 8192) = 0
read(17, "", 8192) = 0
close(17) = 0
open("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5", O_RDWR|O_CREAT, 0600) = 17
flock(17, LOCK_EX) = 0
fcntl64(17, F_SETFD, FD_CLOEXEC) = 0
fstat64(17, {st_mode=S_IFREG|0600, st_size=11, ...}) = 0
pread64(17, "pfid|i:123;", 11, 0) = 11
gettimeofday({1270915896, 122016}, NULL) = 0
time(NULL) = 1270915896
open("/var/www/html/test_session_4.php", O_RDONLY) = 18
fstat64(18, {st_mode=S_IFREG|0644, st_size=141, ...}) = 0
lseek(18, 0, SEEK_CUR) = 0
close(18) = 0
chdir("/var/lib/php/session") = 0
close(17) = 0
open("/var/lib/php/session/sess_qoa6knu9fg77un8le99o1vk1c7", O_RDWR|O_CREAT, 0600) = 17
flock(17, LOCK_EX) = 0
fcntl64(17, F_SETFD, FD_CLOEXEC) = 0
pwrite64(17, "pfid|i:123;", 11, 0) = 11
close(17) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
writev(16, [{"HTTP/1.1 200 OK\r\nDate: Sat, 10 A"..., 386}, {"array(1) {\n [\"pfid\"]=>\n int(12"..., 75}], 2) = 461
write(12, "192.168.0.98 - - [11/Apr/2010:00"..., 210) = 210
shutdown(16, 1 /* send */) = 0
觀察測試結果,藍色部分照舊是session初始化的時候產生的系統內核open調用,接着綠色部分是一個IO讀操做,即讀取session文件中的數據,由第一個var_dump($_SESSION)輸出。隨後,往session加入新的一條已定義了的session記錄,而且經過session_commit()將記錄寫回去。紅色部分就是由session_commit產生的一次IO寫操做。以後,session_unset()並無生效,同時,咱們也沒有在頁面生命週期結束的時候看到任何與session文件有關的IO寫操做。這也正說明了,session_commit()調用的當下,就會將session數據寫回session文件,而且會像session_destroy同樣註銷session,但與session_destroy不一樣的時,session_commit不會刪除session文件,並且會將當前的session數據寫回session文件。咱們能夠查看,調用session_commit以後,session文件仍是依然存在的
[root@localhost html]# ls -lt /var/lib/php/session
-rw------- 1 apache apache 31 Apr 11 03:18 sess_qoa6knu9fg77un8le99o1vk1c7
-rw------- 1 apache apache 11 Apr 11 00:08 sess_4j38nv7l1fq1bj6n80l6g9cum5 …
總結:
1, 用戶註銷web應用系統,最好的調用方式依次是 session_unset(); session_destroy(); unset($_SESSION);
<?php
function user_sigout() {
global $user;
sys_event_register('user_sigout', $user);
session_unset(); //清空session
session_destroy(); //刪除session文件
if (isset($_SESSION)) {
unset($_SESSIONI); //註銷$_SESSION
}
return TRUE;
}
2, 儘可能將鍵與值填入$_SESSION,而不推薦使用session_register()。一樣,儘可能使用unset($_SESSION[‘var’]),而不使用session_unregister()。
3, 對於可能產生大量session的WEB應用,推薦使用的session.save_path的格式是session.save_path=」N:/path」。注意:這些目錄須要手工建立,而且有httpd守護進程屬主寫權限。這樣作能夠得到更好的性能
4, 若是調用了session_regenerate_id()給用戶分配了新的session id。該函數並不會主動刪除舊的session文件,須要定時清理舊的session文件,這樣更優化。
5, 儘可能不要使用session_commit()提交sessioin數據,由於它同時會結束當前session,PHP默認會在頁面生命週期的時候提交session數據到session文件
Session ID傳遞
session終究是由於管理用戶狀態信息才存在的。咱們曾探討過session id的意義:每一個來訪問用戶都會被分配一個惟一的session id,用於區分其它用戶的session數據。換句話說,session id是用戶代表身份的一種標識,就像入場券同樣。用戶一旦從被分配了session id以後的每次訪問(http請求)都會攜帶這個session id給服務端,用於加載該用戶的session數據。那麼,經過什麼方式傳給服務端?這是咱們這節探討的內容。
用戶端與服務端的web通訊協議是http。而PHP經過http取得用戶數據慣用的三種方法分別是:POST方法、GET方法還有Cookie。而PHP默認傳遞方法正是Cookie,也是最佳方法。只有在客戶端不支持Cookie的時候(瀏覽器禁用了Cookie功能)纔會經過GET方法來傳遞session_id,即經過在URL的query_string部分傳遞session id。
肯定了傳遞方法,咱們還有必要清楚一下session id的傳遞過程。用戶經過瀏覽器訪問網頁,將URL輸入地址欄回車,瀏覽器發出請求,在調用sockect send以前瀏覽器引擎會搜索有效的Cookies記錄封裝在http請求頭的Cookie字段,一同發送出去。服務端器接收到請求後,交給PHP處理。這時,session初始化函數若是在$_COOKIE中沒有找到以session_name()做爲鍵值存儲的生素(值爲session id),則會覺得用戶是第一次訪問web。做爲第一次訪問的用戶,session初始化函數總會隨機生成一個session_id而且經過setcookie()函數調用將新生成的session_id以」sesseson_name = session_id」的格式填入http響應頭Set-Cookie字段,發送給客戶端(這樣接下來的請求,http請求頭Cookie字段都會攜帶該Cookie記錄給web服務器)。若是初始化函數發現用戶端Cookies中已定義了存在$_COOKIE[‘sess_name’],則會加載與$_COOKIE[‘sess_name’]相對應的session文件($_COOKIE[‘sess_name’]就是session ID)。若是用戶Cookie記錄過時,則會被瀏覽器刪除。以後的下一次請求,服務器會覺得用戶又是第一次訪問,如此循環。
讓咱們經過測與來驗證以上的陳述
<?php
//p1.php
session_start();
經過截取到的http數據包以下
#
T 192.168.0.98:2290 -< 192.168.0.8:8080 [AP]
GET /a.php HTTP/1.1..Host: 192.168.0.8:8080..Connection: keep-alive..User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1
; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2..Accept: application/xml,application/xhtml+
xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5..Accept-Encoding: gzip,deflate,sdch..Accept-Language: zh-CN,zh;
q=0.8..Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3….
##
T 192.168.0.8:8080 -< 192.168.0.98:2290 [AP]
HTTP/1.1 200 OK..Date: Mon, 12 Apr 2010 08:25:11 GMT..Server: Apache/2.2.3 (CentOS)..X-Powered-By: PHP/5.1.6..Set-Cookie
: PHPSESSID=bk7655dqrm5m884c9nitfi7j00; path=/..Expires: Thu, 19 Nov 1981 08:52:00 GMT..Cache-Control: no-store, no-cach
e, must-revalidate, post-check=0, pre-check=0..Pragma: no-cache..Content-Length: 0..Connection: close..Content-Type: tex
t/html; charset=UTF-8….
第一次訪問/a.php的時候,請求包裏面沒有設置任何Cookie,因此這裏的Cookie字段爲空。固然服務器php也就得不到的$_COOKIE[‘PHPSESSID’](即session id爲空)。如此,服務器會覺得用戶是第一次訪問web。因此session初始化的時候,會給用戶分配一個惟一的session_id而且以Cookie的方法傳回給了用戶端。
咱們再來觀察第二次請求與響應,會有哪些變化:
#
T 192.168.0.98:2314 -< 192.168.0.8:8080 [AP]
GET /a.php HTTP/1.1..Host: 192.168.0.8:8080..Connection: keep-alive..User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1
; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2..Cache-Control: max-age=0..Accept: applicat
ion/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5..Accept-Encoding: gzip,deflate,sdch..
Accept-Language: zh-CN,zh;q=0.8..Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3..Cookie: PHPSESSID=bk7655dqrm5m884c9nitfi7j00..
..
##
T 192.168.0.8:8080 -< 192.168.0.98:2314 [AP]
HTTP/1.1 200 OK..Date: Mon, 12 Apr 2010 08:32:13 GMT..Server: Apache/2.2.3 (CentOS)..X-Powered-By: PHP/5.1.6..Expires: T
hu, 19 Nov 1981 08:52:00 GMT..Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0..Pragma: no-
cache..Content-Length: 0..Connection: close..Content-Type: text/html; charset=UTF-8
首先,咱們觀察http請求,加紅色部分是第一次http請求頭沒有出現的內容。咱們能夠看到,該Cookie正是第一次訪問,服務端經過Set-Cookie要求瀏覽器設置的Cookie。它們是同樣的,即session_id爲bk7655dqrm5m884c9nitfi7j00。而後,咱們再觀察此次的http響應,明顯沒有再要求用戶端設置鍵爲session_name()的Cookie了。
咱們再來測試僞造一個session_id發送給服務,觀察服務端響應。咱們寫一個測試腳本,以下:
<?php
$host = '192.168.0.8';
$port = 8080;
$path = '/p1.php';
$sid = "PHPSESSID=dk7655dqrm5m884c9nitfi7j00";
$fp = fsockopen($host, $port, $error_no, $error_desc, 30);
if ($fp) {
fputs($fp, "GET {$path} HTTTP/1.1\r\n");
fputs($fp, "Host: {$host}\r\n");
fputs($fp, "Cookie: {$sid}\r\n");
fputs($fp, "Connection: close\r\n\r\n");
while (!feof($fp)) {
$d .= fgets($fp, 4096);
}
fclose();
echo $d;
}
抓到的http請求、響應數據包以下:
#
T 192.168.0.98:2400 -< 192.168.0.8:8080 [AP]
GET /p1.php HTTTP/1.1..
Host: 192.168.0.8..Cookie: PHPSESSID=dk7655dqrm5m884c9nitfi7j00..Connection: close….
##
T 192.168.0.8:8080 -< 192.168.0.98:2400 [AP]
HTTP/1.1 200 OK..Date: Mon, 12 Apr 2010 09:03:09 GMT..Server: Apache/2.2.3 (CentOS)..X-Powered-By: PHP/5.1.6..Expires: T
hu, 19 Nov 1981 08:52:00 GMT..Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0..Pragma: no-
cache..Content-Length: 11..Connection: close..Content-Type: text/html; charset=UTF-8….hello world
上面的session_id是用戶端僞造的一個值,它並不實際存在。收到這樣的請求,服務端並無檢查,而是以這個session_id建立了相應的session文件。而且,從httpd響應頭部信息來看,並沒給用戶端分配session id(沒有Set-Cookie)。由此,咱們能夠推斷:只要http請求頭部包含了以session_name()做爲鍵值的Cookie,那麼服務端就不認爲用戶是第一次訪問web,亦不會給客戶端分配session_id。不然,分配新的session_id,並經過Set-Cookie要求瀏覽器建立該Cookie.
咱們再來觀察一下,經過session_regenerate_id()函數給用戶分配一個新的session_id的狀況
<?php
//@file: p2.php
session_start();
session_regenerate_id();
抓取到的http數據包以下
####
T 192.168.0.98:2763 -< 192.168.0.8:8080 [AP]
GET /p2.php HTTP/1.1..Host: 192.168.0.8:8080..Connection: keep-alive..User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.
1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2..Cache-Control: max-age=0..Accept: applica
tion/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5..Accept-Encoding: gzip,deflate,sdch.
.Accept-Language: zh-CN,zh;q=0.8..Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3..Cookie: PHPSESSID=bk7655dqrm5m884c9nitfi7j00.
… ##
T 192.168.0.8:8080 -< 192.168.0.98:2763 [AP]
HTTP/1.1 200 OK..Date: Mon, 12 Apr 2010 11:39:10 GMT..Server: Apache/2.2.3 (CentOS)..X-Powered-By: PHP/5.1.6..Expires: T
hu, 19 Nov 1981 08:52:00 GMT..Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0..Pragma: no-
cache..Set-Cookie: PHPSESSID=f7q6jfdug4ekfsjhop6jftgna7; path=/..Content-Length: 0..Connection: close..Content-Type: tex
t/html; charset=UTF-8….
##
上面能夠觀察獲得,http請求頭Cookie部分帶了session id,而且這個session_id仍是用戶第一次訪問web時被分配獲得的。這一次,http響應頭跟第二次示例http響應有些不同,而是像第一次訪問那樣經過Set-Cookie去要求用戶端瀏覽器更新用戶的session id。這意味着:session_genrate_id()給用戶端從新生成的session id也是經過Cookie的方法傳遞。
1,User01和User02第一次去訪問/p1.php,分別被分配了一個session id。
2,User01和User02第二次訪問web,都會使用由/p1.php分配的session_id
3,User01由於訪問了/p2.php,腳本/p2.php中的session_regenerate_id()給用戶User01從新分配了一個新session_id,從用戶User01第4次訪問的session_id就能夠看得出來,與前面几几回的session_id不一樣了。
4,User02由於沒有訪問/p2.php,也就沒有被服務端從新分配session id,一下沿用着上一次分配的session_id與session id傳遞的有關的php.ini設置
1,session.use_cookie = 1
是否採用Cookie方法傳遞session id值。默認是1,表示啓用。
2,session.name = PHPSESSID
無論是Cookie傳遞sessioin_id,仍是GET方法傳遞session_id,都須要使用鍵值。他們的格式分別是Cookie: sess_name=session_id;和/path.php?sess_name=session_id,其中sess_name就是由這裏指定的
3,session.use_only_cookies = 0
表示只使用Cookie 的方法傳遞session id。咱們說過,傳遞cookie的方法,除了cookie,還有GET方法,GET方法是不安全的方法。在用戶端禁用了cookie的時候,會採用GET方法傳遞session_id,能夠經過這個設置盡用GET方法傳遞session_id。
4,session.cookie_lifetime = 0, session.cookie_path = / 以及session.cookie_domain =
若是使用Cookie方法傳遞session_id的話,這裏分別指定了cookie有效域、目錄和時間。分別對應setcookie()函數的形參$expire、$path和$domain。其中cookie_lifetime=0表示直到關閉瀏覽器才刪除Cookie。還可使用session_set_cookie_params()函數修改這些值。
5,session_name([string $name])
獲取或更新session_name。若是傳了name,則表示不使用默認的名稱PHPSESSID(由session.name)指定,不然獲取當前session_name。注意:若是設置session_name,則必須在session_start()以前調用才生效。
6,session_id([string $id])
與session_name()相似,但它是讀取或者設置session_id的方法。一樣,設置session_id的話,必須在session_start()以前調用纔有效。
7,session_set_cookie_params()和session_get_cookie_params()
經過session_set_cookie_params()能夠從新設定session.cookie_lifetime, session.cookie_path以及session.cookie_domain這三個php.ini設置。而session_get_cookie_params()則是獲取這些設定的值。
Session回收
經過上文幾節介紹,咱們知道session數據存放在服務端指定的session.save_path目錄中,同時會在用戶端存放一條Cookie用以記錄分配給用戶的session id。因此,session數據失效分服務端和客戶端,要刪除(回收)的對象也很清楚:
1,服務端:刪除過時的session文件,啓動PHP GC回收。
2,用戶端:使存儲了過時session_id的用戶端Cookie記錄過時。經過將Cookie的Expire設置爲負值,要求客戶端刪除Cookie。
服務端:刪除過時的session文件
PHP GC進程被啓動之後,則會掃描session.save_path,找出過時的session,並刪除該session文件。所謂,過時的session,是指操做系統當前時間與session文件最後訪問時間之差大於session.gc_maxlifetime的話,該session認爲是過時了。注意:有時候,你會發現,即使是文件過時了,有可能也沒有被及時地刪除掉。這是由於,每次session初始化的時候,並不會都啓動PHP GC進程的,啓動GC進程會大大下降php的運行效率。全部一個啓動機率,這個機率由php.ini設定session.gc_probability / session.gc_divisor二個設置決定,默認機率是1%(1/1000)。這意味着,每1000次用戶請求中,會啓動1次PHP GC回收session文件。好比,咱們下面看到的,過時的session文件依然存在:
# date;find /var/lib/php/session -type f -atime -1440 -print |xargs ls -lt
-rw------- 1 apache apache 0 Apr 12 20:01 /var/lib/php/session/sess_5tlaq5a8im3ob1bikn62motpv7
-rw------- 1 apache apache 0 Apr 12 19:39 /var/lib/php/session/sess_f7q6jfdug4ekfsjhop6jftgna7
-rw------- 1 apache apache 0 Apr 12 17:03 /var/lib/php/session/sess_dk7655dqrm5m884c9nitfi7j00
咱們能夠經過編輯設置,來驗證啓動php session的GC機制
<?php
//@file session_gc.php
ini_set("session.gc_probability", 100);
ini_set("session.gc_divisor", 100);
ini_set("session.gc_maxlifetime", 1440);
session_start();
[root@localhost ~]# strace -p `cat /var/run/httpd.pid`
open("/var/www/html/session_gc.php", O_RDONLY) = 17
fstat64(17, {st_mode=S_IFREG|0644, st_size=144, ...}) = 0
lseek(17, 0, SEEK_CUR) = 0
brk(0x8d35000) = 0x8d35000
read(17, "<?php\nini_set(\"session.gc_probab"..., 8192) = 144
read(17, "", 8192) = 0
read(17, "", 8192) = 0
close(17) = 0
open("/var/lib/php/session/sess_5tlaq5a8im3ob1bikn62motpv7", O_RDWR|O_CREAT, 0600) = 17
flock(17, LOCK_EX) = 0
fcntl64(17, F_SETFD, FD_CLOEXEC) = 0
fstat64(17, {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
open("/var/lib/php/session", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 18
fcntl64(18, F_SETFD, FD_CLOEXEC) = 0
time(NULL) = 1271125492
getdents(18, /* 13 entries */, 32768) = 516
stat64("/var/lib/php/session/sess_bk7655dqrm5m884c9nitfi7j00", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
unlink("/var/lib/php/session/sess_bk7655dqrm5m884c9nitfi7j00") = 0
stat64("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5", {st_mode=S_IFREG|0600, st_size=11, ...}) = 0
unlink("/var/lib/php/session/sess_4j38nv7l1fq1bj6n80l6g9cum5") = 0
stat64("/var/lib/php/session/sess_n660qmcl38solbmp7vkhafqg17", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
unlink("/var/lib/php/session/sess_n660qmcl38solbmp7vkhafqg17") = 0
stat64("/var/lib/php/session/sess_5tlaq5a8im3ob1bikn62motpv7", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
unlink("/var/lib/php/session/sess_5tlaq5a8im3ob1bikn62motpv7") = 0
stat64("/var/lib/php/session/sess_qoa6knu9fg77un8le99o1vk1c7", {st_mode=S_IFREG|0600, st_size=31, ...}) = 0
unlink("/var/lib/php/session/sess_qoa6knu9fg77un8le99o1vk1c7") = 0
stat64("/var/lib/php/session/sess_dutbc682k3h4cgho2sgugc0id4", {st_mode=S_IFREG|0600, st_size=23, ...}) = 0
unlink("/var/lib/php/session/sess_dutbc682k3h4cgho2sgugc0id4") = 0
stat64("/var/lib/php/session/sess_vp8lfqnskjvsiilcp1c4l484d3", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
unlink("/var/lib/php/session/sess_vp8lfqnskjvsiilcp1c4l484d3") = 0
stat64("/var/lib/php/session/sess_dk7655dqrm5m884c9nitfi7j00", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
unlink("/var/lib/php/session/sess_dk7655dqrm5m884c9nitfi7j00") = 0
stat64("/var/lib/php/session/sess_f7q6jfdug4ekfsjhop6jftgna7", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
unlink("/var/lib/php/session/sess_f7q6jfdug4ekfsjhop6jftgna7") = 0
getdents(18, /* 0 entries */, 32768) = 0
close(18) = 0
chdir("/var/lib/php/session") = 0
pwrite64(17, "", 0, 0) = 0
close(17) = 0
setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
writev(16, [{"HTTP/1.1 200 OK\r\nDate: Tue, 13 A"..., 327}], 1) = 327
write(12, "192.168.0.98 - - [13/Apr/2010:10"..., 205) = 205
shutdown(16, 1 /* send */) = 0
從上面藍色部分能夠看出,經過用stat64檢查session文件的狀態,若是發現過時了,則會經過調用系統內核函數ulink()刪除過時的session文件。可見,session初始化的時候會啓動GC, GC會掃描session.save_path中的全部session文件,查看他們狀態而且將過時的文件刪除。正由於如此,因此默認設置啓動的機率是1/1000。
客戶端:刪除過時session id的cookie記錄
若是用戶發現session已通過期,可是服務端的GC尚未啓動,服務端能夠手經過手工代碼setcookie的方式要求用戶端瀏覽器刪除鍵值爲session_name()的Cookie記錄。這樣,下回訪問的時候,瀏覽器覺得用戶是第一次訪問,而且從新給訪問用戶分配一個新的session_id。較好的作法相似這樣:
<?php
//@file session_destroy.php
session_start();
$sess_name = session_name();
$sess_id = session_id();
list(, $path, $domain, ,) = session_get_cookie_params();
if ($sess_name && isset($_COOKIE[$sess_name])) {
setcookie($sess_name, '', -1, $path, $domain);
if ($sess_id) {
session_destroy();
}
}
抓取的http數據包以下:
#
T 192.168.0.98:2638 -< 192.168.0.8:8080 [AP]
GET /session_destroy.php HTTP/1.1..Host: 192.168.0.8:8080..Connection: keep-alive..User-Agent: Mozilla/5.0 (Windows; U;
Windows NT 5.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2..Accept: application/xml,appl
ication/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5..Accept-Encoding: gzip,deflate,sdch..Accept-Langu
age: zh-CN,zh;q=0.8..Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3..Cookie: PHPSESSID=rjdtgvmueggplgqno66qlfket1....
##
T 192.168.0.8:8080 -< 192.168.0.98:2638 [AP]
HTTP/1.1 200 OK..Date: Tue, 13 Apr 2010 07:08:24 GMT..Server: Apache/2.2.3 (CentOS)..X-Powered-By: PHP/5.1.6..Expires: T
hu, 19 Nov 1981 08:52:00 GMT..Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0..Pragma: no-
cache..Set-Cookie: PHPSESSID=deleted; expires=Mon, 13-Apr-2009 07:08:23 GMT..Content-Length: 222..Connection: close..Con
tent-Type: text/html; charset=UTF-8....
##
T 192.168.0.98:2642 -< 192.168.0.8:8080 [AP]
GET /p1.php HTTP/1.1..Host: 192.168.0.8:8080..Connection: keep-alive..User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.
1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2..Accept: application/xml,application/xhtml
+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5..Accept-Encoding: gzip,deflate,sdch..Accept-Language: zh-CN,zh
;q=0.8..Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3....
##
T 192.168.0.8:8080 -< 192.168.0.98:2642 [AP]
HTTP/1.1 200 OK..Date: Tue, 13 Apr 2010 07:09:15 GMT..Server: Apache/2.2.3 (CentOS)..X-Powered-By: PHP/5.1.6..Set-Cookie
: PHPSESSID=lbmk3sc5a88e9cjuekr0aa9pc3; path=/..Expires: Thu, 19 Nov 1981 08:52:00 GMT..Cache-Control: no-store, no-cach
e, must-revalidate, post-check=0, pre-check=0..Pragma: no-cache..Content-Length: 11..Connection: close..Content-Type: te
xt/html; charset=UTF-8....hello world
上面觀察能夠知道,經過訪問/session_destroy.php,它要求客戶端將session_id的Cookie記錄刪除。而接下來訪問/p1.php的時候,http請求頭沒有經過Cookie將用戶的session id帶給服務器(由於剛被要求刪除)。而第二次請求/p1.php的http響應裏頭能夠看到,服務端又給用戶從新分配了一個新的session id,並且不會繼續使用過去的session數據。
與session回收相關的php.ini設置:
1, session.gc_probability和session.gc_divisor
由這二個函數決定了啓用GC的機率,默認是1/1000。也就是說,每一千次用戶請求中有一次會啓動GC回收session。啓動GC進程不宜過於頻繁。上面的例子,咱們能夠看到,它會每次檢查session.save_path目錄下每一個文件的狀態。這樣會下降php的執行效率。
2, session.gc_maxlifetime = 1440
設置session存活時間,單位是秒。每次GC啓動後, 會經過stat獲得session文件最後訪問的unix時間,經過如今時間減去文件最後訪問時間之間大於session.gc_maxlifetime,則會刪除該文件。
總結
1, PHP使用Cookie的方法傳遞session id。儘可能不要使用GET方法傳遞session id,由於這樣很不安全。
2, 能夠經過setcookie()的方法,將客戶端的session id的Cookie記錄刪除。
3, PHP GC進程由session初始化啓動。但不是每一次用戶請求都會被啓動,它的啓動機率默認是1/1000。過於頻繁訪問的網站,併發量大的網站,可減少PHP GC的啓動頻率。PHP GC回收session會下降php的執行效率。
4, 經過下面代碼,優化session回收
<?php
session_start();
if (isset($_SESSION['SESS_TIMEOUT'])) {
if ($_SERVER['REQUEST_TIME'] > $_SESSION['SESS_TIMEOUT']) {
setcookie(session_name(), session_id(), -1, '/');
session_unset();
session_destroy();
}
} else {
$_SESSION['SESS_TIMEOUT'] = $_SERVER['REQUEST_TIME'] + 3600;
}