项目篇:服务器模块及客户端的设计与实现

1.通信接口设计

服务器模块是对当前所有已经实现的模块的一个整合,并进行服务的模块。使用websocket库的接口来搭建服务器的基本架构,再实现四个主要的接口:http请求的接口,建立websocket链接请求,断开websocket链接请求,websocket通信请求。

1.1http请求接口

用户在未建立websocket长连接在前需要通过http进行通信,用户的请求一共分为:请求静态资源、注册用户、用户登录,在用户登录成功之后就需要获取游戏大厅的页面以及用户信息和建立游戏大厅的长连接。其中请求静态资源只需要返回页面信息即可,而注册登录以及获取用户信息都需要访问数据库。

其中请求和响应的方式如下。

复制代码
. 注册⻚⾯请求
 请求: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": "⽤户还未登录"}

1.2websocket长连接建立和关闭请求

因为websocket长连接是基于页面的,页面关闭链接就会断开,所以进入无论是建立链接还是关闭链接都需要区分是游戏房间的请求还是游戏大厅的请求。

复制代码
//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
 }

//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
 }

1.3websocket通信

websocket通信又又分为游戏大厅的请求和游戏房间的请求,游戏大厅的请求有:开始匹配、取消匹配,游戏房间的请求有:下棋、聊天。游戏房间的请求在成功完成后还需要广播给房间内的所有玩家。

复制代码
//⾛棋
 {
 "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": "赶紧点"
 }

2.服务器的具体实现

具体如下。

cpp 复制代码
#ifndef __M_SERVER_H__
#define __M_SERVER_H__
#include "room.hpp"
#include "matcher.hpp"
#include "session.hpp"

#define WWWROOT "./wwwroot"
class gobang_server
{
private:
    websocket_server _wssrv;
    std::string _web_root;
    user_table _ut;
    online_manage _om;
    session_manage _sm;
    room_manage _rm;
    match _mm;

private:
    void file_handler(websocket_server::connection_ptr &conn)
    {
        websocketpp::http::parser::request req = conn->get_request();
        std::string method = req.get_method();
        std::string uri = req.get_uri();
        std::string pathname = _web_root + uri;
        if (uri == "/")
        {
            pathname += "auth.html";
        }
        std::string body;
        bool ret = file_util::read(pathname, body);
        if (ret == false)
        {
            body.clear();
            std::string x = _web_root + "404.html";
            file_util::read(x, body);
            conn->set_status(websocketpp::http::status_code::not_found);
        }
        else
        {
            conn->set_status(websocketpp::http::status_code::ok);
        }
        conn->set_body(body);
    }
    void login(websocket_server::connection_ptr &conn)
    {
        websocketpp::http::parser::request req = conn->get_request();
        Json::Value login_json;
        // 获取正文
        std::string req_body = conn->get_request_body();
        // 反序列化得到用户名和密码
        bool ret = json_util::Unserialization(req_body, login_json);
        if (ret == false)
        {
            DLOG("反序列化失败");
            return http_resp(false, websocketpp::http::status_code::bad_request, "请求格式错误!", conn);
        }
        if (login_json["username"].isNull() || login_json["password"].isNull())
        {
            DLOG("用户提交信息不完全");
            return http_resp(false, websocketpp::http::status_code::bad_request, "请输入用户名/密码!", conn);
        }
        ret = _ut.login(login_json);
        if (ret == false)
        {
            DLOG("用户输入错误用户名/命名");
            return http_resp(false, websocketpp::http::status_code::bad_request, "用户名/密码错误!", conn);
        }
        uint64_t uid = login_json["id"].asUInt64();
        session_ptr ssp = _sm.create_session(uid, ONLOGIN);
        if (ssp.get() == nullptr)
        {
            DLOG("创建会话失败");
            return http_resp(false, websocketpp::http::status_code::internal_server_error, "创建会话失败!", conn);
        }
        _sm.set_session_time(ssp->get_ssid(), SESSION_TIME);
        std::string cokie = "SSID=" + std::to_string(ssp->get_ssid()) + ";Path=/";
        conn->append_header("Set-Cookie", cokie);
        return http_resp(true, websocketpp::http::status_code::ok, "登陆成功", conn);
    }
    void http_resp(bool ret, websocketpp::http::status_code::value code,
                   const std::string &reason, websocket_server::connection_ptr &conn)
    {
        Json::Value resp_json;
        resp_json["result"] = ret;
        resp_json["reason"] = reason;
        std::string resp_body;
        json_util::Serialization(resp_json, resp_body);
        conn->set_body(resp_body);
        conn->set_status(code);
        conn->append_header("Content-Type", "application/json;charset=utf-8");
        return;
    }
    void reg(websocket_server::connection_ptr &conn)
    {
        websocketpp::http::parser::request req = conn->get_request();
        Json::Value login_json;
        // 获取正文
        std::string req_body = conn->get_request_body();
        // 反序列化得到用户名和密码
        bool ret = json_util::Unserialization(req_body, login_json);
        if (ret == false)
        {
            DLOG("反序列化失败");
            return http_resp(false, websocketpp::http::status_code::bad_request, "请求格式错误!", conn);
        }
        if (login_json["username"].isNull() || login_json["password"].isNull())
        {
            DLOG("用户提交信息不完全");
            return http_resp(false, websocketpp::http::status_code::bad_request, "请输入用户名/密码!", conn);
        }
        ret = _ut.insert(login_json);
        if (ret == false)
        {
            DLOG("插入数据库失败");
            return http_resp(false, websocketpp::http::status_code::bad_request, "用户名被占用!", conn);
        }
        return http_resp(true, websocketpp::http::status_code::ok, "注册成功!", conn);
    }
    bool get_cookie_val(const std::string &cookie, const std::string &key, std::string &val)
    {
        std::string tmp_cookie = cookie;
        // 把所有";"替换成"; ",解决分号无空格分割失败
        size_t pos = 0;
        while ((pos = tmp_cookie.find(';', pos)) != std::string::npos)
        {
            tmp_cookie.insert(pos + 1, " ");
            pos += 2;
        }

        std::string sep = "; ";
        std::vector<std::string> cookie_arr;
        string_util::split(tmp_cookie, sep, cookie_arr);

        auto trim = [](std::string &s)
        {
            s.erase(0, s.find_first_not_of(" \t"));
            s.erase(s.find_last_not_of(" \t") + 1);
        };

        for (auto &m : cookie_arr)
        {
            std::vector<std::string> tem;
            string_util::split(m, "=", tem);
            if (tem.size() != 2)
                continue;

            trim(tem[0]);
            trim(tem[1]);

            if (tem[0] == key)
            {
                val = tem[1];
                return true;
            }
        }
        return false;
    }

    void info(websocket_server::connection_ptr &conn)
    {
        // 获取cookie信息
        std::string cookie_str = conn->get_request_header("Cookie");
        if (cookie_str.empty())
        {
            DLOG("找不到cookie信息");
            return http_resp(false, websocketpp::http::status_code::bad_request, "找不到cookie信息,请重新登录!", conn);
        }
        // 获取ssid
        std::string ssid_str;
        bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);
        if (ret == false)
        {
            DLOG("找不到ssid");
            return http_resp(false, websocketpp::http::status_code::bad_request, "找不到ssid信息,请重新登录!", conn);
        }
        // 查找session
        session_ptr ssp = _sm.get_session_by_ssid(std::stoll(ssid_str));
        if (ssp.get() == nullptr)
        {
            DLOG("session过期");
            return http_resp(false, websocketpp::http::status_code::bad_request, "登录过期,请重新登录!", conn);
        }
        // 查询用户信息
        Json::Value root;
        uint64_t uid = ssp->get_uid();
        ret = _ut.selete_by_id(uid, root);
        if (ret == false)
        {
            return http_resp(false, websocketpp::http::status_code::bad_request, "找不到用户信息,请重新登录!", conn);
        }
        // 返回用户信息
        std::string body;
        json_util::Serialization(root, body);
        conn->set_status(websocketpp::http::status_code::ok);
        conn->set_body(body);
        conn->append_header("Content-Type", "application/json;charset=utf-8");
        // 刷新过期时间
        _sm.set_session_time(ssp->get_ssid(), SESSION_TIME);
    }
    void http_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::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 == "/login")
        {
            return login(conn);
        }
        else if (method == "POST" && uri == "/reg")
        {
            return reg(conn);
        }
        else if (method == "GET" && uri == "/info")
        {
            return info(conn);
        }
        else
        {
            return file_handler(conn);
        }
    }
    void ws_resp(websocket_server::connection_ptr conn, Json::Value &resp)
    {
        std::string body;
        json_util::Serialization(resp, body);
        conn->send(body);
    }
    session_ptr get_session_by_cookie(websocket_server::connection_ptr &conn)
    {
        Json::Value err_resp;
        // 获取cookie信息
        std::string cookie_str = conn->get_request_header("Cookie");
        if (cookie_str.empty())
        {
            DLOG("找不到cookie信息");
            err_resp["optype"] = "hall_ready";
            err_resp["reason"] = "找不到cookie信息,请重新登录!";
            err_resp["result"] = false;
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        // 获取ssid
        std::string ssid_str;
        bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);
        if (ret == false)
        {
            DLOG("找不到ssid");
            err_resp["optype"] = "hall_ready";
            err_resp["reason"] = "找不到SSID信息,请重新登录!";
            err_resp["result"] = false;
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        // 查找session
        session_ptr ssp = _sm.get_session_by_ssid(std::stoll(ssid_str));
        if (ssp.get() == nullptr)
        {
            DLOG("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 wopen_hall(websocket_server::connection_ptr conn)
    {
        Json::Value err_resp;
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 是否重复登录
        if (_om.is_in_game_hall(ssp->get_uid()) || _om.is_in_game_room(ssp->get_uid()))
        {
            err_resp["optype"] = "hall_ready";
            err_resp["reason"] = "玩家重复登录!";
            err_resp["result"] = false;
            return ws_resp(conn, err_resp);
        }
        // 将客户端及其链接添加到游戏大厅
        _om.enter_game_hall(ssp->get_uid(), conn);
        Json::Value resp_json;
        resp_json["optype"] = "hall_ready";
        resp_json["result"] = true;
        ws_resp(conn, resp_json);
        // 就以后session设置为永久存在
        _sm.set_session_time(ssp->get_ssid(), SESSION_PERMANENT);
    }
    void wopen_room(websocket_server::connection_ptr conn)
    {
        // 获取用户session
        Json::Value resp_json;
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 是否重复登录
        if (_om.is_in_game_hall(ssp->get_uid()) || _om.is_in_game_room(ssp->get_uid()))
        {
            DLOG("玩家重复登录!");
            resp_json["optype"] = "room_ready";
            resp_json["reason"] = "玩家重复登录!";
            resp_json["result"] = false;
            return ws_resp(conn, resp_json);
        }
        // 是否已经有房间
        room_ptr rm = _rm.get_room_by_uid(ssp->get_uid());
        if (rm.get() == nullptr)
        {
            DLOG("找不到玩家房间信息!");
            resp_json["optype"] = "room_ready";
            resp_json["reason"] = "找不到玩家房间信息!";
            resp_json["result"] = false;
            return ws_resp(conn, resp_json);
        }
        // 添加到在线用户房间中
        _om.enter_game_room(ssp->get_uid(), conn);
        // 设置session永久存在
        _sm.set_session_time(ssp->get_ssid(), SESSION_PERMANENT);
        resp_json["optype"] = "room_ready";
        resp_json["result"] = true;
        resp_json["room_id"] = (Json::UInt64)rm->get_room_id();
        resp_json["uid"] = (Json::UInt64)ssp->get_uid();
        resp_json["white_id"] = (Json::UInt64)rm->get_white_id();

        return ws_resp(conn, resp_json);
    }
    void wopen_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::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 wopen_hall(conn);
        }
        else if (uri == "/room")
        {
            // 建立游戏房间长连接
            return wopen_room(conn);
        }
    }
    void wclose_game_hall(websocket_server::connection_ptr conn)
    {
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 将玩家从游戏大厅中移除
        _om.exit_game_hall(ssp->get_uid());
        // 设置session过期时间
        _sm.set_session_time(ssp->get_ssid(), SESSION_TIME);
    }
    void wclose_game_room(websocket_server::connection_ptr conn)
    {
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 将玩家从游戏房间在线用户列表中移除
        _om.exit_game_room(ssp->get_uid());
        // 设置session永不过期
        _sm.set_session_time(ssp->get_ssid(), SESSION_TIME);
        // 减少房间人数
        _rm.remove_room_user(ssp->get_uid());
    }
    void wclose_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::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 wclose_game_hall(conn);
        }
        else if (uri == "/room")
        {
            // 断开游戏房间长连接
            return wclose_game_room(conn);
        }
    }
    void wmessage_game_hall(websocket_server::connection_ptr conn, websocket_server::message_ptr msg)
    {
        // 验证身份
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 获取请求信息
        std::string resp_str = msg->get_payload();
        Json::Value resp_json;
        bool ret = json_util::Unserialization(resp_str, resp_json);
        if (ret == false)
        {
            resp_json["reasont"] = "请求解析失败";
            resp_json["result"] = false;
            return ws_resp(conn, resp_json);
        }
        if (!resp_json["optype"].isNull() && resp_json["optype"].asString() == "match_start")
        {
            // 将玩家加入匹配队列
            DLOG("处理了匹配消息");
            _mm.add(ssp->get_uid());
            resp_json["optype"] = "match_start";
            resp_json["result"] = true;
            return ws_resp(conn, resp_json);
        }
        else if (!resp_json["optype"].isNull() && resp_json["optype"].asString() == "match_stop")
        {
            // 玩家从匹配队列中移除
            DLOG("处理了取消匹配消息");
            _mm.del(ssp->get_uid());
            resp_json["optype"] = "match_stop";
            resp_json["result"] = true;
            return ws_resp(conn, resp_json);
        }
        resp_json["optype"] = "unknown";
        resp_json["result"] = false;
        return ws_resp(conn, resp_json);
    }
    void wmessage_game_room(websocket_server::connection_ptr conn, websocket_server::message_ptr msg)
    {
        DLOG("wmessage_game_room 开始处理");
        Json::Value resp_json;
        // 获取session信息
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        DLOG("wmessage_game_room: uid=%lu", ssp->get_uid());
        //获取房间信息
        room_ptr rm = _rm.get_room_by_uid(ssp->get_uid());
        if (rm.get() == nullptr)
        {
            DLOG("找不到玩家房间信息!");
            resp_json["optype"] = "unknown";
            resp_json["reason"] = "找不到玩家房间信息!";
            resp_json["result"] = false;
            return ws_resp(conn, resp_json);
        }
        DLOG("找到房间: room_id=%lu", rm->get_room_id());
        // 获取请求信息
        std::string resp_str = msg->get_payload();
        DLOG("收到房间消息: %s", resp_str.c_str());
        bool ret = json_util::Unserialization(resp_str, resp_json);
        if(ret == false)
        {
            DLOG("请求解析失败!");
            resp_json["optype"] = "unknown";
            resp_json["reason"] = "请求解析失败";
            resp_json["result"] = false;
            return ws_resp(conn, resp_json);
        }
        DLOG("调用 handle_request, optype=%s", resp_json["optype"].asString().c_str());
        return rm->handle_request(resp_json);
    }
    void wmessage_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg)
    {
        websocket_server::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")
        {
            // 收到游戏大厅消息
            DLOG("收到游戏大厅消息");
            return wmessage_game_hall(conn, msg);
        }
        else if (uri == "/room")
        {
            // 收到游戏房间消息
            DLOG("收到游戏房间消息");
            return wmessage_game_room(conn,msg);
        }
    }

public:
    gobang_server(const std::string &host,
                  const std::string &user,
                  const std::string &password,
                  const std::string &db, unsigned int port = 3306,
                  const std::string &wwwroot = WWWROOT)
        : _web_root(wwwroot), _ut(host, user, password, db, port), _sm(&_wssrv), _rm(&_ut, &_om), _mm(&_ut, &_om, &_rm)
    {
        _wssrv.set_access_channels(websocketpp::log::alevel::none);
        _wssrv.init_asio();
        _wssrv.set_reuse_addr(true);
        _wssrv.set_open_handler(std::bind(&gobang_server::wopen_callback, this, std::placeholders::_1));
        _wssrv.set_close_handler(std::bind(&gobang_server::wclose_callback, this, std::placeholders::_1));
        _wssrv.set_message_handler(std::bind(&gobang_server::wmessage_callback, this, std::placeholders::_1, std::placeholders::_2));
        _wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));
    }
    void start(int port)
    {
        _wssrv.listen(port);
        _wssrv.start_accept();
        _wssrv.run();
    }
};

#endif

3.客户端

用户认证模块支持登录和注册功能,包含表单非空校验,登录与注册表单支持Tab切换,登录成功后跳转游戏大厅,注册成功后提示返回登录。

游戏大厅模块展示用户信息,包括用户名、积分、总场次和胜场数,支持开始匹配和取消匹配,WebSocket连接具备自动重连机制,匹配成功后自动进入游戏房间。

游戏房间模块提供15×15标准五子棋棋盘,使用Canvas渲染,支持黑白棋子交替落子,回合制对战明确提示当前轮到谁落子,具备胜负判定功能。房间内还包含实时聊天功能,消息区分己方和对方,对手掉线时自动判胜,连接断开时提供返回大厅入口。

3.1具体实现

因为前端都是实现一些简单的功能加上字数过多的缘故就不直接发出来了,可以去文档git里查看。5月/26/SourceCode/server.hpp · 浪客灿心/阿灿 - 码云 - 开源中国