惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
Scott Helme
Scott Helme
爱范儿
爱范儿
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky
V
V2EX
腾讯CDC
博客园_首页
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
小众软件
小众软件
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
罗磊的独立博客
Recorded Future
Recorded Future
博客园 - 聂微东
O
OpenAI News
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
S
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
宝玉的分享
宝玉的分享
K
Kaspersky official blog
N
Netflix TechBlog - Medium
T
The Exploit Database - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
云风的 BLOG
云风的 BLOG
Simon Willison's Weblog
Simon Willison's Weblog
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 热门话题
美团技术团队
L
Lohrmann on Cybersecurity

博客园 - 莫耶

学习RxJS:Cycle.js 学习RxJS: 导入 [Node.js] Node.js项目的持续集成 [Node.js] 使用node-forge保障Javascript应用的传输安全 [Node.js] 对称加密、公钥加密和RSA [Node.js] DSL in action [Node.js] 使用TypeScript编写Node项目 [Node.js] Node.js中的流 [Node.js] 基于Socket.IO 的私聊 [Node.js] 闭包和高阶函数 [Node.js] Promise,Q及Async [Node.js] Express的测试覆盖率 [Node.js] BDD和Mocha框架 [Node.js] 也说this [Node.js] ECMAScript 6中的生成器及koa小析 [Node.js] 使用File API 异步上传文件 [Node.js] OAuth 2 和 passport框架 [Node.js] Cluster,把多核用起来 Python开源框架Scrapy安装及使用
[Node.js] Node + Redis 实现分布式Session方案
莫耶 · 2014-09-28 · via 博客园 - 莫耶

2014-09-28 17:28  莫耶  阅读(4006)  评论()    收藏  举报

原文地址: http://www.moye.me/?p=565

Session是什么?

Session 是面向连接的状态信息,是对 Http 无状态协议的补充。

Session 怎么工作?

Session 数据保留在服务端,而为了标识具体 Session 信息指向哪个连接,需要客户端传递向服务端发送一个连接标识,比如存在Cookies 中的session_id值(也可以通过URL的QueryString传递),服务端根据这个id 存取状态信息。

在服务端存储 Session,可以有很多种方案:

  1. 内存存储
  2. 数据库存储
  3. 分布式缓存存储

分布式Session

随着网站规模(访问量/复杂度/数据量)的扩容,针对单机的方案将成为性能的瓶颈,分布式应用在所难免。所以,有必要研究一下 Session 的分布式存储。

如前述, Session使用的标识其实是客户端传递的 session_id,在分布式方案中,一般会针对这个值进行哈希,以确定其在 hashing ring 的存储位置。

Session_id

在 Session 处理的事务中,最重要的环节莫过于 客户端与服务端 关于 session 标识的传递过程:

  • 服务端查询客户端Cookies 中是否存在 session_id
    1. 有session_id,是否过期?过期了需要重新生成;没有过期则延长过期
    2. 没有 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 是怎么得到的,稍候会有详细的代码示例。

Hashing Ring

hashing ring 就是一个分布式结点的回路(取值范围:0到232-1,在在零点重合):Session 应用场景中,它根据 session_id 的哈希值,按顺时针方向就近安排一个小于其值的结点进行存储。

Hashing Ring
实现这个回路的算法多种多样,比如 一致性哈希

我的哈希环实现( 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('');
});

小结

虽然本文提供的是基于 Express 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的 :)

更多文章请移步我的blog新地址: http://www.moye.me/ 

Creative Commons License