使用过几种 web App 开发语言和框架, 都会接触到 Session 的概念. 即使是一个简单站点访问计数的功能, 也常常使用 Session 来实现的. 其他常用的领域还有购物车, 登录用户等. 但是, 对 Session 一直是一知半解, 知其然而不知其所以然.
在认真的研究了 HTTP 协议, 以及 nodejs 开发栈的 express 和 express-session 后, 我终于比较有把握深入浅出的说清楚 Session 了, 也算是满足了多年来开发过程中, 常常浮现的对 Session 的好奇心吧.
本文使用 nodejs v9.5.0 作为技术验证工具. 阅读本文前需要了解基础的 HTTP 知识和 Cookie 知识. 详细需要参考 rfc6265, 或者阅读HTTP 小书的最后一章.
会话的概念
用户在网站的一组相互关联的的请求和响应, 就是一次会话. 简而言之是这样的:
会话 = 一组访问
访问 = 一次请求和响应
比如一个最简单的 nodejs HTTP 程序:
- var http = require('http')
- http.createServer(function(req,res){
- res.end('hello')
- }).listen(3000)
每个请求都会进入到此处理函数:
function(req,res){res.end('hello') }
, 在此函数内获得请求, 处理响应, 完成后发给客户端, 就是一次访问. 通过浏览器的 developer tools, 可以看到此次会话的请求内容和响应内容.
以站点计数应用为案例来说明的话, 就是这些来自于同样访问者的多次访问, 都可以获得当前站点的访问计数.
引入会话
我们从一个案例开始引入会话的概念. 当我们需要访问站点计数一类的功能时, 我们希望用户访问此站点时:
第一次访问时, 显示你的访问次数为 1
以后每次访问时, 访问计数加 1
此种情况下, 我们需要有一个地方存储当前计数, 这样才能在同一个客户在此访问时, 可以取出当前计数, 加一后返回给客户. 当然也因此需要识别此用户 (浏览器), 为每个用户单独计数. 就是说, 不同的用户访问时, 需要去取对应用户的当前计数.
识别客户的问题, 常用的方法就是使用 Cookie.Cookie 是 HTTP 协议的一部分. HTTP 可以通过头字段 Set-Cookie 为来访客户做一个标记, 这个标记常常就是一个 ID, 下一次访问此站点时, HTTP 会通过 Cookie 头字段, 发送此 ID 到站点, 由此站点知道此客户的身份和这个身份关联的状态信息, 比如当前访问计数, 或者此身份当前的购物车的内容等等.
识别了客户后, 就可以在 Web 服务器内, 为此客户建立它的独特的状态信息.
实现一个会话
基于 nodejs HTTP 模块, 我们实现一个极为简单的 Session 服务. 只是为了展示概念, 而不是为了实用的目的. 此服务可以实现一个共享于同一站点的多次访问的 req.session 变量, 此变量为一个对象, 可以在此变量内写入新的成员, 或者修改现存的成员变量的值, 每次访问后会保存 req.session, 以便下次访问可以得到当前的值:
- var http = require('http')
- var sessionkey = "sessionkey3"
- http.createServer(function(req,res){
- if (req.url =="/"){
- session(req,res)
- req.session.count = (req.session.count+1) || 1
- res.end('hi'+req.session.count)
- }else
- res.end('')
- }).listen(3000)
- console.log('listen on 3000')
- function session(req,res){
- if (req.session)
- return
- var answer ,id
- if(isSessionOk(req)){
- id = getCookie(req)
- answer = getSessionById(id)
- }else{
- answer= {}
- id = createSession(answer)
- setCookie(res,id)
- }
- req.session = answer
- res.on('finish', function() {
- saveSession(id,req.session)
- });
- }
- function hasCookie(req){
- return (getCookie(req)!='')
- }
- function getCookie(req){
- try{
- var c = req.headers['cookie']
- var arr = c.split(';')
- for (var i = 0; i < arr.length; i++) {
- var kv = arr[i]
- var a = kv.split('=')
- if (a[0].trim() == sessionkey)
- return a[1]
- }
- }catch(error){
- return ''
- }
- return ''
- }
- function setCookie(res,id){
- res.setHeader("set-cookie",sessionkey +"="+id)
- }
- var sessions = {}
- var sid = 0
- function getSessionById(sid){
- return sessions[sid]
- }
- function getSessionByReq(req){
- var sid = getCookie(req)
- return sessions[sid]
- }
- function createSession(session){
- sessions[sid++,session]
- return sid
- }
- function saveSession(sid,session){
- sessions[sid] = session
- }
- function isSessionOk(req){
- return hasCookie(req) && getSessionByReq(req) !== undefined
- }
程序代码比较简单, 读者可以保持它到 index.js, 然后执行此程序, 验证概念:
node index.js
然后, 启动 chrome, 访问站点 localhost:3000, 然后多次刷新, 你可以看到每次刷新, 返回的访问次数逐步累加. 在打开另一个浏览器, 比如 safari, 在此访问此站点, 你会发现返回的访问计数从 1 开始, 另外计数. 因为是两个不同的浏览器内器, 这就保证的它们是不同的访问客户, 在站点内的代码, 会区别两者, 分别记录它们的状态信息.
代码使用了 HTTP Cookie, 基本算法很简单:
如果 Session 没有准备好, 那么创建一个 Session, 得到 Session 的 ID, 把此 ID 通过 Set-Cookie 发送给浏览器. 浏览器会在下一次访问此站点时, 发送此 ID.
如果 Session 已经准备好了, 也就是说, 浏览器通过 Cookie 发来了 ID, 并且通过此 ID, 可以在站点内获取到 Session
把创建或者获取的 Session 赋值给 req 对象
在请求处理函数生命周期内, 可以获取和修改 Session 对象
在请求处理完后, 保存此 Session 变量
可能大家看到 sessionkey 这个变量, 感觉有些莫名其妙. 原因是每次 cookie 发送, 同样的站点可能有多个框架需要使用此 cookie 头字段, 比如 php,aspx,jsp 等都是需要使用了, 为了好像不要冲突, 大家各自使用 cookie 头字段内各自的 key/value 对即可. 比如 php 的 key 默认是 phpsessid,express-session 默认的是 connect.sid.
总结
此代码演示了最基础的 Session 的概念, 但是远远不是一个可用的模块, 想要真实世界中使用的 Session 模块, 可以考虑 express-session.
来源: https://juejin.im/entry/5ae2848e6fb9a07ab83dbedb