和之前的房间类、房间管理类一样,我们在设计 session
模块的时候,分为 session
类 和 session
管理类,这样子更加便于我们去分开组织,前者关心的是一个 session
内部的情况,而后者关心的是多个 session
的管理!
在 WEB
开发中,HTTP 协议是⼀种无状态短链接的协议,这就导致一个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。
因此,服务器为每个用户浏览器创建一个会话对象(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
类的实现,其放在头文件 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
。wsserver_t
类型的对象,因为我们需要用到定时器,而定时器的接口就是这个对象类型提供的。 和 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