Session是什麼?
Session 是面向鏈接的狀態信息,是對 Http 無狀態協議的補充。javascript
Session 怎麼工做?
Session 數據保留在服務端,而爲了標識具體 Session 信息指向哪一個鏈接,須要客戶端傳遞向服務端發送一個鏈接標識,好比存在Cookies 中的session_id值(也能夠經過URL的QueryString傳遞),服務端根據這個id 存取狀態信息。html
在服務端存儲 Session,能夠有不少種方案:java
- 內存存儲
- 數據庫存儲
- 分佈式緩存存儲
分佈式Session
隨着網站規模(訪問量/複雜度/數據量)的擴容,針對單機的方案將成爲性能的瓶頸,分佈式應用在所不免。因此,有必要研究一下 Session 的分佈式存儲。node
如前述, Session使用的標識實際上是客戶端傳遞的 session_id,在分佈式方案中,通常會針對這個值進行哈希,以肯定其在 hashing ring 的存儲位置。redis
Session_id
在 Session 處理的事務中,最重要的環節莫過於 客戶端與服務端 關於 session 標識的傳遞過程:算法
- 服務端查詢客戶端Cookies 中是否存在 session_id
- 有session_id,是否過時?過時了須要從新生成;沒有過時則延長過時
- 沒有 session_id,生成一個,並寫入客戶端的 Set-Cookie 的 Header,這樣下一次客戶端發起請求時,就會在 Request Header 的 Cookies帶着這個session_id
好比我用 Express, 那麼我但願這個過程是自動完成的,不須要每次都去寫 Response Header,那麼我須要這麼一個函數(摘自樸靈的《深刻淺出Node.js》):數據庫
var setHeader = function (req, res, next) { var writeHead = res.writeHead; res.writeHead = function () { var cookies = res.getHeader('Set-Cookie'); cookies = cookies || []; console.log('writeHead, cookies: ' + cookies); var session = serialize('session_id', req.session.id); cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session]; res.setHeader('Set-Cookie', cookies); return writeHead.apply(this, arguments); }; next(); };
這個函數替換了writeHead,在每次Response寫Header時它都會獲得執行機會,因此它是自動化的。這個req.session.id 是怎麼獲得的,稍候會有詳細的代碼示例。json
Hashing Ring
hashing ring 就是一個分佈式結點的迴路(取值範圍:0到232 -1,在零點重合):Session 應用場景中,它根據 session_id 的哈希值,按順時針方向就近安排一個大於其值的結點進行存儲。緩存
實現這個迴路的算法多種多樣,好比 一致性哈希。cookie
個人哈希環實現( hashringUtils.js:
var INT_MAX = 0x7FFFFFFF; var node = function (nodeOpts) { nodeOpts = nodeOpts || {}; if (nodeOpts.address) this.address = nodeOpts.address; if (nodeOpts.port) this.port = nodeOpts.port; }; node.prototype.toString = function () { return this.address + ':' + this.port; }; var ring = function (maxNodes, realNodes) { this.nodes = []; this.maxNodes = maxNodes; this.realNodes = realNodes; this.generate(); }; ring.compareNode = function (nodeA, nodeB) { return nodeA.address === nodeB.address && nodeA.port === nodeB.port; }; ring.hashCode = function (str) { if (typeof str !== 'string') str = str.toString(); var hash = 1315423911, i, ch; for (i = str.length - 1; i >= 0; i--) { ch = str.charCodeAt(i); hash ^= ((hash << 5) + ch + (hash >> 2)); } return (hash & INT_MAX); }; ring.prototype.generate = function () { var realLength = this.realNodes.length; this.nodes.splice(0); //clear all for (var i = 0; i < this.maxNodes; i++) { var realIndex = Math.floor(i / this.maxNodes * realLength); var realNode = this.realNodes[realIndex]; var label = realNode.address + '#' + (i - realIndex * Math.floor(this.maxNodes / realLength)); var virtualNode = ring.hashCode(label); this.nodes.push({ 'hash': virtualNode, 'label': label, 'node': realNode }); } this.nodes.sort(function(a, b){ return a.hash - b.hash; }); }; ring.prototype.select = function (key) { if (typeof key === 'string') key = ring.hashCode(key); for(var i = 0, len = this.nodes.length; i<len; i++){ var virtualNode = this.nodes[i]; if(key <= virtualNode.hash) { console.log(virtualNode.label); return virtualNode.node; } } console.log(this.nodes[0].label); return this.nodes[0].node; }; ring.prototype.add = function (node) { this.realNodes.push(node); this.generate(); }; ring.prototype.remove = function (node) { var realLength = this.realNodes.length; var idx = 0; for (var i = realLength; i--;) { var realNode = this.realNodes[i]; if (ring.compareNode(realNode, node)) { this.realNodes.splice(i, 1); idx = i; break; } } this.generate(); }; ring.prototype.toString = function () { return JSON.stringify(this.nodes); }; module.exports.node = node; module.exports.ring = ring;
配置
配置信息是須要根據環境而變化的,某些狀況下它又是不能公開的(好比Session_id 加密用的私鑰),因此須要一個相似的配置文件( config.cfg:
{ "session_key": "session_id", "SECRET": "myapp_moyerock", "nodes": [ {"address": "127.0.0.1", "port": "6379"} ] }
在Node 中 序列化/反序列化JSON 是件使人愉悅的事,寫個配置讀取器也至關容易(configUtils.js:
var fs = require('fs'); var path = require('path'); var cfgFileName = 'config.cfg'; var cache = {}; module.exports.getConfigs = function () { if (!cache[cfgFileName]) { if (!process.env.cloudDriveConfig) { process.env.cloudDriveConfig = path.join(process.cwd(), cfgFileName); } if (fs.existsSync(process.env.cloudDriveConfig)) { var contents = fs.readFileSync( process.env.cloudDriveConfig, {encoding: 'utf-8'}); cache[cfgFileName] = JSON.parse(contents); } } return cache[cfgFileName]; };
分佈式Redis 操做
有了上述的基礎設施,實現一個分佈式 Redis 分配器就變得至關容易了。爲演示,這裏只簡單提供幾個操做 Hashes 的方法(redisMatrix.js:
var hashringUtils = require('../hashringUtils'), ring = hashringUtils.ring, node = hashringUtils.node; var config = require('../configUtils'); var nodes = config.getConfigs().nodes; for (var i = 0, len = nodes.length; i < len; i++) { var n = nodes[i]; nodes[i] = new node({address: n.address, port: n.port}); } var hashingRing = new ring(32, nodes); module.exports = hashingRing; module.exports.openClient = function (id) { var node = hashingRing.select(id); var client = require('redis').createClient(node.port, node.address); client.on('error', function (err) { console.log('error: ' + err); }); return client; }; module.exports.hgetRedis = function (id, key, callback) { var client = hashingRing.openClient(id); client.hget(id, key, function (err, reply) { if (err) console.log('hget error:' + err); client.quit(); callback.call(null, err, reply); }); }; module.exports.hsetRedis = function (id, key, val, callback) { var client = hashingRing.openClient(id); client.hset(id, key, val, function (err, reply) { if (err) console.log('hset ' + key + 'error: ' + err); console.log('hset [' + key + ']:[' + val + '] reply is:' + reply); client.quit(); callback.call(null, err, reply); }); }; module.exports.hdelRedis = function(id, key, callback){ var client = hashingRing.openClient(id); client.hdel(id, key, function (err, reply) { if (err) console.log('hdel error:' + err); client.quit(); callback.call(null, err, reply); }); };
分佈式Session操做
session_id 的事務和 分佈式的Redis都有了,分佈式的 Session 操做呼之欲出(sessionUtils.js:
var crypto = require('crypto'); var config = require('../config/configUtils'); var EXPIRES = 20 * 60 * 1000; var redisMatrix = require('./redisMatrix'); var sign = function (val, secret) { return val + '.' + crypto .createHmac('sha1', secret) .update(val) .digest('base64') .replace(/[\/\+=]/g, ''); }; var generate = function () { var session = {}; session.id = (new Date()).getTime() + Math.random().toString(); session.id = sign(session.id, config.getConfigs().SECRET); session.expire = (new Date()).getTime() + EXPIRES; return session; }; var serialize = function (name, val, opt) { var pairs = [name + '=' + encodeURIComponent(val)]; opt = opt || {}; if (opt.maxAge) pairs.push('Max-Age=' + opt.maxAge); if (opt.domain) pairs.push('Domain=' + opt.domain); if (opt.path) pairs.push('Path=' + opt.path); if (opt.expires) pairs.push('Expires=' + opt.expires); if (opt.httpOnly) pairs.push('HttpOnly'); if (opt.secure) pairs.push('Secure'); return pairs.join('; '); }; var setHeader = function (req, res, next) { var writeHead = res.writeHead; res.writeHead = function () { var cookies = res.getHeader('Set-Cookie'); cookies = cookies || []; console.log('writeHead, cookies: ' + cookies); var session = serialize(config.getConfigs().session_key, req.session.id); console.log('writeHead, session: ' + session); cookies = Array.isArray(cookies) ? cookies.concat(session) : [cookies, session]; res.setHeader('Set-Cookie', cookies); return writeHead.apply(this, arguments); }; next(); }; exports = module.exports = function session() { return function session(req, res, next) { var id = req.cookies[config.getConfigs().session_key]; if (!id) { req.session = generate(); id = req.session.id; var json = JSON.stringify(req.session); redisMatrix.hsetRedis(id, 'session', json, function () { setHeader(req, res, next); }); } else { console.log('session_id found: ' + id); redisMatrix.hgetRedis(id, 'session', function (err, reply) { var needChange = true; console.log('reply: ' + reply); if (reply) { var session = JSON.parse(reply); if (session.expire > (new Date()).getTime()) { session.expire = (new Date()).getTime() + EXPIRES; req.session = session; needChange = false; var json = JSON.stringify(req.session); redisMatrix.hsetRedis(id, 'session', json, function () { setHeader(req, res, next); }); } } if (needChange) { req.session = generate(); id = req.session.id; // id need change var json = JSON.stringify(req.session); redisMatrix.hsetRedis(id, 'session', json, function (err, reply) { setHeader(req, res, next); }); } }); } }; }; module.exports.set = function (req, name, val) { var id = req.cookies[config.getConfigs().session_key]; if (id) { redisMatrix.hsetRedis(id, name, val, function (err, reply) { }); } }; /* get session by name @req request object @name session name @callback your callback */ module.exports.get = function (req, name, callback) { var id = req.cookies[config.getConfigs().session_key]; if (id) { redisMatrix.hgetRedis(id, name, function (err, reply) { callback(err, reply); }); } else { callback(); } }; module.exports.getById = function (id, name, callback) { if (id) { redisMatrix.hgetRedis(id, name, function (err, reply) { callback(err, reply); }); } else { callback(); } }; module.exports.deleteById = function (id, name, callback) { if (id) { redisMatrix.hdelRedis(id, name, function (err, reply) { callback(err, reply); }); } else { callback(); } };
結合 Express 應用
在 Express 中只須要簡單的 use 就能夠了( app.js:
var session = require('../sessionUtils'); app.use(session());
這個被引用的 session 模塊暴露了一些操做 session 的方法,在須要時能夠這樣使用:
app.get('/user', function(req, res){ var id = req.query.sid; session.getById(id, 'user', function(err, reply){ if(reply){ //Some thing TODO } }); res.end(''); });
原網址(http://www.moye.me/2014/09/28/%E5%88%86%E5%B8%83%E5%BC%8Fsession/)