Compartir sesiones con MySql en diferentes servidores

Muchos sitios necesitan correr desde varios servidores, por ejemplo para aprovechar la localidad
de los usuarios con respecto al servidor, para balancear la carga, para compartir usuarios registrados o simplemente como resguardo
por si alguno de los sitios falla. Uno de los aspectos a contemplar para este menester es el
manejo de sesiones, aqui explicamos como mantener sesiones entre multiples servidores.




Descripcion del problema


Supongamos que tenemos 2 servidores que corren el mismo sitio por ejemplo mexico.foo.org y espana.foo.org,
los sitios pueden ser exactamente iguales o bien tener algunas diferencias entre si, lo que es claro es que
los usuarios de foo.org pueden comenzar por cualquiera de los sitios y moverse de un sitio a otro si les
parece conveniente


Sesiones


El uso de sesiones permite mantener presistencia entre las distintas paginas de un site en forma limpia
y eficiente, PHP4 permite manejar sesiones en forma sumamente sencilla, por ejemplo:













session_start();
$data="hola";
session_register("data");




Todas las paginas que usen sesiones deben empezar con session_start() o al menos hacer el session_start() antes de generar cualquier tipo de salida al browser. Una vez inicializada la sesion se utiliza session_register para guardar una variable en la sesion, notar que a session_register se le pasa elnombre de la variable y no la variable misma.
Una vez registrada la variable podemos tener otra pagina php distinta con:













session_start();
echo "$data";




Y la variable $data tomara el valor "hola" ya que habia sido guardada en la sesion con dicho nombre, se puede tambien
eliminar variables de la sesion y algunas otras cosas mas, pueden encontrar el manual en español sobre el uso de sesiones en esta direccion


Bien, podemos suponer ahora que sabemos usar sesiones, las sesiones son muy utiles para llevar datos pertenecientes
al usuario, como por ejemplo su userid u otros datos importantes, si tenemos dos servidores para el mismo sitio es logico
que la sesion se mantenga cuando el usuario pasa de un sitio a otro, pero esto no ocurre automaticamente


El problema


Internamente y sin modificaciones PHP4 guarda los datos de las sesiones en /tmp, periodicamente el mecanismo de "garbage collection" elimina las sesiones vencidas, la duracion maxima de una sesion sin actividad puede configurarse en php.ini (usualmente en /usr/local/lib). Como es evidente al cambiar de un servidor a otro el segundo no tiene acceso al directorio /tmp del primero por lo que la sesion se pierde, el session_start del nuevo servidor mas que abrir la sesion anterior debe crear una nueva y perdemos los datos que veniamos arrastrando en la sesion


Una primera alternativa que funciona es usar NFS para que los dos servidores puedan acceder a /tmp en un mismo lugar, sin embargo, esto no siempre es posible y muchas veces no queremos usar NFS unicamente por esta razon.


La solucion al problema


La solucion mas practica al problema de la persistencia de la sesion entre multiples servidores reside en usar una base de datos, por ejemplo MySQL, para almacenar los datos de la sesion. De esta forma ambos servidores pueden acceder a la misma base de datos (en un solo servidor) para consultar los datos de la sesion. Tantos servidores como deseemos pueden compartir la sesion siempre y cuando puedan acceder al mismo servidor MySQL.


Para que los datos de la sesion se guarden en MySQL es necesario modificar la forma en que PHP maneja sesiones


Cambiando los Session-Handlers


PHP4 dispone de un mecanismo que permite cambiar la forma en la cual se almacenan las sesiones por cualquier metodo que el usuario desee, para ello se usa la funcion session_set_save_handler. La funcion recibe 6 parametros que son los nombres a 6 funciones que se describen a continuacion:




  • Primera funcion:Abrir la sesion

  • Segunda funcion:Cerrar la sesion

  • Tercera funcion:Leer los datos de la sesion

  • Cuerta funcion:Escribir los datos de la sesion

  • Quinta funcion:Destruir una sesion

  • Sexta funcion:Eliminar sesiones vencidas


Por ejemplo:













session_set_save_handler ( 
'mysql_session_open',
'mysql_session_close',
'mysql_session_read',
'mysql_session_write',
'mysql_session_destroy',
'mysql_session_gc' );




En el ejemplo seteamos los nombres de las 6 funciones que seran usadas para manejar sesiones, veamos ahora que
prototipo debe tener y que hace cada funcion










































FuncionPrototipoDescripcion
mysql_session_openmysql_session_open($sess_path, $session_name);Esta funcion es llamada para inicializar la sesion, recibe el path en donde guardar las sesiones si se usan archivos y el bombre de la sesion, en MySQL ninguno de los parametros nos interesa
mysql_session_closemysql_session_close()Esta funcion se llama cuando la pagina termina y el handler necesita dejar cerrado el archivo o lo que sea que haya usado para mantener la sesion, no elimina la sesion solo deja todo listo para volver a abrirla con open
mysql_session_readmysql_session_read($key)Esta funcion se usa para leer los datos asociados a una sesion,las sesiones se identifican por una clave que se recibe como parametro, no es necesario preouparse por los datos que se recuperan ya que esto lo maneja php4 internamente, si el usuario registra 4 variables los handlers solo guardan y recuperan un par clave-valor, la forma en que las 4 variables se serializan y desserializan esta a cargo del PHP
mysql_session_writemysql_session_write($key,$val)Esta funcion se usa para almacenar el contenido de la sesion indicada por $key, $val tiene todas las variables de la sesion serializadas y listas para ser almacenadas.Est funcion se suele llamar al final de cada script antes de hacer el close.
mysql_session_destroymysql_session_destroy($key)Es invocada para eliminar los datos de una sesion.
mysql_session_gcmysql_session_gc($maxlifetime)Esta funcion debe eliminar todas las sesiones que tengan mas de $maxlifetime segundos de vida.


Preparando la base para guardar sesiones


Una vez que hemos analizado como funcionan los handlers es bueno analizar como vamos a guaradar la informacion en MySQL, si entendimos bien el mecanismo solo necesitamos guardar algo de tipo clave-valor por cada sesion y ademas un timestamp que permita saber hace cuanto tiempo que no se usa la sesion. Podemos crear una tabla de la forma:













DROP TABLE IF EXISTS PHPSessions;
CREATE TABLE PHPSessions (
SessionID CHAR(32) NOT NULL PRIMARY KEY,
LastActive INTEGER NOT NULL,
Data TEXT
);



SessionID es la clave de la sesion, LastActive la ultima fecha de uso de la sesion y Data los datos de la sesion, notemos que al estar definida como PRIMARY KEY la columna SessionID esta indexada lo cual agiliza mucho el manejo de sesiones cuando hay miles de usuarios en el sistema.


Handlers para Mysql


Analicemos ahora que debe hacer cada funcion para almacenar las sesiones en Mysql



































FuncionDescripcion
mysql_session_open($path,$name);En esta funcion tenemos que abrir la conexion a la base, usar mysql_pconnect con una conexion persistente es muy recomendable, ademas seleccionamos la base con mysql_select_db. Los dos parametros que recibe la funcion se ignoran.
mysql_session_close();Aqui no hace falta hacer nada ya que no es necesario cerrar la conexion a la base
mysql_session_read($key);Esto es simplemente un select a la base y devolvemos la columna data para la clave recibida. Ademas actualizar el timestamp del registro.
mysql_session_write($key,$data);Aquie hay que hacer un update si la sesion ya existe o bien un insert en caso contrario. Ademas actualizamos el timestamp del registro.
mysql_session_destroy($key);Un simple delete de la base
mysql_session_gc($maxlifetime);Borramos de la tabla todos los registros para los cuales la diferencia entre el ultimo tiempo de acceso y el actual sea mayor al parametro recibido


El codigo


Una vez analizado que es lo que hay que hacer solo resta programarlo, a continuacion presentamos el codigo y que pueden obtener tambien desde este link













<? 
// This code is released under the same license as PHP

$SessionTableName = "PHPSessions";
assert(!empty($SessionTableName));

function mysql_session_open ($save_path, $session_name) {
mysql_pconnect("HOST","USER","PASS");
mysql_select_db("BASE");
return true;
}

function mysql_session_close() {
return true;
}

function mysql_session_read ($SessionID) {
global $SessionTableName;

$SessionID = addslashes($SessionID);

$session_data = mysql_query("SELECT Data FROM $SessionTableName
WHERE SessionID = '$SessionID'") or die(db_error_message());
if (mysql_numrows($session_data) == 1) {
return mysql_result($session_data, 0);
} else {
return false;
}
}

function mysql_session_write ($SessionID, $val) {
global $SessionTableName;

$SessionID = addslashes($SessionID);
$val = addslashes($val);

$SessionExists = mysql_result(mysql_query("SELECT COUNT(*) FROM $SessionTableName
WHERE SessionID = '$SessionID'"), 0);

if ($SessionExists == 0) {
$retval = mysql_query("INSERT INTO $SessionTableName
(SessionID, LastActive, Data)
VALUES ('$SessionID', UNIX_TIMESTAMP(NOW()), '$val')")
or die(db_error_message());
} else {
$retval = mysql_query("UPDATE $SessionTableName SET
Data = '$val', LastActive = UNIX_TIMESTAMP(NOW())
WHERE SessionID = '$SessionID'") or die(db_error_message());
if (mysql_affected_rows() == 0) {
error_log("unable to update session data for session $SessionID");
}
}

return $retval;
}

function mysql_session_destroy ($SessionID) {
global $SessionTableName;

$SessionID = addslashes($SessionID);

$retval = mysql_query("DELETE FROM $SessionTableName
WHERE SessionID = '$SessionID'") or die(db_error_message());
return $retval;
}

function mysql_session_gc ($maxlifetime = 300) {
global $SessionTableName;
$CutoffTime = time() - $maxlifetime;
$retval = mysql_query("DELETE FROM $SessionTableName
WHERE LastActive < $CutoffTime") or die(db_error_message());
return $retval;
}

session_set_save_handler (
'mysql_session_open',
'mysql_session_close',
'mysql_session_read',
'mysql_session_write',
'mysql_session_destroy',
'mysql_session_gc' );
?>




Como usar el los nuevos handlers


Para usar los nuevos handlers tenemos que, en primer lugar crear la tabla PHPSessions con el codigo provisto para alguna base mysql, si se quiere se puede crear una nueva base para las sesiones, una vez creada la tabla modificar el script con los handlers indicando HOST,USER,PASS y BASE en la funcion mysql_session_open, luego solamente resta poner el script en el servidor y hacer un include del mismo "antes" del session_start, y eso es todo, pueden probarlo y ver como los datos se almacenan en la tabla y se mantiene la persistencia de la sesion. Para usarlo en otro server el proceso es el mismo, no olviden poner correctamente el HOST, USER,PASS en la funcion de conexion para que los dos servidores se conecten a la misma base MySQL.


Conclusiones


Las tecnica que hemos descripto permite compartir sesiones facilmente entre servidores y ademas ha demostrado ser
mucho mas limpia y eficiente incluso en instalaciones con un unico servidor, esta es una de las cosas a tener en cuenta
cuando nos lancemos a construir multiples sitios o sitios que quieran compartir usuarios en forma limpia y efectiva.