PHP and Node.JS session share using memcache
最近在部署nodejs和php的通过memcached实现session共享的pc和andriod的实时聊天。看到此片文章,感觉非常不错,留下以备后用。谢谢作者了,帮助很大。
I was needing some real time performance using PHP. After searching and creating « patched » systems, i’ve to admit there is no real good system using only PHP.
First step
The best way i found was using long polling system using a file system + Mysql to store current system (file, when user first connect), and last commands sended (Mysql, for user already logged who need only small updates). The system still have some trouble because of lost messages while transfering and sometimes time difference while using clearstatcache.
You can found a really basic example of such kind of system here.
This system in general is not stable for production. So I decide to extend existing PHP system with Node.JS + socket.io.
Extending PHP
Because the PHP system was already finished, and only few parts use real time, the idea was using a share system between PHP and Node.JS. The main problem was in fact to share session from PHP to Node.JS.
Understanding PHP’s session
PHP session are not easy to change/manipulate when serialized, basically PHP use a handler to store data, a stored sessions will look like this :<br style="margin:0px;padding:0px;" />user_id|s:1:"1";password|s:0:"";firstname|s:7:"Charles";<br style="margin:0px;padding:0px;" />
Basically it is : key | type : length : value ;
In case of integer, it will be type : value directly.
The problem is that, this serialize system is not so easy to manipulate in PHP, and in Node.JS also. PHP use a specific serializer/unserializer called session_decode and session_encode for that. And Node.JS got nothing. The problem is pretty simple, imagine a string variable using a « ; » inside. In such case you just can’t use a split(« ; ») (like many link on internet suggest) because you will cut the variable content. That’s why we should change the storing system to get a more easy-to-change format for both PHP and Node.JS : JSON.
PHP Session Handler
There is many way to change the session handler, but in fact only one is enough flexible to change the stored result. If you use directly php.ini to set a different storage (like memcache), you will not get what we want, because session will still be stored like previous system but in memcache, and you cannot change anything to that. PHP provide a more flexible system, using the session_set_save_handler. This function allow to manipule each basic operation on session storage : open/read/write/delete/close and gc (garbage collector).
By the way, we must have a shared system flexible for Node.JS and PHP, basically there is two main system : memcache and SQL storage, which are both pretty easy to use in Node and PHP.
From now, I consider you got PHP/Node.JS & memcache configured, with memcache running on port 11211 on localhost, PHP is linked to memcache using php-memcache. There is plenty tutorials on internet, for every system (Windows, linux/OSX).
Custom Handler to share with Node.JS
The problem with session_set_save_handler function, is that save function already got session encoded data version passed (so we recieve the encoded session), and read function must retrieve the same already-encoded version, so we must unserialize before storing, and re-serialize after storing.
This class already do the trick properly :
<?php /** * memcacheSessionHandler class * @class memcacheSessionHandler * @file memcacheSessionHandler.class.php * @brief This class is used to store session data with memcache, it store in json the session to be used more easily in Node.JS * @version 0.1 * @date 2012-04-11 * @author Deisss * @licence LGPLv3 * This class is used to store session data with memcache, it store in json the session to be used more easily in Node.JS */ class memcacheSessionHandler{ private $host = "localhost"; private $port = 11211; private $lifetime = 0; private $memcache = null; /** * Constructor */ public function __construct(){ $this->memcache = new Memcache; $this->memcache->connect($this->host, $this->port) or die("Error : Memcache is not ready"); session_set_save_handler( array($this, "open"), array($this, "close"), array($this, "read"), array($this, "write"), array($this, "destroy"), array($this, "gc") ); } /** * Destructor */ public function __destruct(){ session_write_close(); $this->memcache->close(); } /** * Open the session handler, set the lifetime ot session.gc_maxlifetime * @return boolean True if everything succeed */ public function open(){ $this->lifetime = ini_get('session.gc_maxlifetime'); return true; } /** * Read the id * @param string $id The SESSID to search for * @return string The session saved previously */ public function read($id){ $tmp = $_SESSION; $_SESSION = json_decode($this->memcache->get("sessions/{$id}"), true); if(isset($_SESSION) && !empty($_SESSION) && $_SESSION != null){ $new_data = session_encode(); $_SESSION = $tmp; return $new_data; }else{ return ""; } } /** * Write the session data, convert to json before storing * @param string $id The SESSID to save * @param string $data The data to store, already serialized by PHP * @return boolean True if memcached was able to write the session data */ public function write($id, $data){ $tmp = $_SESSION; session_decode($data); $new_data = $_SESSION; $_SESSION = $tmp; return $this->memcache->set("sessions/{$id}", json_encode($new_data), 0, $this->lifetime); } /** * Delete object in session * @param string $id The SESSID to delete * @return boolean True if memcached was able delete session data */ public function destroy($id){ return $this->memcache->delete("sessions/{$id}"); } /** * Close gc * @return boolean Always true */ public function gc(){ return true; } /** * Close session * @return boolean Always true */ public function close(){ return true; } } new memcacheSessionHandler(); ?>
You just need to include this script at the beginning of each script wich use session, it must be included before every session_start call (it start by itself memcacheSessionHandler class).
This system will overpass all php.ini config about session handling. Now if you check inside memcache, the session are now stored in JSON. It will serialize/unserialize to still let PHP system using his own way. It is totally transparent for PHP.
Be carefull session_set_save_handler change with PHP5.4, so in this case you have to modify little bit this class :
class memcacheSessionHandler implements SessionHandlerInterface{
For PHP5.4 now there is interface to implements. There is also session_set_save_handler little bit different (replace on __construct function) :
session_set_save_handler(&$this, true);
Instead of long code function… This is enough for support on PHP5.4.
Node.JS Part
Now we just need to use memcache (wich store JSON session file, and delete them after session max lifetime) inside Node.JS :
First don’t forget to install memcache : npm install memcache
Here is a basic example how to use PHP Session (using cookie session ID) :
var app = require("http").createServer(handler), fs = require("fs"), memcache = require("memcache"), co = require("./cookie.js"); app.listen(7070); //On client incomming, we send back index.html function handler(req, res){ fs.readFile(__dirname + "/index.html", function(err, data){ if(err){ res.writeHead(500); return res.end("Error loading index.html"); }else{ res.writeHead(200); res.end(data); } }); //Using php session to retrieve important data from user var cookieManager = new co.cookie(req.headers.cookie); var client = new memcache.Client(11211, "localhost"); client.connect(); client.get("sessions/"+cookieManager.get("PHPSESSID"), function(error, result){ console.log("error : "+error); if(typeof(error)==="undefined"){ var session = JSON.parse(result); } }); }
You need to get also this cookie module (./cookie.js) :
//Directly send cookie to system, if it's node.js handler, send : //request.headers.cookie //If it's socket.io cookie, send : //client.request.headers.cookie module.exports.cookie = function(co){ this.cookies = {}; co && co.split(';').forEach(function(cookie){ var parts = cookie.split('='); this.cookies[parts[0].trim()] = (parts[1] || '').trim(); }.bind(this)); //Retrieve all cookies available this.list = function(){ return this.cookies; }; //Retrieve a key/value pair this.get = function(key){ if(this.cookies[key]){ return this.cookies[key]; }else{ return {}; } }; //Retrieve a list of key/value pair this.getList = function(map){ var cookieRet = {}; for(var i=0; i<map.length; i++){ if(this.cookies[map[i]]){ cookieRet[map[i]] = this.cookies[map[i]]; } } return cookieRet; }; };
Now it’s almost finish, Node.JS and PHP are both linked to memcache, and share session in JSON. When one (PHP in fact), disconnect, the other (Node.JS) will do the same because PHP continue to keep hand on memcache like it use to do with session (so login & logout should stay on PHP side).
Don’t forget on Node.JS the PHPSESSID => may be different if you use a different session name on PHP.ini.