web集群,跨域,跨服等,需要共享session。
想要共享 SESSION 数据,那就必须实现两个目标:
一个是各个服务器对同一个客户端产生的 SESSION ID 必须相同,并且可通过同一个 COOKIE 进行传递,也就是说各个服务器必须可以读取同一个名为 PHPSESSID 的 COOKIE;
另一个是 SESSION 数据的存储方式/位置必须保证各个服务器都能够访问到。
简单地说就是多服务器共享客户端的 SESSION ID,同时还必须共享服务器端的 SESSION 数据。
第一个目标的实现其实很简单,只需要对 COOKIE 的域(domain)进行特殊地设置即可,默认情况下,COOKIE 的域是当前服务器的域名/IP 地址,而域不同的话,各个服务器所设置的 COOKIE 是不能相互访问的,如 www.aaa.com 的服务器是不能读写 www.bbb.com 服务器设置的 COOKIE 的。
第二个目标的实现可以使用文件共享方式,如 NFS 方式,但设置、操作上有些复杂。
class MyRedis
{
private $host;
private $port;
private $password;
private $handle;
/**
*
* @param string $host
* @param int $port
* @param string $password
*/
public function __construct( $host = '127.0.0.1', $port = 6379, $password = NULL )
{
$this->host = $host;
$this->port = $port;
$this->password = $password;
}
/**
* Ping server
*
* @param int $server_index Index of the server
*/
public function ping( $server_index )
{
return $this->execute_command($this->get_connection( $server_index ), 'PING');
}
# === Scalar operations ===
/**
* @param string $key
* @param mixed $value
*/
public function set( $key, $value )
{
$value = $this->pack_value($value);
$cmd = array("SET {$key} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $this->get_error($response);
}
/**
* @param string $key
* @return mixed
*/
public function get( $key )
{
$response = $this->execute_command( "GET {$key}" );
if ( $this->get_error($response) )
{
return;
}
$length = (int)substr($response, 1);
if ( $length > 0 )
{
$value = $this->get_response();
return $this->unpack_value($value);
}
}
/**
* @param string $key
* @return mixed
*/
public function set_expire($key, $lifetime, $value)
{
if(!$this->set($key, $value)){
return $this->execute_command( "EXPIRE {$key} {$lifetime}" );
}
}
/**
* @param string $key
* @return mixed
*/
public function setex($key, $lifetime, $value)
{
$value = $this->pack_value($value);
$cmd = array("SETEX {$key} {$lifetime} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $this->get_error($response);
}
/**
* @param string $key
*/
public function del( $key )
{
return $this->execute_command( "DEL {$key}" );
}
/**
* @param string $key
* @return boolean
*/
public function exists( $key )
{
return $this->execute_command( "EXISTS {$key}" ) == ':1';
}
/**
* @param string $index
* @return boolean
*/
public function select( $index )
{
return $this->execute_command( "SELECT {$index}" );
}
/**
*
* @param string $key
* @param int $by
*/
public function inc( $key, $by = 1 )
{
$response = $this->execute_command( "INCRBY {$key} {$by}" );
return substr($response, 1);
}
/**
*
* @param string $key
* @param int $by
*/
public function dec( $key, $by = 1 )
{
$response = $this->execute_command( "DECRBY {$key} {$by}" );
return substr($response, 1);
}
# === List operations ===
public function prepend( $key, $value )
{
$value = $this->pack_value($value);
$cmd = array("LPUSH {$key} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $this->get_error($response);
}
public function append( $key, $value )
{
$value = $this->pack_value($value);
$cmd = array("RPUSH {$key} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $this->get_error($response);
}
public function get_list($key, $limit, $offset = 0)
{
$limit--;
$start = $offset;
$end = $start + $limit;
$response = $this->execute_command( "LRANGE {$key} {$start} {$end}" );
if ( $this->get_error($response) )
{
return;
}
$count = (int)substr($response, 1);
$list = array();
for ( $i = 0; $i < $count; $i++ )
{
$length = substr($this->get_response(), 1);
$value = $this->get_response();
$list[] = $this->unpack_value($value);
}
return $list;
}
public function get_filtered_list($key, $filters, $limit = 0, $offset = 0)
{
$start = 0;
$end = $this->get_list_length($key);
$response = $this->execute_command( "LRANGE {$key} {$start} {$end}" );
if ( $this->get_error($response) )
{
return;
}
$limit = !$limit ? $end : $limit + $offset;
$list = array();
for ( $i = 0; $i < $end; $i++ )
{
$length = substr($this->get_response(), 1);
$value = $this->get_response();
$value = $this->unpack_value( $value );
if ( ( $filters == array_intersect($value, $filters) ) && ( ++$added <= $limit ) )
{
$list[] = $value;
}
}
$list = array_slice($list, $offset);
return $list;
}
public function get_list_length($key)
{
$response = $this->execute_command( "LLEN {$key}" );
if ( $this->get_error($response) )
{
return;
}
return (int)substr($response, 1);
}
public function remove_from_list($key, $value, $count = 0)
{
$value = $this->pack_value($value);
$response = $this->execute_command( array("LREM {$key} {$count} " . strlen($value), $value) );
if ( $this->get_error($response) )
{
return;
}
return (int)substr($response, 1);
}
public function remove_by_filter($key, $filters)
{
$list = $this->get_filtered_list($key, $filters);
foreach ( $list as $item )
{
$this->remove_from_list($key, $item);
}
}
public function truncate_list($key, $limit, $offset = 0)
{
$limit--;
$start = $offset;
$end = $start + $limit;
$response = $this->execute_command( "LTRIM {$key} {$start} {$end}" );
if ( $this->get_error($response) )
{
return;
}
return true;
}
# === Set operations ===
public function add_member( $key, $value )
{
$value = $this->pack_value($value);
$cmd = array("SADD {$key} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $response == ':1';
}
public function remove_member( $key, $value )
{
$value = $this->pack_value($value);
$cmd = array("SREM {$key} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $response == ':1';
}
public function is_member( $key, $value )
{
$value = $this->pack_value($value);
$cmd = array("SISMEMBER {$key} " . strlen($value), $value);
$response = $this->execute_command( $cmd );
return $response == ':1';
}
public function get_members($key)
{
$response = $this->execute_command( "SMEMBERS {$key}" );
if ( $this->get_error($response) )
{
return;
}
$count = (int)substr($response, 1);
$list = array();
for ( $i = 0; $i < $count; $i++ )
{
$length = substr($this->get_response(), 1);
$value = $this->get_response();
$list[] = $this->unpack_value($value);
}
return $list;
}
public function get_members_count($key)
{
$response = $this->execute_command( "SCARD {$key}" );
if ( $this->get_error($response) )
{
return;
}
return (int)substr($response, 1);
}
# === Middle tier ===
/**
* Init connection
*/
private function get_connection()
{
$this->handle = fsockopen($this->host, $this->port, $errno, $errstr);
if ( !$this->handle )
{
return false;
}
// AUTH if password exists
if( $this->password ) {
$response = $this->execute_command('AUTH '.$this->password);
if ( $this->get_error($response) ){
return;
}
}
return $this->handle;
}
private function pack_value( $value )
{
if ( is_numeric($value) )
{
return $value;
}
else
{
return serialize($value);
}
}
private function unpack_value( $packed )
{
if ( is_numeric($packed) )
{
return $packed;
}
return unserialize($packed);
}
private function execute_command( $commands )
{
if(!$this->handle){
if(!$this->get_connection()) return false;
}
if ( is_array($commands) )
{
$commands = implode("\n", $commands);
}
$command = '';
$command .= $commands . "\r\n";
for ( $written = 0; $written < strlen($command); $written += $fwrite )
{
if ( !$fwrite = fwrite($this->handle, substr($command, $written)) )
{
return false;
}
}
return $this->get_response();
}
private function get_response()
{
if ( !$this->handle ) return false;
return trim(fgets($this->handle), "\r\n ");
}
private function get_error( $response )
{
if ( strpos($response, '-ERR') === 0 )
{
return substr($response, 5);
}
return false;
}
}
class Redissession
{
private $redis = NULL;
private $db = 0;
// stores settings
public $settings;
private static $_instance;
/**
* Constructor
*
* sets the settings to their new values or uses the default values
*
* @param (Array) $settings
* @return void
*/
public function __construct( $settings = array() )
{
// A neat way of doing setting initialization with default values
$this->settings = array_merge(array(
'session.name' => 'pp_zone_session',
'session.id' => '',
'session.expires' => 3600,
//'session.expires' => ini_get('session.gc_maxlifetime'),
'cookie.lifetime' => 0,
'cookie.path' => '/',
'cookie.domain' => '',
'cookie.secure' => false,
'cookie.httponly' => true,
'redis.host' => '127.0.0.1',
'redis.port' => 6379,
'redis.password' => ''
), $settings);
// if the setting for the expire is a string convert it to an int
if ( is_string($this->settings['session.expires']) ){
$this->settings['session.expires'] = intval($this->settings['session.expires']);
}
// cookies blah!
session_name($this->settings['session.name']);
session_set_cookie_params(
$this->settings['cookie.lifetime'],
$this->settings['cookie.path'],
$this->settings['cookie.domain'],
$this->settings['cookie.secure'],
$this->settings['cookie.httponly']
);
ini_set('session.save_handler', 'user');
session_set_save_handler(
array($this, "open"),
array($this, "close"),
array($this, "read"),
array($this, "write"),
array($this, "destroy"),
array($this, "gc")
);
session_start();
//session_regenerate_id(true);
}
public static function instance($settings = array())
{
if (self::$_instance == null) {
self::$_instance = new self($settings);
}
return self::$_instance;
}
public function open($save_path, $session_name)
{
if ($this->redis === NULL){
if(class_exists('Redis')){
$this->redis = new Redis();
$this->redis->connect($this->settings['redis.host'], $this->settings['redis.port']);
if($this->settings['redis.password']){
$this->redis->auth($this->settings['redis.password']);
}
} else {
$this->redis = new MyRedis($this->settings['redis.host'], $this->settings['redis.port'], $this->settings['redis.password']);
}
if ($this->db != 0) {
$this->redis->select($this->db);
}
}
return true;
}
public function close()
{
$this->redis = NULL;
return true;
}
public function read($session_id)
{
$key = "{$this->settings['session.name']}:{$session_id}";
$sess_data = $this->redis->get($key);
if ($sess_data === NULL){
return "";
}
return $sess_data;
}
public function write($session_id, $sess_data)
{
$key = "{$this->settings['session.name']}:{$session_id}";
$lifetime = $this->settings['session.expires'];
$this->redis->setex($key, $lifetime, $sess_data);
}
public function destroy($session_id)
{
$key = "{$this->settings['session.name']}:{$session_id}";
$this->redis->del($key);
}
public function gc($maxlifetime)
{
return true;
}
public function init()
{
}
/**
* Destructor
*
* do things
* @return void
*/
public function __destruct()
{
// the following prevents unexpected effects when using objects as save handlers
session_write_close();
}
/**
* Set session
*
* @access public
* @return
*/
public function set_session($newdata = array(), $newval = '')
{
//$this->init();
if (is_string($newdata)){
$newdata = array($newdata => $newval);
}
if (count($newdata) > 0){
foreach ($newdata as $key => $val){
$_SESSION[$key] = $val;
}
}
}
/**
* Get one session
*
* @access public
* @return string
*/
public function get_session($key)
{
return ( ! isset($_SESSION[$key])) ? FALSE : $_SESSION[$key];
}
/**
* Get all session
*
* @access public
* @return array
*/
public function get_all_session()
{
return (empty($_SESSION)) ? FALSE : $_SESSION;
}
/**
* Destroy the current session
*
* @access public
* @return void
*/
public function destroy_session()
{
$_SESSION = array();
/***删除sessin id.由于session默认是基于cookie的**/
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
// Kill session data
session_destroy();
}
}