接著,你必須修改 PHP 的 Session Save Handler,讓 PHP 懂得利用 Memcached Server 存放 PHP 的 Session 資料並能從 Memcached Server 取出 Session 的資料。PHP提供了 session_set_save_handler() 函式讓我們能輕易修改 Session Save Handler ,以下是我修改後的 PHP 程式碼,你必須在呼叫 session_start() 之前使用:
<? require_once"memcached-client.php"; define("SHARED_SESS_TIME",3600); // Timeout // Session Class by Fred class Shared_Session { function init() { ini_set("session.use_trans_sid",0); ini_set("session.gc_maxlifetime", SHARED_SESS_TIME); ini_set("session.use_cookies",1); ini_set("session.cookie_path","/"); ini_set("session.cookie_domain",".yourdomain.com.tw"); session_module_name("user"); session_set_save_handler( array("Shared_Session","open"), array("Shared_Session","close"), array("Shared_Session","read"), array("Shared_Session","write"), array("Shared_Session","destroy"), array("Shared_Session","gc") ); } function open($save_path,$session_name) { returntrue; } function close() { returntrue; } function read($sesskey) { global$memcache; return$memcache->get($sesskey); } function write($sesskey,$data) { global$memcache; $memcache->set($sesskey,$data, SHARED_SESS_TIME); returntrue; } function destroy($sesskey) { global$memcache; $memcache->delete($sesskey); $memcache->flush_all(); returntrue; } function gc($maxlifetime=null) { returntrue; } } $GLOBALS["memcache"] =new memcached(array( 'servers'=>array('127.0.0.1:11212'), 'debug'=>false, 'compress_threshold'=>10240, 'persistant'=>true)); Shared_Session::init(); ?>
其中粗字體的部分,是要特別修改的地方:
3600 是 Session 的生命周期﹝以秒為單位﹞,這應該不用再做太多解釋。
.yourdomain.com.tw 是你的網域名稱:想像一個情況若是 Loadbalance 在用戶第一次連線分配用戶到A伺服器,第二次連線分配給同一用戶到B伺服器,會導致 B 伺服器無法透過 cookies 取得 A 伺服器分配給用戶的 session_id,因為 cookies 無法跨網域存取,解決方法是必須修改 cookies 的網域設定,讓 www1.yourdomain.com.tw、www2.yourdomain.com.tw、www3.yourdomain.com.tw...等等,都可以共同存取同一個 cookies ,以取得同一個 session_id,故此時你必須設定成為『.yourdomain.com.tw』。
192.168.1.1 這是你的 Memcached Server 的 IP 位置,這裡值得提的是 add_server() 方法,你可以有多行設定許多 IP 做 Loadbalance 負載分配,前面也講到 Memcached 是純粹使用記憶體,若其中一台機器記憶體滿了,本方法可以從中找到另一台可用的機器使用。故你可以建立一個 Memcached 的叢集來處理 Session。
<?php // // +---------------------------------------------------------------------------+ // | memcached client, PHP | // +---------------------------------------------------------------------------+ // | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net> | // | All rights reserved. | // | | // | Redistribution and use in source and binary forms, with or without | // | modification, are permitted provided that the following conditions | // | are met: | // | | // | 1. Redistributions of source code must retain the above copyright | // | notice, this list of conditions and the following disclaimer. | // | 2. Redistributions in binary form must reproduce the above copyright | // | notice, this list of conditions and the following disclaimer in the | // | documentation and/or other materials provided with the distribution. | // | | // | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR | // | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | // | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. | // | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, | // | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | // | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | // | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | // +---------------------------------------------------------------------------+ // | Author: Ryan T. Dean <rtdean@cytherianage.net> | // | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. | // | Permission granted by Brad Fitzpatrick for relicense of ported Perl | // | client logic under 2-clause BSD license. | // +---------------------------------------------------------------------------+ // // $TCAnet$ // /** * This is the PHP client for memcached - a distributed memory cache daemon. * More information is available at http://www.danga.com/memcached/ * * Usage example: * * require_once 'memcached.php'; * * $mc = new memcached(array( * 'servers' => array('127.0.0.1:10000', * array('192.0.0.1:10010', 2), * '127.0.0.1:10020'), * 'debug' => false, * 'compress_threshold' => 10240, * 'persistant' => true)); * * $mc->add('key', array('some', 'array')); * $mc->replace('key', 'some random string'); * $val = $mc->get('key'); * * @author Ryan T. Dean <rtdean@cytherianage.net> * @package memcached-client * @version 0.1.2 */ // {{{ requirements // }}} // {{{ constants // {{{ flags /** * Flag: indicates data is serialized */ define("MEMCACHE_SERIALIZED",1<<0); /** * Flag: indicates data is compressed */ define("MEMCACHE_COMPRESSED",1<<1); // }}} /** * Minimum savings to store data compressed */ define("COMPRESSION_SAVINGS",0.20); // }}} // {{{ class memcached /** * memcached client class implemented using (p)fsockopen() * * @author Ryan T. Dean <rtdean@cytherianage.net> * @package memcached-client */ class memcached { // {{{ properties // {{{ public /** * Command statistics * * @var array * @access public */ var$stats; // }}} // {{{ private /** * Cached Sockets that are connected * * @var array * @access private */ var$_cache_sock; /** * Current debug status; 0 - none to 9 - profiling * * @var boolean * @access private */ var$_debug; /** * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again' * * @var array * @access private */ var$_host_dead; /** * Is compression available? * * @var boolean * @access private */ var$_have_zlib; /** * Do we want to use compression? * * @var boolean * @access private */ var$_compress_enable; /** * At how many bytes should we compress? * * @var interger * @access private */ var$_compress_threshold; /** * Are we using persistant links? * * @var boolean * @access private */ var$_persistant; /** * If only using one server; contains ip:port to connect to * * @var string * @access private */ var$_single_sock; /** * Array containing ip:port or array(ip:port, weight) * * @var array * @access private */ var$_servers; /** * Our bit buckets * * @var array * @access private */ var$_buckets; /** * Total # of bit buckets we have * * @var interger * @access private */ var$_bucketcount; /** * # of total servers we have * * @var interger * @access private */ var$_active; // }}} // }}} // {{{ methods // {{{ public functions // {{{ memcached() /** * Memcache initializer * * @param array $args Associative array of settings * * @return mixed * @access public */ function memcached ($args) { $this->set_servers($args['servers']); $this->_debug =$args['debug']; $this->stats =array(); $this->_compress_threshold =$args['compress_threshold']; $this->_persistant =isset($args['persistant']) ?$args['persistant'] :false; $this->_compress_enable =true; $this->_have_zlib =function_exists("gzcompress"); $this->_cache_sock =array(); $this->_host_dead =array(); } // }}} // {{{ add() /** * Adds a key/value to the memcache server if one isn't already set with * that key * * @param string $key Key to set with data * @param mixed $val Value to store * @param interger $exp (optional) Time to expire data at * * @return boolean * @access public */ function add ($key,$val,$exp=0) { return$this->_set('add',$key,$val,$exp); } // }}} // {{{ decr() /** * Decriment a value stored on the memcache server * * @param string $key Key to decriment * @param interger $amt (optional) Amount to decriment * * @return mixed FALSE on failure, value on success * @access public */ function decr ($key,$amt=1) { return$this->_incrdecr('decr',$key,$amt); } // }}} // {{{ delete() /** * Deletes a key from the server, optionally after $time * * @param string $key Key to delete * @param interger $time (optional) How long to wait before deleting * * @return boolean TRUE on success, FALSE on failure * @access public */ function delete ($key,$time=0) { if (!$this->_active) returnfalse; $sock=$this->get_sock($key); if (!is_resource($sock)) returnfalse; $key=is_array($key) ?$key[1] :$key; $this->stats['delete']++; $cmd="delete $key $time "; if(!fwrite($sock,$cmd,strlen($cmd))) { $this->_dead_sock($sock); returnfalse; } $res=trim(fgets($sock)); if ($this->_debug) printf("MemCache: delete %s (%s) ",$key,$res); if ($res=="DELETED") returntrue; returnfalse; } // }}} // {{{ disconnect_all() /** * Disconnects all connected sockets * * @access public */ function disconnect_all () { foreach ($this->_cache_sock as$sock) fclose($sock); $this->_cache_sock =array(); } // }}} // {{{ enable_compress() /** * Enable / Disable compression * * @param boolean $enable TRUE to enable, FALSE to disable * * @access public */ function enable_compress ($enable) { $this->_compress_enable =$enable; } // }}} // {{{ forget_dead_hosts() /** * Forget about all of the dead hosts * * @access public */ function forget_dead_hosts () { $this->_host_dead =array(); } // }}} // {{{ get() /** * Retrieves the value associated with the key from the memcache server * * @param string $key Key to retrieve * * @return mixed * @access public */ function get ($key) { if (!$this->_active) returnfalse; $sock=$this->get_sock($key); if (!is_resource($sock)) returnfalse; $this->stats['get']++; $cmd="get $key "; if (!fwrite($sock,$cmd,strlen($cmd))) { $this->_dead_sock($sock); returnfalse; } $val=array(); $this->_load_items($sock,$val); if ($this->_debug) foreach ($valas$k=>$v) printf("MemCache: sock %s got %s => %s ",$sock,$k,$v); return$val[$key]; } // }}} // {{{ get_multi() /** * Get multiple keys from the server(s) * * @param array $keys Keys to retrieve * * @return array * @access public */ function get_multi ($keys) { if (!$this->_active) returnfalse; $this->stats['get_multi']++; foreach ($keysas$key) { $sock=$this->get_sock($key); if (!is_resource($sock)) continue; $key=is_array($key) ?$key[1] :$key; if (!isset($sock_keys[$sock])) { $sock_keys[$sock] =array(); $socks[] =$sock; } $sock_keys[$sock][] =$key; } // Send out the requests foreach ($socksas$sock) { $cmd="get"; foreach ($sock_keys[$sock] as$key) { $cmd.="".$key; } $cmd.=""; if (fwrite($sock,$cmd,strlen($cmd))) { $gather[] =$sock; } else { $this->_dead_sock($sock); } } // Parse responses $val=array(); foreach ($gatheras$sock) { $this->_load_items($sock,$val); } if ($this->_debug) foreach ($valas$k=>$v) printf("MemCache: got %s => %s ",$k,$v); return$val; } // }}} // {{{ incr() /** * Increments $key (optionally) by $amt * * @param string $key Key to increment * @param interger $amt (optional) amount to increment * * @return interger New key value? * @access public */ function incr ($key,$amt=1) { return$this->_incrdecr('incr',$key,$amt); } // }}} // {{{ replace() /** * Overwrites an existing value for key; only works if key is already set * * @param string $key Key to set value as * @param mixed $value Value to store * @param interger $exp (optional) Experiation time * * @return boolean * @access public */ function replace ($key,$value,$exp=0) { return$this->_set('replace',$key,$value,$exp); } // }}} // {{{ run_command() /** * Passes through $cmd to the memcache server connected by $sock; returns * output as an array (null array if no output) * * NOTE: due to a possible bug in how PHP reads while using fgets(), each * line may not be terminated by a . More specifically, my testing * has shown that, on FreeBSD at least, each line is terminated only * with a . This is with the PHP flag auto_detect_line_endings set * to falase (the default). * * @param resource $sock Socket to send command on * @param string $cmd Command to run * * @return array Output array * @access public */ function run_command ($sock,$cmd) { if (!is_resource($sock)) returnarray(); if (!fwrite($sock,$cmd,strlen($cmd))) returnarray(); while (true) { $res=fgets($sock); $ret[] =$res; if (preg_match('/^END/',$res)) break; if (strlen($res) ==0) break; } return$ret; } // }}} // {{{ set() /** * Unconditionally sets a key to a given value in the memcache. Returns true * if set successfully. * * @param string $key Key to set value as * @param mixed $value Value to set * @param interger $exp (optional) Experiation time * * @return boolean TRUE on success * @access public */ function set ($key,$value,$exp=0) { return$this->_set('set',$key,$value,$exp); } // }}} // {{{ set_compress_threshold() /** * Sets the compression threshold * * @param interger $thresh Threshold to compress if larger than * * @access public */ function set_compress_threshold ($thresh) { $this->_compress_threshold =$thresh; } // }}} // {{{ set_debug() /** * Sets the debug flag * * @param boolean $dbg TRUE for debugging, FALSE otherwise * * @access public * * @see memcahced::memcached */ function set_debug ($dbg) { $this->_debug =$dbg; } // }}} // {{{ set_servers() /** * Sets the server list to distribute key gets and puts between * * @param array $list Array of servers to connect to * * @access public * * @see memcached::memcached() */ function set_servers ($list) { $this->_servers =$list; $this->_active =count($list); $this->_buckets =null; $this->_bucketcount =0; $this->_single_sock =null; if ($this->_active ==1) $this->_single_sock =$this->_servers[0]; } // }}} // }}} // {{{ private methods // {{{ _close_sock() /** * Close the specified socket * * @param string $sock Socket to close * * @access private */ function _close_sock ($sock) { $host=array_search($sock,$this->_cache_sock); fclose($this->_cache_sock[$host]); unset($this->_cache_sock[$host]); } // }}} // {{{ _connect_sock() /** * Connects $sock to $host, timing out after $timeout * * @param interger $sock Socket to connect * @param string $host Host:IP to connect to * @param float $timeout (optional) Timeout value, defaults to 0.25s * * @return boolean * @access private */ function _connect_sock (&$sock,$host,$timeout=0.25) { list ($ip,$port) =explode(":",$host); if ($this->_persistant ==1) { $sock= @pfsockopen($ip,$port,$errno,$errstr,$timeout); } else { $sock= @fsockopen($ip,$port,$errno,$errstr,$timeout); } if (!$sock) returnfalse; returntrue; } // }}} // {{{ _dead_sock() /** * Marks a host as dead until 30-40 seconds in the future * * @param string $sock Socket to mark as dead * * @access private */ function _dead_sock ($sock) { $host=array_search($sock,$this->_cache_sock); list ($ip,$port) =explode(":",$host); $this->_host_dead[$ip] =time() +30+intval(rand(0,10)); $this->_host_dead[$host] =$this->_host_dead[$ip]; unset($this->_cache_sock[$host]); } // }}} // {{{ get_sock() /** * get_sock * * @param string $key Key to retrieve value for; * * @return mixed resource on success, false on failure * @access private */ function get_sock ($key) { if (!$this->_active) returnfalse; if ($this->_single_sock !==null) return$this->sock_to_host($this->_single_sock); $hv=is_array($key) ?intval($key[0]) :$this->_hashfunc($key); if ($this->_buckets ===null) { foreach ($this->_servers as$v) { if (is_array($v)) { for ($i=0; $i<$v[1]; $i++) $bu[] =$v[0]; } else { $bu[] =$v; } } $this->_buckets =$bu; $this->_bucketcount =count($bu); } $realkey=is_array($key) ?$key[1] :$key; for ($tries=0; $tries<20; $tries++) { $host=$this->_buckets[$hv%$this->_bucketcount]; $sock=$this->sock_to_host($host); if (is_resource($sock)) return$sock; $hv+=$this->_hashfunc($tries.$realkey); } returnfalse; } // }}} // {{{ _hashfunc() /** * Creates a hash interger based on the $key * * @param string $key Key to hash * * @return interger Hash value * @access private */ function _hashfunc ($key) { $hash=0; for ($i=0; $i<strlen($key); $i++) { $hash=$hash*33+ord($key[$i]); } return$hash; } // }}} // {{{ _incrdecr() /** * Perform increment/decriment on $key * * @param string $cmd Command to perform * @param string $key Key to perform it on * @param interger $amt Amount to adjust * * @return interger New value of $key * @access private */ function _incrdecr ($cmd,$key,$amt=1) { if (!$this->_active) returnnull; $sock=$this->get_sock($key); if (!is_resource($sock)) returnnull; $key=is_array($key) ?$key[1] :$key; $this->stats[$cmd]++; if (!fwrite($sock,"$cmd $key $amt ")) return$this->_dead_sock($sock); stream_set_timeout($sock,1,0); $line=fgets($sock); if (!preg_match('/^(d+)/',$line,$match)) returnnull; return$match[1]; } // }}} // {{{ _load_items() /** * Load items into $ret from $sock * * @param resource $sock Socket to read from * @param array $ret Returned values * * @access private */ function _load_items ($sock,&$ret) { while (1) { $decl=fgets($sock); if ($decl=="END ") { returntrue; } elseif (preg_match('/^VALUE (S+) (d+) (d+) $/',$decl,$match)) { list($rkey,$flags,$len) =array($match[1],$match[2],$match[3]); $bneed=$len+2; $offset=0; while ($bneed>0) { $data=fread($sock,$bneed); $n=strlen($data); if ($n==0) break; $offset+=$n; $bneed-=$n; $ret[$rkey] .=$data; } if ($offset!=$len+2) { // Something is borked! if ($this->_debug) printf("Something is borked! key %s expecting %d got %d length ",$rkey,$len+2,$offset); unset($ret[$rkey]); $this->_close_sock($sock); returnfalse; } $ret[$rkey] =rtrim($ret[$rkey]); if ($this->_have_zlib &&$flags& MEMCACHE_COMPRESSED) $ret[$rkey] =gzuncompress($ret[$rkey]); if ($flags& MEMCACHE_SERIALIZED) $ret[$rkey] =unserialize($ret[$rkey]); } else { print("Error parsing memcached response "); return0; } } } // }}} // {{{ _set() /** * Performs the requested storage operation to the memcache server * * @param string $cmd Command to perform * @param string $key Key to act on * @param mixed $val What we need to store * @param interger $exp When it should expire * * @return boolean * @access private */ function _set ($cmd,$key,$val,$exp) { if (!$this->_active) returnfalse; $sock=$this->get_sock($key); if (!is_resource($sock)) returnfalse; $this->stats[$cmd]++; $flags=0; if (!is_scalar($val)) { $val=serialize($val); $flags|= MEMCACHE_SERIALIZED; if ($this->_debug) printf("client: serializing data as it is not scalar "); } $len=strlen($val); if ($this->_have_zlib &&$this->_compress_enable && $this->_compress_threshold &&$len>=$this->_compress_threshold) { $c_val=gzcompress($val,9); $c_len=strlen($c_val); if ($c_len<$len*(1- COMPRESS_SAVINGS)) { if ($this->_debug) printf("client: compressing data; was %d bytes is now %d bytes ",$len,$c_len); $val=$c_val; $len=$c_len; $flags|= MEMCACHE_COMPRESSED; } } if (!fwrite($sock,"$cmd $key $flags $exp $len $val ")) return$this->_dead_sock($sock); $line=trim(fgets($sock)); if ($this->_debug) { if ($flags& MEMCACHE_COMPRESSED) $val='compressed data'; printf("MemCache: %s %s => %s (%s) ",$cmd,$key,$val,$line); } if ($line=="STORED") returntrue; returnfalse; } // }}} // {{{ sock_to_host() /** * Returns the socket for the host * * @param string $host Host:IP to get socket for * * @return mixed IO Stream or false * @access private */ function sock_to_host ($host) { if (isset($this->_cache_sock[$host])) return$this->_cache_sock[$host]; $now=time(); list ($ip,$port) =explode (":",$host); if (isset($this->_host_dead[$host]) &&$this->_host_dead[$host] >$now|| isset($this->_host_dead[$ip]) &&$this->_host_dead[$ip] >$now) returnnull; if (!$this->_connect_sock($sock,$host)) return$this->_dead_sock($host); // Do not buffer writes stream_set_write_buffer($sock,0); $this->_cache_sock[$host] =$sock; return$this->_cache_sock[$host]; } // }}} // }}} // }}} } // }}} ?>