Session是什么?
Session 是面向连接的状态信息,是对 Http 无状态协议的补充。
Session 怎么工作?
Session 数据保留在服务端,而为了标识具体 Session 信息指向哪个连接,需要客户端传递向服务端发送一个连接标识,比如存在Cookies 中的session_id值(也可以通过URL的QueryString传递),服务端根据这个id 存取状态信息。
在服务端存储 Session,可以有很多种方案:
- 内存存储
- 数据库存储
- 分布式缓存存储
分布式Session
随着网站规模(访问量/复杂度/数据量)的扩容,针对单机的方案将成为性能的瓶颈,分布式应用在所难免。所以,有必要研究一下 Session 的分布式存储。
如前述, Session使用的标识其实是客户端传递的 session_id,在分布式方案中,一般会针对这个值进行哈希,以确定其在 hashing ring 的存储位置。
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 是怎么得到的,稍候会有详细的代码示例。
Hashing Ring
hashing ring 就是一个分布式结点的回路(取值范围:0到232 -1,在零点重合):Session 应用场景中,它根据 session_id 的哈希值,按顺时针方向就近安排一个大于其值的结点进行存储。
实现这个回路的算法多种多样,比如 一致性哈希。
我的哈希环实现( 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(
''
);
});
|