和之前的房间类、房间管理类一样,我们在设计 session 模块的时候,分为 session类 和 session管理类,这样子更加便于我们去分开组织,前者关心的是一个 session 内部的情况,而后者关心的是多个 session 的管理!
Ⅰ. 什么是session
在 WEB 开发中,HTTP 协议是⼀种无状态短链接的协议,这就导致一个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。
因此,服务器为每个用户浏览器创建一个会话对象(**session**对象),注意:一个浏览器独占一个 session 对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的 session 中,当用户使用浏览器访问其它程序时,其它程序可以从用户的 session 中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。
Ⅱ. session工作原理

所以涉及到了一个会话过期的操作,那么就要用定时器,来执行定时删除会话的操作!下面是 websocketpp 提供的定时器,之前我们在介绍 websocketpp 的时候也有列出过这些接口,但是都没有仔细介绍过:
template <typename config>
class endpoint
: public config::socket_type
{
typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr; // 定时器句柄
// websocketpp提供的定时器,以毫秒为单位
timer_ptr set_timer(long duration, timer_handler callback);
// 取消定时器,并且会触发之前设置的callback函数 -- 具体触发的时机是不确定的
std::size_t cancel();
} 举个例子:在该 http_callback() 函数中设置定时器,5000ms 也就是 5 秒后触发 print() 函数,但是注意这并不是准时的去触发 print() 函数,实际上会有一些延迟,所以可能会造成一些问题,下面我们在讲模块的时候会详细探讨。
当服务器收到了 http 请求的时候,此时就会触发 http_callback() 函数,就会去设置定时器,时间到了底层会帮我们去触发对应的函数!
void print(const std::string& body)
{
std::cout << body << std::endl;
}
void http_callback(wsserver_t* server, websocketpp::connection_hdl hdl)
{
// ......
wsserver_t::timer_ptr tp = server->set_timer(5000, std::bind(print, "lirenniubi !"));
tp->cancel();
}Ⅲ. session类设计与实现
这里我们简单的设计一个 session 类,但是 session 对象不能一直存在,这样是一种资源泄漏,因此需要使用定时器对每个创建的 session 对象进行定时销毁,当一个客户端连接断开后,一段时间内都没有重新连接则销毁 session。所以我们设计的 session 类需要有以下这些成员变量:
- session 标识符:用来表示会话的唯一性。
- session 对应用户的 ID
- 用户状态:用于判断当前用户是未登录还是已经登录。
- 与当前 session 关联的定时器:每个
session都会有各自的定时器,因为其生存时间是不一样的。
接着就是接口,其实 session 类的接口很简单,就是一些给成员变量赋值和获取成员变量的功能,下面直接给出 session 类的实现,其放在头文件 session.hpp 中实现
typedef enum { UNLOGIN, LOGIN } STATUS;
class session
{
private:
uint64_t _sessionID; // 会话标识符
uint64_t _userID; // 用户ID
STATUS _status; // 用户状态
wsserver_t::timer_ptr _timer; // 定时器
public:
// 构造函数和析构函数
session(uint64_t sessionID) : _sessionID(sessionID)
{
DLOG("SESSION %p 被创建!!", this);
}
~session()
{
DLOG("SESSION %p 被释放!!", this);
}
// 判断状态接口
bool isLogin() { return (_status == LOGIN); }
// 获取成员变量接口
uint64_t getSessionID() { return _sessionID; }
uint64_t getUserID() { return _userID; }
STATUS getStatus() { return _status; }
wsserver_t::timer_ptr& getTimer() { return _timer; }
// 给成员变量赋值接口
void setUserID(uint64_t userID) { _userID = userID; }
void setStatus(STATUS status) { _status = status; }
void setTimer(const wsserver_t::timer_ptr& timer) { _timer = timer; }
};Ⅳ. session管理类设计与实现
这个管理类最重要的接口无非就是创建 session、销毁 session、获取 session、设置 session 过期时间,等,其中可能还需要一些子函数来帮助完成这四个主要的接口!下面是成员变量的设计:
- 会话ID分配计数器:为每个
session分配一个唯一的、递增的ID。 - 互斥锁
- websocketpp的服务器对象:也就是
wsserver_t类型的对象,因为我们需要用到定时器,而定时器的接口就是这个对象类型提供的。 - 会话ID和会话信息的管理哈希表:就是 会话ID 和 会话信息 的映射嘛,方便我们通过 会话ID 找到 会话信息。
和 session 类一样,这个 session 管理类也放在 session.hpp 中实现,所以下面给出基本的框架:
#define SESSION_EXPIRE_TIME 30000 // 代表会话保存时间为30000ms
#define SESSION_FOREVER -1 // 代表会话永久,不过期
using session_ptr = std::shared_ptr<session>; // 声明一个智能指针管理的会话对象类型
class session_manager
{
private:
uint64_t _count; // 会话ID分配计数器
std::mutex _mtx; // 互斥锁
wsserver_t* _server; // websocketpp的服务器对象
std::unordered_map<uint64_t, session_ptr> _hash; // 会话ID和会话信息的管理哈希表
public:
// 构造函数和析构函数
session_manager(wsserver_t* server)
: _count(1), _server(server)
{
DLOG("session管理器初始化完毕!");
}
~session_manager()
{
DLOG("session管理器即将销毁!");
}
// 创建session函数,返回一个智能指针管理的会话对象类型
session_ptr add_session(uint64_t userID, STATUS status)
{
std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
// 1. 通过计数器创建一个session对象,由session_ptr管理
session_ptr sp(new session(_count));
if(sp.get() == nullptr)
return session_ptr();
// 2. 设置会话状态,并且映射会话id和会话信息的关系
sp->setStatus(status);
sp->setUserID(userID);
_hash[_count] = sp;
// 3. 别忘了计数器要自增
_count++;
return sp;
}
// 通过会话ID获取会话信息函数
session_ptr get_session_by_sesssionID(uint64_t sessionID)
{
std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
auto ret = _hash.find(sessionID);
if(ret == _hash.end())
return session_ptr();
return ret->second;
}
// 销毁session函数
void removeSession(uint64_t sessionID)
{
std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
_hash.erase(sessionID);
}
// 设置session过期时间函数
void set_session_expire_time(uint64_t sessionID, int ms);
}; 这里还有一个 set_session_expire_time() 函数,它实现起来比较麻烦,我们单独拎出来!
这个函数比较吃逻辑关系,下面列举几点帮忙理清一下思路:
- 我们下面的操作中,关于设置定时器的操作,都是通过成员变量
_server的接口set_timer()来实现的 - 代码中对于
session_ptr类型对象的定时器操作,只是将我们设置好的定时器通过其设置进session,或者从session中获取其已有的定时器,最本质来说定时器的操作还是通过_server成员变量也就是wsserver_t::timer_ptr来实现的,这点要搞清楚! - 判断语句中,对于后两种判断,都是基于
session设置了定时删除的情况下,此时就要先将这个session原来的定时器取消掉。- 但是问题是取消了定时器之后,其触发函数会在很短时间内执行,这个触发函数就是我们设定的去删除会话的函数,它会删掉当前
session的映射关系,所以我们取消定时器之后必须去重新将当前 session 添加到映射关系中。 - 问题又来了,因为触发函数不是瞬间就触发执行的,可能会经历一小段时间后才执行,此时如果马上就去重新添加会话的话,有可能添加完系统程序才转而去执行触发函数也就是删除会话函数,相当于白添加会话了,所以不能直接进行重新添加会话,问题是我们也不知道它会什么时候触发呀,这可怎么办❓❓❓
- 解决方法就是
重新设定定时器,并将定时器的触发时间设为 0,触发函数设为添加会话函数。为什么这样子就可以了呢❓❓❓因为定时器的触发函数事件是有队列排序的,当一个事件执行完才会去执行下一个函数,那么只要我们用定时器,将添加会话函数排到删除会话函数的后面,那么就一定保证了重新添加会话函数执行有效!
- 但是问题是取消了定时器之后,其触发函数会在很短时间内执行,这个触发函数就是我们设定的去删除会话的函数,它会删掉当前
剩下的细节参考代码中的注释:
void append_session(const session_ptr& sp)
{
std::unique_lock<std::mutex> lock(_mtx);
_hash[sp->getSessionID()] = sp;
}
// 设置session过期时间函数
void set_session_expire_time(uint64_t sessionID, int ms)
{
// 通过websocketpp的定时器来完成session生命周期的管理。
// 登录之后,创建session,这个session需要在指定时间、无通信后删除
// 但是进入游戏大厅,或者游戏房间,这个session就应该永久存在,因为不可能说玩完游戏之后提示重新输入密码
// 等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除
// 1. 创建session句柄
session_ptr sp = get_session_by_sesssionID(sessionID);
if(sp.get() == nullptr)
return;
// 2. 通过session句柄接口获取定时器
wsserver_t::timer_ptr timer = sp->getTimer();
// 3. 通过定时器和参数ms来分别处理四种不同情况
// - 其中定时器为空表示会话是永久的,因为没有设置;不为空则说明要定时删除会话
// - ms为SESSION_FOREVER代表要设置会话为永久;不为SESSION_FOREVER代表要设置过期时间为ms
if(timer.get() == nullptr && ms == SESSION_FOREVER)
{
// 1. 在session永久存在的情况下,设置永久存在
// 这种情况相当于不用设置,什么都不做
}
else if(timer.get() == nullptr && ms != SESSION_FOREVER)
{
// 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务
timer = _server->set_timer(ms, std::bind(&session_manager::removeSession, this, sessionID));
sp->setTimer(timer);
}
else if(timer.get() != nullptr && ms == SESSION_FOREVER)
{
// 3. 在session设置了定时删除的情况下,将session设置为永久存在
// 首先就得将原来要删除的任务取消,但是就会触发对应的执行函数也就是删除会话,所以取消完要去重新添加会话信息
timer->cancel();
sp->setTimer(wsserver_t::timer_ptr()); // 并将该session的计数器更新一下,构造一个空的定时器表示为会话永久
// 又因为该触发函数可能不会立马执行,所以我们不能马上就去重新添加上会话
// 这里得再搞一个定时器,设置触发时间为0,触发函数为添加会话函数
// 也就是此时添加会话函数的执行的顺序就排在了删除会话函数后,保证了不会提取删的情况!
// 但是因为add_session函数是新建一个session,我们要添加的是原来这个session,所以创建一个子函数append来满足我们这个要求
_server->set_timer(0, std::bind(&session_manager::append_session, this, sp));
}
else if(timer.get() != nullptr && ms != SESSION_FOREVER)
{
// 4. 在session设置了定时删除的情况下,将session重置删除时间。
// 这种情况比较复杂,首先取消定时器,那么就会触发了触发函数去删除会话,我们就要重新添加会话
// 因为触发函数并不是立马被执行的,为了保证添加会话一定成功,我们再设定一次定时器,时间设为0,触发函数是append添加会话函数
// 这样子就能保证append添加会话函数在删除会话函数之后才执行!
timer->cancel();
sp->setTimer(wsserver_t::timer_ptr());
_server->set_timer(0, std::bind(&session_manager::append_session, this, sp));
// 然后再重新设置删除时间
wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::removeSession, this, sp->getSessionID()));
sp->setTimer(tmp_tp); // 重新设置session关联的定时器
}
}完整代码
#ifndef __MY_SESSION_H__
#define __MY_SESSION_H__
#include "util.hpp"
#include <unordered_map>
#include <functional>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls_client.hpp>
typedef enum { UNLOGIN, LOGIN } STATUS;
class session
{
private:
uint64_t _sessionID; // 会话标识符
uint64_t _userID; // 用户ID
STATUS _status; // 用户状态
wsserver_t::timer_ptr _timer; // 定时器
public:
// 构造函数和析构函数
session(uint64_t sessionID) : _sessionID(sessionID)
{
DLOG("SESSION %p 被创建!!", this);
}
~session()
{
DLOG("SESSION %p 被释放!!", this);
}
// 判断状态接口
bool isLogin() { return (_status == LOGIN); }
// 获取成员变量接口
uint64_t getSessionID() { return _sessionID; }
uint64_t getUserID() { return _userID; }
STATUS getStatus() { return _status; }
wsserver_t::timer_ptr& getTimer() { return _timer; }
// 给成员变量赋值接口
void setUserID(uint64_t userID) { _userID = userID; }
void setStatus(STATUS status) { _status = status; }
void setTimer(const wsserver_t::timer_ptr& timer) { _timer = timer; }
};
#define SESSION_EXPIRE_TIME 30000
#define SESSION_FOREVER -1
using session_ptr = std::shared_ptr<session>; // 声明一个智能指针管理的会话对象类型
class session_manager
{
private:
uint64_t _count; // 会话ID分配计数器
std::mutex _mtx; // 互斥锁
wsserver_t* _server; // websocketpp的服务器对象
std::unordered_map<uint64_t, session_ptr> _hash; // 会话ID和会话信息的管理哈希表
public:
// 构造函数和析构函数
session_manager(wsserver_t* server)
: _count(1), _server(server)
{
DLOG("session管理器初始化完毕!");
}
~session_manager()
{
DLOG("session管理器即将销毁!");
}
// 创建session函数,返回一个智能指针管理的会话对象类型
session_ptr add_session(uint64_t userID, STATUS status)
{
std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
// 1. 通过计数器创建一个session对象,由session_ptr管理
session_ptr sp(new session(_count));
if(sp.get() == nullptr)
return session_ptr();
// 2. 设置会话状态,并且映射会话id和会话信息的关系
sp->setStatus(status);
sp->setUserID(userID);
_hash[_count] = sp;
// 3. 别忘了计数器要自增
_count++;
return sp;
}
// 通过会话ID获取会话信息函数
session_ptr get_session_by_sesssionID(uint64_t sessionID)
{
std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
auto ret = _hash.find(sessionID);
if(ret == _hash.end())
return session_ptr();
return ret->second;
}
// 销毁session函数
void removeSession(uint64_t sessionID)
{
std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
_hash.erase(sessionID);
}
// 设置session过期时间函数
void set_session_expire_time(uint64_t sessionID, int ms)
{
// 通过websocketpp的定时器来完成session生命周期的管理。
// 登录之后,创建session,这个session需要在指定时间、无通信后删除
// 但是进入游戏大厅,或者游戏房间,这个session就应该永久存在,因为不可能说玩完游戏之后提示重新输入密码
// 等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除
// 1. 创建session句柄
session_ptr sp = get_session_by_sesssionID(sessionID);
if(sp.get() == nullptr)
return;
// 2. 通过session句柄接口获取定时器
wsserver_t::timer_ptr timer = sp->getTimer();
// 3. 通过定时器和参数ms来分别处理四种不同情况
// - 其中定时器为空表示会话是永久的,因为没有设置;不为空则说明要定时删除会话
// - ms为SESSION_FOREVER代表要设置会话为永久;不为SESSION_FOREVER代表要设置过期时间为ms
if(timer.get() == nullptr && ms == SESSION_FOREVER)
{
// 1. 在session永久存在的情况下,设置永久存在
// 这种情况相当于不用设置,什么都不做
}
else if(timer.get() == nullptr && ms != SESSION_FOREVER)
{
// 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务
timer = _server->set_timer(ms, std::bind(&session_manager::removeSession, this, sessionID));
sp->setTimer(timer);
}
else if(timer.get() != nullptr && ms == SESSION_FOREVER)
{
// 3. 在session设置了定时删除的情况下,将session设置为永久存在
// 首先就得将原来要删除的任务取消,但是就会触发对应的执行函数也就是删除会话,所以取消完要去重新添加会话信息
timer->cancel();
sp->setTimer(wsserver_t::timer_ptr()); // 并将该session的计数器更新一下,构造一个空的定时器表示为会话永久
// 又因为该触发函数可能不会立马执行,所以我们不能马上就去重新添加上会话
// 这里得再搞一个定时器,设置触发时间为0,触发函数为添加会话函数
// 也就是此时添加会话函数的执行的顺序就排在了删除会话函数后,保证了不会提取删的情况!
// 但是因为add_session函数是新建一个session,我们要添加的是原来这个session,所以创建一个子函数append来满足我们这个要求
_server->set_timer(0, std::bind(&session_manager::append_session, this, sp));
}
else if(timer.get() != nullptr && ms != SESSION_FOREVER)
{
// 4. 在session设置了定时删除的情况下,将session重置删除时间。
// 这种情况比较复杂,首先取消定时器,那么就会触发了触发函数去删除会话,我们就要重新添加会话
// 因为触发函数并不是立马被执行的,为了保证添加会话一定成功,我们再设定一次定时器,时间设为0,触发函数是append添加会话函数
// 这样子就能保证append添加会话函数在删除会话函数之后才执行!
timer->cancel();
sp->setTimer(wsserver_t::timer_ptr());
_server->set_timer(0, std::bind(&session_manager::append_session, this, sp));
// 然后再重新设置删除时间
wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::removeSession, this, sp->getSessionID()));
sp->setTimer(tmp_tp); // 重新设置session关联的定时器
}
}
void append_session(const session_ptr& sp)
{
std::unique_lock<std::mutex> lock(_mtx);
_hash[sp->getSessionID()] = sp;
}
};
#endif