深入解析:【个人项目】C++基于websocket的多用户网页五子棋(下)

目录

1.session管理模块设计

什么是session

session⼯作原理

session类设计实现

session管理设计实现

2.五⼦棋对战玩家匹配管理设计实现

1.匹配队列实现

2.玩家匹配管理模块设计实现

3.整合封装服务器模块设计实现

3.服务器模块实现

4.客⼾端开发

登录⻚⾯: login.html

注册⻚⾯: register.html

游戏⼤厅⻚⾯:game_hall.html

游戏房间⻚⾯:game_room.html

5.项⽬扩展

实现局时/步时

保存棋谱&录像回放

观战功能

虚拟对⼿&⼈机对战

6.项目页面


紧接上文《【个人项目】C++基于websocket的多用户网页五子棋(上)》

1.session管理模块设计

什么是session

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

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

session⼯作原理

session类设计实现

  • 这⾥我们简单的设计⼀个session类,但是session对象不能⼀直存在,这样是⼀种资源泄漏,因此 需要使⽤定时器对每个创建的session对象进⾏定时销毁(⼀个客⼾端连接断开后,⼀段时间内都 没有重新连接则销毁session)。
  • _ssid使⽤时间戳填充。实际上, 我们通常使⽤唯⼀id⽣成器⽣成⼀个唯⼀的id
  • _user保存当前⽤⼾的信息
  • timer_ptr _tp保存当前session对应的定时销毁任务
class session
{
private:
    uint64_t _ssid;             //标识符
    uint64_t _uid;              // session对应的用户ID
    ss_statu _statu;            // 用户状态:未登录,已登录
    server_t::timer_ptr _tp;    //session关联的定时器
public:
    session(uint64_t ssid): _ssid(ssid) { DLOG("SESSION %p 被创建!!", this); }
    ~session() { DLOG("SESSION %p 被释放!!", this); }
    uint64_t ssid(){ return _ssid; }
    void set_user(uint64_t uid){ _uid = uid; }
    uint64_t get_user(){ return _uid; }
    void set_statu(ss_statu statu){ _statu = statu; }
    bool is_login(){ return _statu == LOGIN; }
    void set_timer(const server_t::timer_ptr& tp){ _tp = tp; }
    server_t::timer_ptr& get_timer(){ return _tp; }
};

session管理设计实现

session的管理主要包含以下⼏个点:

  1. 创建⼀个新的session
  2. 通过ssid获取session
  3. 通过ssid判断session是否存在
  4. 销毁session。
  5. 为session设置过期时间,过期后session被销毁
#define SESSION_FORVER -1
#define SESSION_REMOVE 30000
using session_ptr = std::shared_ptr;
class session_manager
{
private:
    uint64_t _next_ssid;
    std::mutex _mutex;
    std::unordered_map _session;
    server_t* _server;
public:
    session_manager(server_t* srv): _next_ssid(1), _server(srv)
    {
        DLOG("session管理器初始化完毕");
    }
    ~session_manager(){ DLOG("session管理器即将销毁"); }
    session_ptr create_session(uint64_t uid, ss_statu statu)
    {
        std::unique_lock lock(_mutex);
        session_ptr ssp(new session(_next_ssid));
        ssp->set_statu(statu);
        ssp->set_user(uid);
        _session[_next_ssid] = ssp;
        _next_ssid++;
        return ssp;
    }
    session_ptr get_session_by_ssid(uint64_t ssid)
    {
        std::unique_lock lock(_mutex);
        if(_session.count(ssid)) return _session[ssid];
        return session_ptr();
    }
    void remove_session(uint64_t ssid)
    {
        std::unique_lock lock(_mutex);
        _session.erase(ssid);
    }
    void append_session(const session_ptr &ssp)
    {
        std::unique_lock lock(_mutex);
        _session[ssp->ssid()] = ssp;
    }
    void set_session_expire_time(uint64_t ssid, int ms)
    {
        //依赖于websocketpp的定时器来完成session生命周期的管理。
        //登录之后,创建session,session需要在指定时间无通信后删除
        //但是进入游戏大厅,或者游戏房间,这个session就应该永久存在
        //等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除
        session_ptr ssp = get_session_by_ssid(ssid);
        if(ssp.get() == nullptr) return;
        server_t::timer_ptr tp = ssp->get_timer();
        // 1. 在session永久存在的情况下,设置永久存在
        if(tp.get() == nullptr && ms == SESSION_FORVER) return;
        else if(tp.get() == nullptr && ms != SESSION_FORVER)
        {
            // 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务
            server_t::timer_ptr tmp_tp = _server->set_timer(ms,
                std::bind(&session_manager::remove_session, this, ssid));
            ssp->set_timer(tmp_tp);
        }
        else if(tp.get() != nullptr && ms == SESSION_FORVER)
        {
            // 3. 在session设置了定时删除的情况下,将session设置为永久存在
            tp->cancel();
            ssp->set_timer(server_t::timer_ptr());
            _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
        }
        else if(tp.get() != nullptr && ms != SESSION_FORVER)
        {
            // 4. 在session设置了定时删除的情况下,将session重置删除时间。
            tp->cancel(); // 因为这个取消定时任务并不是立即取消的
            ssp->set_timer(server_t::timer_ptr());
            _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
            server_t::timer_ptr tmp_tp = _server->set_timer(ms,
                std::bind(&session_manager::remove_session, this, ssid));
            ssp->set_timer(tmp_tp);
        }
    }
};

2.五⼦棋对战玩家匹配管理设计实现

1.匹配队列实现

五⼦棋对战的玩家匹配是根据⾃⼰的天梯分数进⾏匹配的,⽽服务器中将玩家天梯分数分为三个档 次:

  1. ⻘铜:天梯分数⼩于2000分
  2. ⽩银:天梯分数介于2000~3000分之间
  3. ⻩⾦:天梯分数⼤于3000分

⽽实现玩家匹配的思想⾮常简单,为不同的档次设计各⾃的匹配队列,当⼀个队列中的玩家数量⼤于等于2的时候,则意味着同⼀档次中,有2个及以上的⼈要进⾏实战匹配,则出队队列中的前两个⽤⼾,相当于队⾸2个玩家匹配成功,这时候为其创建房间,并将两个⽤⼾信息加⼊房间中。

class match_queue
{
    private:
        /*用链表而不直接使用queue是因为我们有中间删除数据的需要*/
        std::list _list;
        /*实现线程安全*/
        std::mutex _mutex;
        /*这个条件变量主要为了阻塞消费者,后边使用的时候:队列中元素个数<2则阻塞*/
        std::condition_variable _cond;
    public:
        /*获取元素个数*/
        int size()
        {
            std::unique_lock lock(_mutex);
            return _list.size();
        }
        /*判断是否为空*/
        bool empty()
        {
            std::unique_lock lock(_mutex);
            return _list.empty();
        }
        /*阻塞线程*/
        void wait()
        {
            std::unique_lock lock(_mutex);
            _cond.wait(lock);
        }
        /*入队数据,并唤醒线程*/
        void push(const T &data)
        {
            std::unique_lock lock(_mutex);
            _list.push_back(data);
            _cond.notify_all();
        }
        /*出队数据*/
        bool pop(T &data)
        {
            std::unique_lock lock(_mutex);
            if (_list.empty() == true)
            {
                return false;
            }
            data = _list.front();
            _list.pop_front();
            return true;
        }
        /*移除指定的数据*/
        void remove(T &data)
        {
            std::unique_lock lock(_mutex);
            _list.remove(data);
        }
};

std::mutex:用于保护共享资源的互斥锁,保证多线程操作的原子性。

std::condition_variable:条件变量,用于线程间的 “等待 - 通知” 同步(让线程等待某个条件,或唤醒等待的线程)。

这里不用 std::lock_guard 的原因:condition_variable::wait 需要临时解锁互斥量,lock_guard 不支持手动解锁,而 unique_lock 更灵活(支持解锁 / 重新加锁)。

_cond.wait(lock);

这是条件变量的核心操作,执行时会做两件事:

  1. 释放锁:临时释放 unique_lock 持有的 _mutex,让其他线程可以获取该锁。
  2. 阻塞线程:当前线程进入等待状态,暂停执行,直到被其他线程通过 _cond.notify_one() 或 _cond.notify_all() 唤醒。

当线程被唤醒后,wait() 会重新获取 _mutex 的锁,然后 wait() 方法返回,函数继续执行(直到结束,unique_lock 析构解锁)。

2.玩家匹配管理模块设计实现

class matcher
{
    private:
        /*普通选手匹配队列*/
        match_queue _q_normal;
        /*高手匹配队列*/
        match_queue _q_high;
        /*大神匹配队列*/
        match_queue _q_super;
        /*对应三个匹配队列的处理线程*/
        std::thread _th_normal;
        std::thread _th_high;
        std::thread _th_super;
        room_manager *_rm;
        user_table *_ut;
        online_manager *_om;
    private:
        void handle_match(match_queue &mq)
        {
            while(1)
            {
                //1. 判断队列人数是否大于2,<2则阻塞等待
                while (mq.size() < 2)
                {
                    mq.wait();
                }
                //2. 走下来代表人数够了,出队两个玩家
                DLOG("玩家数量:%d", mq.size());
                uint64_t uid1, uid2;
                bool ret = mq.pop(uid1);
                if (ret == false)
                {
                    continue;
                }
                ret = mq.pop(uid2);
                if (ret == false)
                {
                    this->add(uid1);
                    continue;
                }
                //3. 校验两个玩家是否在线,如果有人掉线,则要吧另一个人重新添加入队列
                server_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);
                if (conn1.get() == nullptr)
                {
                    this->add(uid2);
                    continue;
                }
                server_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);
                if (conn2.get() == nullptr)
                {
                    this->add(uid1);
                    continue;
                }
                //4. 为两个玩家创建房间,并将玩家加入房间中
                room_ptr rp = _rm->create_room(uid1, uid2);
                if (rp.get() == nullptr)
                {
                    this->add(uid1);
                    this->add(uid2);
                    continue;
                }
                // rp->add_black_user(uid1);
                // rp->add_white_user(uid2);
                //5. 对两个玩家进行响应
                Json::Value resp;
                resp["optype"] = "match_success";
                resp["result"] = true;
                std::string body;
                json_util::serialize(resp, body);
                std::error_code code1 =  conn1->send(body);
                DLOG("错误码:%d", code1.value());
                std::error_code code2 =  conn2->send(body);
                DLOG("错误码:%d", code2.value());
            }
        }
        void th_normal_entry() { return handle_match(_q_normal); }
        void th_high_entry() { return handle_match(_q_high); }
        void th_super_entry() { return handle_match(_q_super); }
    public:
        matcher(room_manager *rm, user_table *ut, online_manager *om):
            _rm(rm), _ut(ut), _om(om),
            _th_normal(std::thread(&matcher::th_normal_entry, this)),
            _th_high(std::thread(&matcher::th_high_entry, this)),
            _th_super(std::thread(&matcher::th_super_entry, this))
        {
            DLOG("游戏匹配模块初始化完毕....");
        }
        bool add(uint64_t uid)
        {
            //根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列
            // 1. 根据用户ID,获取玩家信息
            Json::Value user;
            bool ret = _ut->selectById(uid, user);
            if (ret == false)
            {
                DLOG("获取玩家:%ld 信息失败!!", uid);
                return false;
            }
            int score = user["score"].asInt();
            // 2. 添加到指定的队列中
            if (score < 2000)
            {
                _q_normal.push(uid);
            }else if (score >= 2000 && score < 3000)
            {
                _q_high.push(uid);
            }
            else
            {
                _q_super.push(uid);
            }
            return true;
        }
        bool del(uint64_t uid)
        {
            Json::Value user;
            bool ret = _ut->selectById(uid, user);
            if (ret == false)
            {
                DLOG("获取玩家:%ld 信息失败!!", uid);
                return false;
            }
            int score = user["score"].asInt();
            // 2. 添加到指定的队列中
            if (score < 2000)
            {
                _q_normal.remove(uid);
            }
            else if (score >= 2000 && score < 3000)
            {
                _q_high.remove(uid);
            }
            else
            {
                _q_super.remove(uid);
            }
            return true;
        }
};

3.整合封装服务器模块设计实现

服务器模块,是对当前所实现的所有模块的⼀个整合,并进⾏服务器搭建的⼀个模块,最终封装实现出⼀个gobang_server的服务器模块类,向外提供搭建五⼦棋对战服务器的接⼝。通过实例化的对象可以简便的完成服务器的搭建。

通信接⼝设计(Restful⻛格)

静态资源请求

 静态资源⻚⾯,在后台服务器上就是个html/css/js⽂件
 静态资源请求的处理,其实就是将⽂件中的内容发送给客⼾端
 1. 注册⻚⾯请求
 请求:GET /register.html HTTP/1.1
 响应:
 HTTP/1.1 200 OK
 Content-Length: xxx
 Content-Type: text/html
 register.html⽂件的内容数据
 2. 登录⻚⾯请求
 请求:GET /login.html HTTP/1.1
 3. ⼤厅⻚⾯请求
 请求:GET /game_hall.html HTTP/1.1
 4. 房间⻚⾯请求
 请求:GET /game_room.html HTTP/1.1

注册⽤⼾

 POST /reg HTTP/1.1
 Content-Type: application/json
 Content-Length: 32
 {"username":"xiaobai", "password":"123123"}
 #成功时的响应
 HTTP/1.1 200 OK
 Content-Type: application/json
 Content-Length: 15
 {"result":true}
 #失败时的响应
 HTTP/1.1 400 Bad Request
 Content-Type: application/json
 Content-Length: 43
 {"result":false, "reason": "⽤⼾名已经被占⽤"}

⽤⼾登录

 POST /login HTTP/1.1
 Content-Type: application/json
 Content-Length: 32
 {"username":"xiaobai", "password":"123123"}
 #成功时的响应
 HTTP/1.1 200 OK
 Content-Type: application/json
 Content-Length: 15
 {"result":true}
 #失败时的响应
 HTTP/1.1 400 Bad Request
 Content-Type: application/json
 Content-Length: 43
 {"result":false, "reason": "⽤⼾名或密码错误"}

获取客⼾端信息

 GET /userinfo HTTP/1.1
 Content-Type: application/json
 Content-Length: 0
 #成功时的响应
 HTTP/1.1 200 OK
 Content-Type: application/json
 Content-Length: 58
 {"id":1, "username":"xiaobai", "score":1000, "total_count":4, "win_count":2}
 #失败时的响应
 HTTP/1.1 401 Unauthorized
 Content-Type: application/json
 Content-Length: 43
 {"result":false, "reason": "⽤⼾还未登录"}

websocket⻓连接协议切换请求(进⼊游戏⼤厅)

 /* ws://localhost:9000/hall */
 GET /hall HTTP/1.1
 Connection: Upgrade
 Upgrade: WebSocket
 ......
 HTTP/1.1 101 Switching
 ......

WebSocket握⼿成功后的回复:表⽰游戏⼤厅已经进⼊成功。

 {
 "optype": "hall_ready",
 "uid": 1
 }

开始对战匹配

 {
 "optype": "match_start"
 }
 /*后台正确处理后回复*/
 {
 "optype": "match_start", //表⽰成功加⼊匹配队列
 "result": true
 }
 /*后台处理出错回复*/
 {
 "optype": "match_start"
 "result": false,
 "reason": "具体原因...."
 }
 /*匹配成功了给客⼾端的回复*/
 {
 "optype": "match_success", //表⽰成匹配成功
 "result": true
 }

停⽌匹配

 {
 "optype": "match_stop"
 }
 /*后台正确处理后回复*/
 {
     "optype": "match_stop"
 "result": true
 }
 /*后台处理出错回复*/
 {
     "optype": "match_stop"
 "result": false,
 "reason": "具体原因...."
 }

websocket⻓连接协议切换请求(进⼊游戏房间)

 /* ws://localhost:9000/room */
 GET /room HTTP/1.1
 Connection: Upgrade
 Upgrade: WebSocket
 ......
 HTTP/1.1 101 Switching
 ......

WebSocket握⼿成功后的回复:表⽰游戏房间已经进⼊成功。

 /*协议切换成功, 房间已经建⽴*/
 {
 "optype": "room_ready",
 "room_id": 222, //房间ID
 "self_id": 1, //⾃⾝ID
 "white_id": 1, //⽩棋ID
 "black_id": 2, //⿊棋ID
 }

⾛棋

 {
 "optype": "put_chess", // put_chess表⽰当前请求是下棋操作
 "room_id": 222, // room_id 表⽰当前动作属于哪个房间
 "uid": 1, // 当前的下棋操作是哪个⽤⼾发起的
 "row": 3, // 当前下棋位置的⾏号
 "col": 2 // 当前下棋位置的列号
 }
 {
 "optype": "put_chess",
 "result": false
 "reason": "⾛棋失败具体原因...."
 }
 {
 "optype": "put_chess",
 "room_id": 222,
 "uid": 1,
 "row": 3,
 "col": 2,
 "result": true,
 "reason": "对⽅掉线,不战⽽胜!" / "对⽅/⼰⽅五星连珠,战⽆敌/虽败犹荣!",
 "winner": 0 // 0-未分胜负, !0-已分胜负 (uid是谁,谁就赢了)
 }

聊天

 {
 "optype": "chat",
 "room_id": 222,
 "uid": 1,
 "message": "赶紧点"
 }
 {
 "optype": "chat",
 "result": false
 "reason": "聊天失败具体原因....⽐如有敏感词..."
 }
 {
 "optype": "chat",
 "result": true,
 "room_id": 222,
 "uid": 1,
 "message": "赶紧点"
 }

3.服务器模块实现

#include "util.hpp"
#include "db.hpp"
#include "log.hpp"
#include "matcher.hpp"
#include "online.hpp"
#include "room.hpp"
#include "session.hpp"
#define WWWROOT "./wwwroot"
class gobang_server
{
private:
    std::string _web_root;
    user_table _ut;
    matcher _mm;
    online_manager _om;
    room_manager _rm;
    session_manager _sm;
    server_t _wssrv;
private:
    void file_handler(server_t::connection_ptr& conn)
    {
        //静态资源请求的处理
        //1.获取到请求uri资源路径,获取客户端请求的页面文件名称
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        //2.组合出文件的实际路径 相对根目录 + uri
        std::string realpath = _web_root + uri;
        //3.如果请求的是个目录,增加一个后缀 login.html
        if(realpath.back() == '/') realpath += "login.html";
        //4.读取文件内容
        Json::Value resp_json;
        std::string body;
        bool ret = file_util::read(realpath, body);
        //  1.文件不存在,读取文件内容失败,返回404
        if(ret == false)
        {
            body = "

404 NOT FOUND

"; conn->set_status(websocketpp::http::status_code::not_found); } //5.设置响应正文 conn->set_status(websocketpp::http::status_code::ok); conn->set_body(body); } void http_resp(server_t::connection_ptr &conn, bool result, websocketpp::http::status_code::value code, const std::string &reason) { Json::Value resp_json; resp_json["result"] = result; resp_json["reason"] = reason; std::string resp_body; json_util::serialize(resp_json, resp_body); conn->set_status(code); conn->set_body(resp_body); conn->append_header("Content-Type", "application/json"); return; } void reg(server_t::connection_ptr& conn) { //用户注册功能请求的处理 websocketpp::http::parser::request req = conn->get_request(); //1. 获取到请求正文 std::string req_body = conn->get_request_body(); //2. 对正文进行json反序列化,得到用户名和密码 Json::Value login_info; bool ret = json_util::unserialize(req_body, login_info); if (ret == false) { DLOG("反序列化注册信息失败"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误"); } //3. 进行数据库的用户新增操作 if (login_info["username"].isNull() || login_info["password"].isNull()) { DLOG("用户名密码不完整"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码"); } ret = _ut.insert(login_info); if (ret == false) { DLOG("向数据库插入数据失败"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名已经被占用!"); } // 如果成功了,则返回200 return http_resp(conn, true, websocketpp::http::status_code::ok, "注册用户成功"); } void login(server_t::connection_ptr& conn) { //用户登录功能请求的处理 //1. 获取请求正文,并进行json反序列化,得到用户名和密码 std::string req_body = conn->get_request_body(); Json::Value login_info; bool ret = json_util::unserialize(req_body, login_info); if (ret == false) { DLOG("反序列化登录信息失败"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误"); } //2. 校验正文完整性,进行数据库的用户信息验证 if (login_info["username"].isNull() || login_info["password"].isNull()) { DLOG("用户名密码不完整"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码"); } ret = _ut.login(login_info); if (ret == false) { // 1. 如果验证失败,则返回400 DLOG("用户名密码错误"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名密码错误"); } //3. 如果验证成功,给客户端创建session uint64_t uid = login_info["id"].asUInt64(); session_ptr ssp = _sm.create_session(uid, LOGIN); if (ssp.get() == nullptr) { DLOG("创建会话失败"); return http_resp(conn, false, websocketpp::http::status_code::internal_server_error , "创建会话失败"); } _sm.set_session_expire_time(ssp->ssid(), SESSION_REMOVE); //4. 设置响应头部:Set-Cookie,将sessionid通过cookie返回 std::string cookie_ssid = "SSID=" + std::to_string(ssp->ssid()); conn->append_header("Set-Cookie", cookie_ssid); return http_resp(conn, true, websocketpp::http::status_code::ok , "登录成功"); } bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val) { // Cookie: SSID=XXX; path=/; //1. 以 ; 作为间隔,对字符串进行分割,得到各个单个的cookie信息 std::string sep = "; "; std::vector cookie_arr; string_util::split(cookie_str, sep, cookie_arr); for (auto str : cookie_arr) { //2. 对单个cookie字符串,以 = 为间隔进行分割,得到key和val std::vector tmp_arr; string_util::split(str, "=", tmp_arr); if (tmp_arr.size() != 2) { continue; } if (tmp_arr[0] == key) { val = tmp_arr[1]; return true; } } return false; } void info(server_t::connection_ptr& conn) { //用户信息获取功能请求的处理 Json::Value err_resp; // 1. 获取请求信息中的Cookie,从Cookie中获取ssid std::string cookie_str = conn->get_request_header("Cookie"); if (cookie_str.empty()) { //如果没有cookie,返回错误:没有cookie信息,让客户端重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到cookie信息,请重新登录"); } // 1.5. 从cookie中取出ssid std::string ssid_str; bool ret = get_cookie_val(cookie_str, "SSID", ssid_str); if (ret == false) { //cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到ssid信息,请重新登录"); } // 2. 在session管理中查找对应的会话信息 session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str)); if (ssp.get() == nullptr) { //没有找到session,则认为登录已经过期,需要重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "登录过期,请重新登录"); } // 3. 从数据库中取出用户信息,进行序列化发送给客户端 uint64_t uid = ssp->get_user(); Json::Value user_info; ret = _ut.selectById(uid, user_info); if (ret == false) { //获取用户信息失败,返回错误:找不到用户信息 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到用户信息,请重新登录"); } std::string body; json_util::serialize(user_info, body); conn->set_body(body); conn->append_header("Content-Type", "application/json"); conn->set_status(websocketpp::http::status_code::ok); // 4. 刷新session的过期时间 _sm.set_session_expire_time(ssp->ssid(), SESSION_REMOVE); } void http_callback(websocketpp::connection_hdl hdl) { server_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl); websocketpp::http::parser::request req = conn->get_request(); std::string method = req.get_method(); std::string uri = req.get_uri(); if(method == "POST" && uri == "/reg") return reg(conn); else if(method == "POST" && uri == "/login") return login(conn); else if(method == "GET" && uri == "/info") return info(conn); else return file_handler(conn); } void ws_resp(server_t::connection_ptr conn, Json::Value &resp) { std::string body; json_util::serialize(resp, body); conn->send(body); } session_ptr get_session_by_cookie(server_t::connection_ptr conn) { Json::Value err_resp; // 1. 获取请求信息中的Cookie,从Cookie中获取ssid std::string cookie_str = conn->get_request_header("Cookie"); if (cookie_str.empty()) { //如果没有cookie,返回错误:没有cookie信息,让客户端重新登录 err_resp["optype"] = "hall_ready"; err_resp["reason"] = "没有找到cookie信息,需要重新登录"; err_resp["result"] = false; ws_resp(conn, err_resp); return session_ptr(); } // 1.5. 从cookie中取出ssid std::string ssid_str; bool ret = get_cookie_val(cookie_str, "SSID", ssid_str); if (ret == false) { //cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录 err_resp["optype"] = "hall_ready"; err_resp["reason"] = "没有找到SSID信息,需要重新登录"; err_resp["result"] = false; ws_resp(conn, err_resp); return session_ptr(); } // 2. 在session管理中查找对应的会话信息 session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str)); if (ssp.get() == nullptr) { //没有找到session,则认为登录已经过期,需要重新登录 err_resp["optype"] = "hall_ready"; err_resp["reason"] = "没有找到session信息,需要重新登录"; err_resp["result"] = false; ws_resp(conn, err_resp); return session_ptr(); } return ssp; } void wsopen_game_hall(server_t::connection_ptr conn) { //游戏大厅长连接建立成功 Json::Value resp_json; //1. 登录验证--判断当前客户端是否已经成功登录 session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { return; } //2. 判断当前客户端是否是重复登录 if (_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())) { resp_json["optype"] = "hall_ready"; resp_json["reason"] = "玩家重复登录!"; resp_json["result"] = false; return ws_resp(conn, resp_json); } //3. 将当前客户端以及连接加入到游戏大厅 _om.enter_game_hall(ssp->get_user(), conn); //4. 给客户端响应游戏大厅连接建立成功 resp_json["optype"] = "hall_ready"; resp_json["result"] = true; ws_resp(conn, resp_json); //5. 记得将session设置为永久存在 _sm.set_session_expire_time(ssp->ssid(), SESSION_FORVER); } void wsopen_game_room(server_t::connection_ptr conn) { Json::Value resp_json; //1. 获取当前客户端的session session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { return; } //2. 当前用户是否已经在在线用户管理的游戏房间或者游戏大厅中---在线用户管理 if (_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())) { resp_json["optype"] = "room_ready"; resp_json["reason"] = "玩家重复登录!"; resp_json["result"] = false; return ws_resp(conn, resp_json); } //3. 判断当前用户是否已经创建好了房间 --- 房间管理 room_ptr rp = _rm.get_room_by_uid(ssp->get_user()); if (rp.get() == nullptr) { resp_json["optype"] = "room_ready"; resp_json["reason"] = "没有找到玩家的房间信息"; resp_json["result"] = false; return ws_resp(conn, resp_json); } //4. 将当前用户添加到在线用户管理的游戏房间中 _om.enter_game_room(ssp->get_user(), conn); //5. 将session重新设置为永久存在 _sm.set_session_expire_time(ssp->ssid(), SESSION_FORVER); //6. 回复房间准备完毕 resp_json["optype"] = "room_ready"; resp_json["result"] = true; resp_json["room_id"] = (Json::UInt64)rp->id(); resp_json["uid"] = (Json::UInt64)ssp->get_user(); resp_json["white_id"] = (Json::UInt64)rp->get_white_user(); resp_json["black_id"] = (Json::UInt64)rp->get_black_user(); return ws_resp(conn, resp_json); } void wsopen_callback(websocketpp::connection_hdl hdl) { //websocket长连接建立成功之后的处理函数 server_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl); websocketpp::http::parser::request req = conn->get_request(); std::string uri = req.get_uri(); if (uri == "/hall") { //建立了游戏大厅的长连接 return wsopen_game_hall(conn); } else if (uri == "/room") { //建立了游戏房间的长连接 return wsopen_game_room(conn); } } void wsclose_game_hall(server_t::connection_ptr conn) { //游戏大厅长连接断开的处理 //1. 登录验证--判断当前客户端是否已经成功登录 session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { return; } //1. 将玩家从游戏大厅中移除 _om.exit_game_hall(ssp->get_user()); //2. 将session恢复生命周期的管理,设置定时销毁 _sm.set_session_expire_time(ssp->ssid(), SESSION_REMOVE); } void wsclose_game_room(server_t::connection_ptr conn) { //获取会话信息,识别客户端 session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { return; } //1. 将玩家从在线用户管理中移除 _om.exit_game_room(ssp->get_user()); //2. 将session回复生命周期的管理,设置定时销毁 _sm.set_session_expire_time(ssp->ssid(), SESSION_REMOVE); //3. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间 _rm.remove_room_user(ssp->get_user()); } void wsclose_callback(websocketpp::connection_hdl hdl) { //websocket连接断开前的处理 server_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl); websocketpp::http::parser::request req = conn->get_request(); std::string uri = req.get_uri(); if (uri == "/hall") { //建立了游戏大厅的长连接 return wsclose_game_hall(conn); } else if (uri == "/room") { //建立了游戏房间的长连接 return wsclose_game_room(conn); } } void wsmsg_game_hall(server_t::connection_ptr conn, server_t::message_ptr msg) { Json::Value resp_json; std::string resp_body; //1. 身份验证,当前客户端到底是哪个玩家 session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { return; } //2. 获取请求信息 std::string req_body = msg->get_payload(); Json::Value req_json; bool ret = json_util::unserialize(req_body, req_json); if (ret == false) { resp_json["result"] = false; resp_json["reason"] = "请求信息解析失败"; return ws_resp(conn, resp_json); } //3. 对于请求进行处理: if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start") { // 开始对战匹配:通过匹配模块,将用户添加到匹配队列中 _mm.add(ssp->get_user()); resp_json["optype"] = "match_start"; resp_json["result"] = true; return ws_resp(conn, resp_json); } else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop") { // 停止对战匹配:通过匹配模块,将用户从匹配队列中移除 _mm.del(ssp->get_user()); resp_json["optype"] = "match_stop"; resp_json["result"] = true; return ws_resp(conn, resp_json); } resp_json["optype"] = "unknow"; resp_json["reason"] = "请求类型未知"; resp_json["result"] = false; return ws_resp(conn, resp_json); } void wsmsg_game_room(server_t::connection_ptr conn, server_t::message_ptr msg) { Json::Value resp_json; //1. 获取客户端session,识别客户端身份 session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { DLOG("房间-没有找到会话信息"); return; } //2. 获取客户端房间信息 room_ptr rp = _rm.get_room_by_uid(ssp->get_user()); if (rp.get() == nullptr) { resp_json["optype"] = "unknow"; resp_json["reason"] = "没有找到玩家的房间信息"; resp_json["result"] = false; DLOG("房间-没有找到玩家房间信息"); return ws_resp(conn, resp_json); } //3. 对消息进行反序列化 Json::Value req_json; std::string req_body = msg->get_payload(); bool ret = json_util::unserialize(req_body, req_json); if (ret == false) { resp_json["optype"] = "unknow"; resp_json["reason"] = "请求解析失败"; resp_json["result"] = false; DLOG("房间-反序列化请求失败"); return ws_resp(conn, resp_json); } DLOG("房间:收到房间请求,开始处理...."); //4. 通过房间模块进行消息请求的处理 return rp->handle_request(req_json); } void wsmsg_callback(websocketpp::connection_hdl hdl, server_t::message_ptr msg) { //websocket长连接通信处理 server_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl); websocketpp::http::parser::request req = conn->get_request(); std::string uri = req.get_uri(); if (uri == "/hall") { //建立了游戏大厅的长连接 return wsmsg_game_hall(conn, msg); }else if (uri == "/room") { //建立了游戏房间的长连接 return wsmsg_game_room(conn, msg); } } public: gobang_server(const std::string &host, const std::string &user, const std::string &passwd, const std::string &dbname, u_int16_t port = 3306, const std::string& wwwroot = WWWROOT): _web_root(wwwroot), _ut(host, user, passwd, dbname, port), _rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om) { _wssrv.set_access_channels(websocketpp::log::alevel::none); _wssrv.init_asio(); _wssrv.set_reuse_addr(true); _wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1)); _wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1)); _wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1)); _wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2)); } void start(int port) { _wssrv.listen(port); _wssrv.start_accept(); _wssrv.run(); } };

4.客⼾端开发

登录⻚⾯: login.html




    
    
    
    登录
    
    


    
    
    <script src="./js/jquery.min.js"></script>
    <script>
        //1. 给按钮添加点击事件,调用登录请求函数
        //2. 封装登录请求函数
        function login() {
            //  1. 获取输入框中的用户名和密码,并组织json对象
            var login_info = {
                username: document.getElementById("user_name").value,
                password: document.getElementById("password").value
            };
            //  2. 通过ajax向后台发送登录验证请求
            $.ajax({
                url: "/login",
                type: "post",
                data: JSON.stringify(login_info),
                success: function(result) {
                    //  3. 如果验证通过,则跳转游戏大厅页面
                    alert("登录成功");
                    window.location.assign("/game_hall.html");
                },
                error: function(xhr) {
                    //  4. 如果验证失败,则提示错误信息,并清空输入框
                    alert(JSON.stringify(xhr));
                    document.getElementById("user_name").value = "";
                    document.getElementById("password").value = "";
                }
            })
        }
    </script>

注册⻚⾯: register.html




    
    
    
    注册
    
    


    
    
    <script src="js/jquery.min.js"></script>
    <script>
        //1. 给按钮添加点击事件,调用注册函数
        //2. 封装实现注册函数
        function reg() {
            //  1. 获取两个输入框空间中的数据,组织成为一个json串
            var reg_info = {
                username: document.getElementById("user_name").value,
                password: document.getElementById("password").value
            };
            console.log(JSON.stringify(reg_info));
            //  2. 通过ajax向后台发送用户注册请求
            $.ajax({
                url : "/reg",
                type : "post",
                data : JSON.stringify(reg_info),
                success : function(res) {
                    if (res.result == false) {
                        //  4. 如果请求失败,则清空两个输入框内容,并提示错误原因
                        document.getElementById("user_name").value = "";
                        document.getElementById("password").value = "";
                        alert(res.reason);
                    }else {
                        //  3. 如果请求成功,则跳转的登录页面
                        alert(res.reason);
                        window.location.assign("/login.html");
                    }
                },
                error : function(xhr) {
                    document.getElementById("user_name").value = "";
                    document.getElementById("password").value = "";
                    alert(JSON.stringify(xhr));
                }
            })
        }
    </script>

游戏⼤厅⻚⾯:game_hall.html




    
    
    
    游戏大厅
    
    


    
    
    
开始匹配
<script src="./js/jquery.min.js"></script> <script> var ws_url = "ws://" + location.host + "/hall"; var ws_hdl = null; window.onbeforeunload = function() { ws_hdl.close(); } //按钮有两个状态:没有进行匹配的状态,正在匹配中的状态 var button_flag = "stop"; //点击按钮的事件处理: var be = document.getElementById("match-button"); be.onclick = function() { if (button_flag == "stop") { //1. 没有进行匹配的状态下点击按钮,发送对战匹配请求 var req_json = { optype: "match_start" } ws_hdl.send(JSON.stringify(req_json)); }else { //2. 正在匹配中的状态下点击按钮,发送停止对战匹配请求 var req_json = { optype: "match_stop" } ws_hdl.send(JSON.stringify(req_json)); } } function get_user_info() { $.ajax({ url: "/info", type: "get", success: function(res) { var info_html = "

" + "用户:" + res.username + " 积分:" + res.score + "
" + "比赛场次:" + res.total_count + " 获胜场次:" + res.win_count + "

"; var screen_div = document.getElementById("screen"); screen_div.innerHTML = info_html; ws_hdl = new WebSocket(ws_url); ws_hdl.onopen = ws_onopen; ws_hdl.onclose = ws_onclose; ws_hdl.onerror = ws_onerror; ws_hdl.onmessage = ws_onmessage; }, error: function(xhr) { alert(JSON.stringify(xhr)); location.replace("/login.html"); } }) } function ws_onopen() { console.log("websocket onopen"); } function ws_onclose() { console.log("websocket onopen"); } function ws_onerror() { console.log("websocket onopen"); } function ws_onmessage(evt) { var rsp_json = JSON.parse(evt.data); if (rsp_json.result == false) { alert(evt.data); location.replace("/login.html"); return; } if (rsp_json["optype"] == "hall_ready") { alert("游戏大厅连接建立成功!"); }else if (rsp_json["optype"] == "match_success") { //对战匹配成功 alert("对战匹配成功,进入游戏房间!"); location.replace("/game_room.html"); }else if (rsp_json["optype"] == "match_start") { console.log("玩家已经加入匹配队列"); button_flag = "start"; be.innerHTML = "匹配中....点击按钮停止匹配!"; return; }else if (rsp_json["optype"] == "match_stop"){ console.log("玩家已经移除匹配队列"); button_flag = "stop"; be.innerHTML = "开始匹配"; return; }else { alert(evt.data); location.replace("/login.html"); return; } } get_user_info(); </script>

游戏房间⻚⾯:game_room.html

在游戏房间⻚⾯中,关于棋盘的绘制部分已经直接提供。




    
    
    
    游戏房间
    
    


    
    
等待玩家连接中...
<script> let chessBoard = []; let BOARD_ROW_AND_COL = 15; let chess = document.getElementById('chess'); let context = chess.getContext('2d');//获取chess控件的2d画布 var ws_url = "ws://" + location.host + "/room"; var ws_hdl = new WebSocket(ws_url); var room_info = null;//用于保存房间信息 var is_me; function initGame() { initBoard(); context.strokeStyle = "#BFBFBF"; // 背景图片 let logo = new Image(); logo.src = "image/sky.jpeg"; logo.onload = function () { // 绘制图片 context.drawImage(logo, 0, 0, 450, 450); // 绘制棋盘 drawChessBoard(); } } function initBoard() { for (let i = 0; i < BOARD_ROW_AND_COL; i++) { chessBoard[i] = []; for (let j = 0; j < BOARD_ROW_AND_COL; j++) { chessBoard[i][j] = 0; } } } // 绘制棋盘网格线 function drawChessBoard() { for (let i = 0; i < BOARD_ROW_AND_COL; i++) { context.moveTo(15 + i * 30, 15); context.lineTo(15 + i * 30, 430); //横向的线条 context.stroke(); context.moveTo(15, 15 + i * 30); context.lineTo(435, 15 + i * 30); //纵向的线条 context.stroke(); } } //绘制棋子 function oneStep(i, j, isWhite) { if (i < 0 || j < 0) return; context.beginPath(); context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI); context.closePath(); var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0); // 区分黑白子 if (!isWhite) { gradient.addColorStop(0, "#0A0A0A"); gradient.addColorStop(1, "#636766"); } else { gradient.addColorStop(0, "#D1D1D1"); gradient.addColorStop(1, "#F9F9F9"); } context.fillStyle = gradient; context.fill(); } //棋盘区域的点击事件 chess.onclick = function (e) { // 1. 获取下棋位置,判断当前下棋操作是否正常 // 1. 当前是否轮到自己走棋了 // 2. 当前位置是否已经被占用 // 2. 向服务器发送走棋请求 if (!is_me) { alert("等待对方走棋...."); return; } let x = e.offsetX; let y = e.offsetY; // 注意, 横坐标是列, 纵坐标是行 // 这里是为了让点击操作能够对应到网格线上 let col = Math.floor(x / 30); let row = Math.floor(y / 30); if (chessBoard[row][col] != 0) { alert("当前位置已有棋子!"); return; } //oneStep(col, row, true); //向服务器发送走棋请求,收到响应后,再绘制棋子 send_chess(row, col); } function send_chess(r, c) { var chess_info = { optype : "put_chess", room_id: room_info.room_id, uid: room_info.uid, row: r, col: c }; ws_hdl.send(JSON.stringify(chess_info)); console.log("click:" + JSON.stringify(chess_info)); } window.onbeforeunload = function() { ws_hdl.close(); } ws_hdl.onopen = function() { console.log("房间长连接建立成功"); } ws_hdl.onclose = function() { console.log("房间长连接断开"); } ws_hdl.onerror = function() { console.log("房间长连接出错"); } function set_screen(me) { var screen_div = document.getElementById("screen"); if (me) { screen_div.innerHTML = "轮到己方走棋..."; }else { screen_div.innerHTML = "轮到对方走棋..."; } } ws_hdl.onmessage = function(evt) { //1. 在收到room_ready之后进行房间的初始化 // 1. 将房间信息保存起来 var info = JSON.parse(evt.data); console.log(JSON.stringify(info)); if (info.optype == "room_ready") { room_info = info; is_me = room_info.uid == room_info.white_id ? true : false; set_screen(is_me); initGame(); }else if (info.optype == "put_chess"){ console.log("put_chess" + evt.data); //2. 走棋操作 // 3. 收到走棋消息,进行棋子绘制 if (info.result == false) { alert(info.reason); return; } //当前走棋的用户id,与我自己的用户id相同,就是我自己走棋,走棋之后,就轮到对方了 is_me = info.uid == room_info.uid ? false : true; set_screen(is_me); //绘制棋子的颜色,应该根据当前下棋角色的颜色确定 isWhite = info.uid == room_info.white_id ? true : false; //绘制棋子 if (info.row != -1 && info.col != -1){ oneStep(info.col, info.row, isWhite); //设置棋盘信息 chessBoard[info.row][info.col] = 1; } //是否有胜利者 if (info.winner == 0) { return; } var screen_div = document.getElementById("screen"); if (room_info.uid == info.winner) { screen_div.innerHTML = info.reason; }else { screen_div.innerHTML = "你输了"; } var chess_area_div = document.getElementById("chess_area"); var button_div = document.createElement("div"); button_div.innerHTML = "返回大厅"; button_div.onclick = function() { ws_hdl.close(); location.replace("/game_hall.html"); } chess_area_div.appendChild(button_div); } else if (info.optype == "chat") { //收到一条消息,判断result,如果为true则渲染一条消息到显示框中 if(info.result == false) { alert(info.reason); return; } var msg_div = document.createElement("p"); msg_div.innerHTML = info.message; if (info.uid == room_info.uid) { msg_div.setAttribute("id", "self_msg"); }else { msg_div.setAttribute("id", "peer_msg"); } var br_div = document.createElement("br"); var msg_show_div = document.getElementById("chat_show"); msg_show_div.appendChild(msg_div); msg_show_div.appendChild(br_div); document.getElementById("chat_input").value = ""; } } //3. 聊天动作 // 1. 捕捉聊天输入框消息 // 2. 给发送按钮添加点击事件,点击俺就的时候,获取到输入框消息,发送给服务器 var cb_div = document.getElementById("chat_button"); cb_div.onclick = function() { var send_msg = { optype : "chat", room_id : room_info.room_id, uid : room_info.uid, message : document.getElementById("chat_input").value }; ws_hdl.send(JSON.stringify(send_msg)); } </script>

我们必须使⽤两个浏览器或者⼀个浏览器的⽆痕模式打开两个标签⻚, 避免cookie和session相互影响导致检测到多开。

5.项⽬扩展

实现局时/步时

局时: ⼀局游戏中玩家能思考的总时间

步时: ⼀步落⼦过程中,玩家能思考的时间

保存棋谱&录像回放

服务器可以把每⼀局对局、玩家轮流落⼦的位置都记录下来

玩家可以在游戏⼤厅⻚⾯选定某个曾经的⽐赛,在⻚⾯上回放出对局的程

观战功能

在游戏⼤厅显⽰当前所有的对局房间

玩家可以选中某个房间以观众的形式加⼊到房间中,实时的看到选⼿的对局情况

虚拟对⼿&⼈机对战

如果当前⻓时间匹配不到选⼿,则⾃动分配⼀个 AI 对⼿, 实现⼈机对战

6.项目页面

注册页面:

登录页面:

游戏大厅页面:

两个浏览器两个账号匹配成功:

两个浏览器两个账号游戏房间页面:(云服务器配置低,带宽低,所以没有加载出来棋盘)

        现在页面还比较简陋,后续会用vue重做页面布局,并且添加项目扩展里面的内容。

posted @ 2026-02-12 17:04  clnchanpin  阅读(7)  评论(0)    收藏  举报