转载自: http://www.igooda.cn/jzjl/20141031644.html
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(''); });
小结
虽然本文提供的是基于 Express 的示例,但基于哈希算法和缓存设施的分布式思路,其实是放之四海而皆准的
相关推荐
基于vue+elementui+nodejs+mysql实现的仓库管理系统源码.zip 该项目是个人毕设项目,答辩评审分达到95分,代码都经过调试测试,确保可以运行!欢迎下载使用,可用于小白学习、进阶。 该资源主要针对计算机、通信、...
项目:使用NodeJs + Redis(BDA NoSQL)+ Docker + Bull在后台创建作业该项目的目的是使用NodeJS + Redis(BDA NoSQL)在异步后台创建作业,并具有注册用户(带有名称和电子邮件)的功能,其中将发送电子邮件以生成...
程序在nodemq文件夹下 1 默认队列 ... 2 自定义队列 ... 获取队列内容: ...http://127.0.0.1:8000/getList queueName">一个...在安装好redis和nodejs后 配置config js 执行: $ node index js 启动服务 入队: 1 默认队列
SpringBoot+Vue+Redis+Mysql实现水果商城.zip SpringBoot+Vue+Redis+Mysql实现水果商城.zip SpringBoot+Vue+Redis+Mysql实现水果商城.zip SpringBoot+Vue+Redis+Mysql实现水果商城.zip SpringBoot+Vue+Redis+...
基于Nodejs+TypeScript+Koa+MySQL实现的宿舍管理系统源码(前端+后端)+使用说明+sql数据库.zip基于Nodejs+TypeScript+Koa+MySQL实现的宿舍管理系统源码(前端+后端)+使用说明+sql数据库.zip基于Nodejs+TypeScript+Koa+...
基于nodejs+mysql实现的仿京东商城app项目 前端 页面结构(H5,CSS3,原生JS) 框架(基于Vue脚手架:vue-cli)进行搭建 数据请求处理框架(Axios) Vue-Router进行路由处理 Vue-LazyLoad进行图片赖加载 服务端 选用NodeJs...
本文主要介绍NodeJS+Express+Mysql 实现POST和GET请求的增删改查,后续会在博客发布详细说明,可以关注一下
通过以上步骤,我们完成了NodeJS+Vue实现支付宝支付的沙箱环境下的完整流程。这个过程中,不仅涉及到了前后端的交互,还涵盖了第三方支付平台的接入,对于提升开发者在实际项目中的综合能力非常有帮助。在实际生产...
基于Nodejs+Vue+elementplus实现的卓越人才选拔系统源码(前端+后端).zip毕业新项目-基于Nodejs+Vue+elementplus实现的卓越人才选拔系统源码(前端+后端).zip毕业新项目-基于Nodejs+Vue+elementplus实现的卓越人才选拔...
Nodejs+eggjs+mariadb 编写的一套内容管理系统 Nodejs+eggjs+mariadb 编写的一套内容管理系统 Nodejs+eggjs+mariadb 编写的一套内容管理系统 Nodejs+eggjs+mariadb 编写的一套内容管理系统 Nodejs+eggjs+...
基于Vue+Nodejs+MongoDB实现的超市后台商品的订单管理系统代码+文档说明+数据库,含有代码注释,新手也可看懂,个人手打98分项目,导师非常认可的高分项目,毕业设计、期末大作业和课程设计高分必看,下载下来,简单...
基于nodejs+crypto+elliptic实现的一些加密算法源码+详细注释(课程作业).zip基于nodejs+crypto+elliptic实现的一些加密算法源码+详细注释(课程作业).zip基于nodejs+crypto+elliptic实现的一些加密算法源码+详细注释...
此外,课程还涵盖了Redis高速缓存、Memcache内存对象缓存、MVC设计模式、PDO数据库操作等高级话题,以及面向对象编程思想的深入剖析,助您构建高效、可扩展的应用系统。通过实战项目演练,您将学会如何将所学知识...
内容概要,一个基于逻辑为使用node做为后端端口,使用kafka主题作为流接口,利用前端发送的数据来到kafak主题然后进行spark分析形成推荐列表进行实时推荐,并且使用历史数据实现历史推荐,使用了redis作为缓存数据库...
图书管理系统,java+express+mongodb+nodejs+gulp.zip 图书管理系统,java+express+mongodb+nodejs+gulp.zip 图书管理系统,java+express+mongodb+nodejs+gulp.zip 图书管理系统,java+express+mongodb+nodejs+...
基于Vue+Nodejs+MongoDB实现的超市后台商品的订单管理系统+源代码+文档说明+数据库 .zip本资源中的源码都是经过本地编译过可运行的,评审分达到95分以上。资源项目的难度比较适中,内容都是经过助教老师审定过的能够...
liunx nodejs+nginx配置收藏版liunx nodejs+nginx配置收藏版
9. **自动登录(Remember Me)**:自动登录功能可能通过设置持久化的用户凭据(如cookie或session)来实现,以便用户在下次访问时无需重新登录。 10. **错误处理**:错误页面表明项目包含了完善的错误处理机制,...
Java企业级快速开发平台 前后端分离基于nodejs+vue2+webpack+springboot.zipJava企业级快速开发平台 前后端分离基于nodejs+vue2+webpack+springboot.zipJava企业级快速开发平台 前后端分离基于nodejs+vue2+webpack+...
毕业设计,基于Vue+NodeJS+Express+MongoDb开发的在线考试系统,内含NodeJS完整源代码,数据库脚本 基于Vue+Express+MongoDB在线考试系统设计毕业源码案例设计 开发工具: WebStorm 开发环境:Nodejs + vue + ...