Session 的原理及最佳實踐


Http協議是基於請求和響應的一種無狀態的協議,而通過session可以使得Http應用變得有狀態,即可以“記住”客戶端的信息。今天就來說說這個session和cookie。

Session 的原理

session是在服務器端保持用戶會話數據的一種方法,對應的cookie是在客戶端保持用戶數據。為了在客戶端(比如瀏覽器)可以跨頁面交流數據,Netscape將cookie引入瀏覽器。所以,cookie是保存在瀏覽器端的。

那么服務器是如何獲取這些瀏覽器的cookie的呢?是通過超全局變量$_COOKIE。就是說,服務端在接到請求(request)的時候通過這個$_COOKIE獲取客戶端的數據,而在響應(respsonse)的時候又通過$_COOKIE這個變量把數據傳給客戶端;對於客戶端,通過$_COOKIE將自己的數據發送給服務端,而又通過$_COOKIE獲取服務端方面想要傳遞的值。這是服務端和客戶端交流數據cookie的方式。這里說的客戶端,可以是瀏覽器,也可以是別的;相同的瀏覽器,不同的網頁發的請求都屬於統一客戶端;不同瀏覽器發的相同請求屬於不同的客戶端。

那么服務器是如何記住眾多用戶的會話數據呢?首先要將客戶端和服務端建立一一對應關系,使得每個客戶端都要一個唯一標示,這樣,服務器才能識別出來。建立唯一標識的方法是通過cookie或者通過GET方式在服務端建立session。php在使用session的時候,默認的會建立一個名叫"PHPSESSID"的cookie,值是唯一的(這個名字可以通過php.ini修改session.name值去改,這個唯一值的生成方式也可以修改),並在某個文件(session.save_path目錄)下保存一個文件(文件名唯一由剛剛生成的那個PHPSESSID cookie決定),然后發送給客戶端,客戶端在再次發送請求的時候,就會把這個名叫"PHPSESSID"的cookie帶過來,也就是$_COOKIE["PHPSESSID"],這個cookie的值不是session本身,而是一個session_id,一個和客戶端一一對應的id。

php會根據這個session_id去找對應的session文件。比如:sess_vv9lpgf0nmkurgvkba1vbvj915,這里的vv9lpgf0nmkurgvkba1vbvj915就是一個session_id。這個文件和客戶端也是一一對應的。這里面保存的值就是對應這個客戶端在服務端所保存的數據。

使用Session之前為什么必須先執行session_start()?

了解的原理之后,所謂的session其實就是客戶端一個session id服務器端一個session file。新建session之前執行session_start()是告訴服務器要種一個cookie($_COOKIE["PHPSESSID"])以及准備好對應的session文件,然后再發送出去,是誰發過來的請求,誰就會獲得這個唯一的標示。讀取session之前執行session_start()是告訴服務器,趕緊根據從客戶端發過來的$_COOKIE中去獲取$_COOKIE["PHPSESSID"]這個session_id,然后去某個路徑下獲取由session_id決定的唯一的文件(sess_vv9lpgf0nmkurgvkba1vbvj915),再把值讀取出來。

需要注意的是:

  • PHPSESSID這個名稱是可以配置的
  • ession保存的位置也是可以配置的通過php.ini中session.save_path設置,甚至,可以通過別的方式保存在數據庫或者緩存中
  • 在存session時限序列化,讀取的時候,先反序列化

這就是session的實現機制和原理。在機制不變的情況下,每個環節的實現方式幾乎都可以自定義。比如可以在這些方面實現自定義:

  • 如果客戶端不支持cookie,你也可以通過GET方式將session id發送到服務端
  • 如果你想改變唯一session_id的生成方式,你也可以選擇用uniqid
  • 你也可以改變session存放的路徑
  • 可以改變session文件的前綴,后綴
  • 甚至你可以把session存在數據庫,緩存當中

因此,session的使用方式靈活多樣,前提是你了解這個實現機制。大部分上面的DIY都可以在php.ini中通過配置實現。下面我們介紹下如何實現將session保存在除文件以外別的媒介中,畢竟,文件也只是一種而已。

自定義存儲

要想將session保存在別的媒介(比如緩存)需要先介紹一個接口:SessionHandlerInterface
可以通過實現這個接口,來自定義session的存貯方式,比如數據庫。當然,要實現一些基本的方法。

SessionHandlerInterface {
	/* 方法 */
	abstract public bool close ( void )
	abstract public bool destroy ( string $session_id )
	abstract public bool gc ( string $maxlifetime )
	abstract public bool open ( string $save_path , string $name )
	abstract public string read ( string $session_id )
	abstract public bool write ( string $session_id , string $session_data )
}

比如自己來重新寫一個:

<?php
class MySessionHandler implements SessionHandlerInterface
{
    private $savePath;

    public function open($savePath, $sessionName)
    {
        $this->savePath = $savePath;
        if (!is_dir($this->savePath)) {
            mkdir($this->savePath, 0777);
        }

        return true;
    }

    public function close()
    {
        return true;
    }

    public function read($id)
    {
        return (string)@file_get_contents("$this->savePath/sess_$id");
    }

    public function write($id, $data)
    {
        return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
    }

    public function destroy($id)
    {
        $file = "$this->savePath/sess_$id";
        if (file_exists($file)) {
            unlink($file);
        }

        return true;
    }

    public function gc($maxlifetime)
    {
        foreach (glob("$this->savePath/sess_*") as $file) {
            if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
                unlink($file);
            }
        }

        return true;
    }
}

$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();

如果是緩存(如memcached或者redis),就應該持有一個Cache或者Redis實例,並委托這個實例來實現open/close/read/write功能,整體不復雜,重在規范。

至於這個gc值得介紹下,為啥要有個gc?因為session總要有個過期或者失效時間,因此啟用一個garbage collection機制,也就是幾率性回收機制,。但是問題又來了,為啥不每次都去檢測下當前過期的session並除之而后快呢?這里涉及到一個性能問題,對session讀寫操作是頻繁的,如果每次都要在session操作時在所有的session當中檢測下哪些過期,哪些沒有過期,是不是太低效了點?尤其是session保存在文件中時尤其如此,一個session就是一個文件,客戶連接數一多,文件的數量將非常巨大。因此不用每次檢查,而是按照一定幾率去回收,這樣性能就不至於收到太大影響,在足夠多次數訪問后,絕大多數過期session也能得到清理。

那么,這又牽扯進另一個問題——什么是過期的session?

Session的過期問題

在php中設置session有很多方面,包括給session設置值或直接設置過期、失效和有效期。

服務端方面

在PHP中主要通過設置session.gc_maxlifetime來設定Session的生存周期:

ini_set('session.gc_maxlifetime', 3600); //設置時間 
ini_get('session.gc_maxlifetime');//得到ini中設定值 

超過這個gc_maxlifetime時間,session會被認為是garbage,有垃圾就有垃圾回收,但是垃圾回收的檢查卻不是每次都進行,而是按照一個幾率,分別是這兩個參數:

session.gc_probability = 1
session.gc_divisor = 1000

那么,每次請求,垃圾會被回收的概率就是 session.gc_probability / session.gc_divisor = 1/1000
所以,在服務端方面,session主要和session.gc_maxlifetime有關,次要和session.gc_probabilitysession.gc_divisor有關;

客戶端方面

主要和cookie的過期時間有關。php.ini中通過session.name = PHPSESSID來保存session的cookie名字默認為PHPSESSID(可以修改),那么可以設定這個cookie的過期時間來實現session的過期。

session.use_cookies = 1;

把這個的值設置為1,利用cookie來傳遞sessionid;

session.cookie_lifetime = 0

這個代表SessionID在客戶端Cookie儲存的時間,默認是0,代表瀏覽器一關閉sessionid就作廢。如果想使得PHPSESSID cookie永久有效,這個可以設為一個很大的值如999999999。另外也可以通過session_set_cookie_params()函數來設定PHPSESSID cookie的有效期

結論

不管是設置Session的為N秒過期還是永不過期,都要同時設定session.gc_maxlifetimesession.cookie_lifetime為同一值N;永不過期時可以設定為一個非常大的值(如9999999)

設置session過期的例子

有個設置session過期時間的例子可以參考下:


function start_session($expire = 0)
{
    if ($expire == 0) {
        $expire = ini_get('session.gc_maxlifetime');
    } else {
        ini_set('session.gc_maxlifetime', $expire);
    }
    if (empty($_COOKIE['PHPSESSID'])) {
        session_set_cookie_params($expire);
        session_start();
    } else {
        session_start();
        setcookie('PHPSESSID', session_id(), time() + $expire);
    }
}

使用方法:

//600秒以后過期
start_session(600);

最佳實踐

session_start() 會創建新會話或者重用現有會話,與之相反session_commit()/session_write_close()即保存當前session數據,並且關閉當前會話。前者是open+read,后者是write+close,由此可見session_commit的重要性了吧?

為了防止並發的寫session,任何時刻只能允許有一個PHP腳本在操作session,因此,一個腳本一旦session_start打開session,那么在此腳本終止或者調用session_write_close()之前,別的任何腳本都不能使用session。在默認情況下腳本結束時會自動寫入和關閉session,但是在腳本執行時間比較長的時候,此腳本就一致占據鎖使得別的腳本無法使用session,因此導致許多錯誤。
因此,最佳實踐是,任何session變量,數據的更改(如$_SESSION[xx] = xxx),都要及時使用session_commit()保存數據,關閉會話
下面介紹一個有可能在單點登錄中會用到的例子:

//從數據庫中獲取先前登錄的session_id
$pre_sid = $user_session['session_id'];
//當前登錄的session_id
$now_sid = session_id();

//先將當前會話置為$pre_sid
session_id($pre_sid);
//open+read之前的會話數據
session_start();
//unset()掉之前會話數據
session_unset();
//write+close之前的會話數據
session_commit();

//再將會話設置成當前的
session_id($now_sid);
//open+read當前的會話數據
session_start();
$_SESSION['username'] = $username;
//write+close當前的會話數據
session_commit();


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM