首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【在线五子棋对战】九、session管理模块

【在线五子棋对战】九、session管理模块

作者头像
利刃大大
发布2025-07-22 08:33:39
发布2025-07-22 08:33:39
11500
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

前言

​ 和之前的房间类、房间管理类一样,我们在设计 session 模块的时候,分为 sessionsession管理类,这样子更加便于我们去分开组织,前者关心的是一个 session 内部的情况,而后者关心的是多个 session 的管理!

Ⅰ. 什么是session

​ 在 WEB 开发中,HTTP 协议是⼀种无状态短链接的协议,这就导致一个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。

​ 因此,服务器为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个 session 对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的 session 中,当用户使用浏览器访问其它程序时,其它程序可以从用户的 session 中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。

Ⅱ. session工作原理

​ 所以涉及到了一个会话过期的操作,那么就要用定时器,来执行定时删除会话的操作!下面是 websocketpp 提供的定时器,之前我们在介绍 websocketpp 的时候也有列出过这些接口,但是都没有仔细介绍过:

代码语言:javascript
代码运行次数:0
运行
复制
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() 函数,就会去设置定时器,时间到了底层会帮我们去触发对应的函数!

代码语言:javascript
代码运行次数:0
运行
复制
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 中实现

代码语言:javascript
代码运行次数:0
运行
复制
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 中实现,所以下面给出基本的框架:

代码语言:javascript
代码运行次数:0
运行
复制
#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() 函数,它实现起来比较麻烦,我们单独拎出来!

这个函数比较吃逻辑关系,下面列举几点帮忙理清一下思路:

  1. 我们下面的操作中,关于 设置定时器的操作,都是通过成员变量 _server 的接口 set_timer() 来实现的
  2. 代码中对于 session_ptr 类型对象的定时器操作,只是将我们设置好的定时器通过其设置进 session,或者从 session 中获取其已有的定时器,最本质来说定时器的操作还是通过 _server 成员变量也就是 wsserver_t::timer_ptr 来实现的,这点要搞清楚!
  3. 判断语句中,对于后两种判断,都是基于 session 设置了定时删除的情况下,此时就要先将这个 session 原来的定时器取消掉。
    • 但是问题是取消了定时器之后,其触发函数会在很短时间内执行,这个触发函数就是我们设定的去删除会话的函数,它会删掉当前 session 的映射关系,所以我们 取消定时器之后必须去重新将当前 session 添加到映射关系中
    • 问题又来了,因为触发函数不是瞬间就触发执行的,可能会经历一小段时间后才执行,此时如果马上就去重新添加会话的话,有可能添加完系统程序才转而去执行触发函数也就是删除会话函数,相当于白添加会话了,所以不能直接进行重新添加会话,问题是我们也不知道它会什么时候触发呀,这可怎么办❓❓❓
    • 解决方法就是 重新设定定时器,并将定时器的触发时间设为 0,触发函数设为添加会话函数。为什么这样子就可以了呢❓❓❓因为定时器的触发函数事件是有队列排序的,当一个事件执行完才会去执行下一个函数,那么 只要我们用定时器,将添加会话函数排到删除会话函数的后面,那么就一定保证了重新添加会话函数执行有效

​ 剩下的细节参考代码中的注释:

代码语言:javascript
代码运行次数: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关联的定时器
    }
}

完整代码

代码语言:javascript
代码运行次数:0
运行
复制
#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
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-07-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Ⅰ. 什么是session
  • Ⅱ. session工作原理
  • Ⅲ. session类设计与实现
  • Ⅳ. session管理类设计与实现
  • 完整代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档