网络版五子棋C++实现

news2024/11/17 15:42:09

目录

1.项目介绍

2.开发环境

3.核心技术

4.环境搭建

5.WebSocketpp介绍

5.1WebSocketpp是什么

5.2为什么使用WebSocketpp

5.3原理解析:

5.4WebSocketpp主要特性

6.WebSocketpp使用

7.JsonCpp使用

8.MySQL API

9.项目模块设计以及流程图

10.封装日志宏

11.封装util工具类

12.数据管理模块的实现

12.1数据库的设计

12.2数据库类(user_table)的设计

13.在线用户管理模块

14.游戏房间管理模块

14.1游戏房间类的设计

14.2游戏房间管理类的设计

15.session管理模块

15.1session是什么

15.2session工作原理

15.3session模块设计

15.4session管理模块设计

16.玩家匹配管理模块

16.1玩家匹配队列

16.2玩家匹配管理模块

17.服务器模块

17.1通信接口的设计(协议定制)

17.1.1静态资源的请求

17.1.2注册用户

17.1.3用户登录

17.1.4获取客户端信息

17.1.5websocket长连接协议切换请求(进入游戏大厅)

17.1.6开始对战匹配

17.1.7停⽌匹配

17.1.8websocket长连接协议切换请求(进入游戏房间)

17.1.9走棋

17.1.9聊天

17.2服务器模块实现

18.前端模块

18.1注册页面

18.2登录页面

18.3游戏大厅页面

18.4游戏房间页面

19.项目扩展

20.项目源码


1.项目介绍

此项目实现了一个网页版的五子棋对战的游戏,玩家可以通过浏览器访问服务器来实现人与人之间的对战,主要支持一下功能:

1.用户管理:实现用户注册,用户登录,展示用户的信息包括分数,比赛场次等

2.匹配对战:两个用户在相同的分数段进行匹配对战,胜利一方加分,失败扣分

3.聊天功能:实现两个玩家在下棋的同时还能支持实时聊天的功能

项目成果展示:

注册页面:


登录页面:

登录成功之后游戏大厅页面:

匹配进入游戏房间并进行下棋和聊天的功能展示:

比赛获胜以及对局中实时聊天展示:

2.开发环境

Linux(Centos-7.6)

VSCode/Vim

g++/gdb
Makefile

3.核心技术

HTTP/WebSocket  用于网络通信
Websocketpp   用于服务器可以主动向客户端发送信息
JsonCpp     用于序列化和反序列化
Mysql      用于保存用户的数据
C++11      包装器&bind&智能指针等的使用
BlockQueue        使用list来封装,原因后面讲述
HTML/CSS/JS/AJAX       用于前端页面的展示

4.环境搭建

通过yum来安装各种需要使用到的工具:
gcc/g++编译器、gdb调试器、git、boost库、jsoncpp库、MySQL、cmake工具

5.WebSocketpp介绍

5.1WebSocketpp是什么

WebSocket是从HTML5开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制。

5.2为什么使用WebSocketpp

传统的web程序都是属于"⼀问⼀答"的形式,即客⼾端给服务器发送了⼀个HTTP请求,服务器给客⼾端返回⼀个HTTP响应。这种情况下服务器是属于被动的⼀⽅,如果客⼾端不主动发起请求服务器就⽆法主动给客户端响应。
像⽹⻚即时聊天或者我们做的五⼦棋游戏这样的程序都是⾮常依赖"消息推送"的,即需要服务器主动推动消息到客⼾端。
如果只是使⽤原⽣的HTTP协议,要想实现消息推送⼀般需要通过"轮询"的⽅式实现,⽽轮询的成本⽐较⾼并且也不能及时的获取到消息的响应
基于上述两个问题,就产⽣了WebSocket协议。WebSocket更接近于TCP这种级别的通信⽅式,
⼀旦连接建⽴完成客⼾端或者服务器都可以主动的向对⽅发送数据

这是http下的通信模式:

这是websocket的通信模式:

从上图就可以知道websocket下的通信方式是比http的效率高很多的

5.3原理解析:

WebSocket协议本质上是⼀个基于TCP的协议。为了建⽴⼀个WebSocket连接,客户端浏览器⾸先要向服务器发起⼀个HTTP请求,这个请求和通常的HTTP请求不同,包含了⼀些附加头信息,通过这个附加头信息完成握⼿过程并升级协议的过程

协议升级流程:

5.4WebSocketpp主要特性

事件驱动的接口
⽀持HTTP/HTTPS、WS/WSS、IPv6
灵活的依赖管理—Boost库/C++11标准库
可移植性:Posix/Windows、32/64bit、Intel/ARM
线程安全

6.WebSocketpp使用

6.1常见接口使用:
 

namespace websocketpp {
 typedef lib::weak_ptr<void> connection_hdl;
 
 template <typename config>
 class endpoint : public config::socket_type {
 typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;
 typedef typename connection_type::ptr connection_ptr;
 typedef typename connection_type::message_ptr message_ptr;
 
 typedef lib::function<void(connection_hdl)> open_handler;
 typedef lib::function<void(connection_hdl)> close_handler;
 typedef lib::function<void(connection_hdl)> http_handler;
 typedef lib::function<void(connection_hdl,message_ptr)> 
message_handler;
 /* websocketpp::log::alevel::none 禁⽌打印所有⽇志*/
 void set_access_channels(log::level channels);/*设置⽇志打印等级*/
 void clear_access_channels(log::level channels);/*清除指定等级的⽇志*/ /*设置指定事件的回调函数*/
 void set_open_handler(open_handler h);/*websocket握⼿成功回调处理函数*/ void set_close_handler(close_handler h);/*websocket连接关闭回调处理函数*/ void set_message_handler(message_handler h);/*websocket消息回调处理函数*/ void set_http_handler(http_handler h);/*http请求回调处理函数*/
 /*发送数据接⼝*/
 void send(connection_hdl hdl, std::string& payload, 
frame::opcode::value op);
 void send(connection_hdl hdl, void* payload, size_t len, 
frame::opcode::value op);
 /*关闭连接接⼝*/
 void close(connection_hdl hdl, close::status::value code, std::string& 
reason);
 /*获取connection_hdl 对应连接的connection_ptr*/
 connection_ptr get_con_from_hdl(connection_hdl hdl);
 /*websocketpp基于asio框架实现,init_asio⽤于初始化asio框架中的io_service调度器*/
 void init_asio();
 /*设置是否启⽤地址重⽤*/
 void set_reuse_addr(bool value);
 /*设置endpoint的绑定监听端⼝*/
 void listen(uint16_t port);
 /*对io_service对象的run接⼝封装,⽤于启动服务器*/
 std::size_t run();
 /*websocketpp提供的定时器,以毫秒为单位*/
 timer_ptr set_timer(long duration, timer_handler callback);
 };
 template <typename config>
 class server : public endpoint<connection<config>,config> {
 /*初始化并启动服务端监听连接的accept事件处理*/
 void start_accept();
 };
 
 template <typename config>
 class connection
 : public config::transport_type::transport_con_type
 , public config::connection_base
 {
 /*发送数据接⼝*/
 error_code send(std::string&payload, frame::opcode::value 
op=frame::opcode::text);
 /*获取http请求头部*/
 std::string const & get_request_header(std::string const & key)
 /*获取请求正⽂*/
 std::string const & get_request_body();
 /*设置响应状态码*/
 void set_status(http::status_code::value code);
 /*设置http响应正⽂*/
 void set_body(std::string const & value);
 /*添加http响应头部字段*/
 void append_header(std::string const & key, std::string const & val);
 /*获取http请求对象*/
 request_type const & get_request();
 /*获取connection_ptr 对应的 connection_hdl */
 connection_hdl get_handle();
 };
 
 namespace http {
 namespace parser {
 class parser {
 std::string const & get_header(std::string const & key)
 }
 class request : public parser {
 /*获取请求⽅法*/
 std::string const & get_method()
 /*获取请求uri接⼝*/
 std::string const & get_uri()
 };
 }};
 
 namespace message_buffer {
 /*获取websocket请求中的payload数据类型*/
 frame::opcode::value get_opcode();
 /*获取websocket中payload数据*/
 std::string const & get_payload();
 };
 
 namespace http {
 namespace status_code {
 enum value {
 uninitialized = 0,
 continue_code = 100,
 switching_protocols = 101,
 ok = 200,
 created = 201,
 accepted = 202,
 non_authoritative_information = 203,
 no_content = 204,
 reset_content = 205,
 partial_content = 206,
 multiple_choices = 300,
 moved_permanently = 301,
 found = 302,
 see_other = 303,
 not_modified = 304,
 use_proxy = 305,
 temporary_redirect = 307,
 bad_request = 400,
 unauthorized = 401,
 payment_required = 402,
 forbidden = 403,
 not_found = 404,
 method_not_allowed = 405,
 not_acceptable = 406,
 proxy_authentication_required = 407,
 request_timeout = 408,
 conflict = 409,
 gone = 410,
 length_required = 411,
 precondition_failed = 412,
 request_entity_too_large = 413,
 request_uri_too_long = 414,
 unsupported_media_type = 415,
 request_range_not_satisfiable = 416,
 expectation_failed = 417,
 im_a_teapot = 418,
 upgrade_required = 426,
 precondition_required = 428,
 too_many_requests = 429,
 request_header_fields_too_large = 431,
 internal_server_error = 500,
 not_implemented = 501,
 bad_gateway = 502,
 service_unavailable = 503,
 gateway_timeout = 504,
 http_version_not_supported = 505,
 not_extended = 510,
 network_authentication_required = 511
 };}}
}

这是我们之后使用到的主要的接口,其中status_code中主要使用到的是ok:200 、bad_request:400 、not found:404

通过了解上面的接口,我们可以做出一个简单的服务器:

实现步骤:
1.创建服务器,设置日志等级,设置调度器asio,然后设置各个websocket的各个回调函数,

因为传入的是一个包装器,所以这里需要使用的是C++11中bind开绑定参数解决。例如连接成功和断开以及http返回页面还有信息的处理(这里简单处理,用于测试)

2.监听,建立新连接,启动服务器
 

#include <iostream>
#include <string>
#include <functional>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
using namespace std;

using wsserver_t = websocketpp::server<websocketpp::config::asio>;

void http_callback(wsserver_t* wsserver,websocketpp::connection_hdl hdl)
{
    //给用户返回一个hello world界面
    wsserver_t::connection_ptr conn = wsserver->get_con_from_hdl(hdl);
    std::cout<<"body: "<<conn->get_request_body()<<std::endl;
    websocketpp::http::parser::request req = conn->get_request();
    std::cout<<"uri: "<<req.get_uri()<<std::endl;
    std::cout<<"method: "<<req.get_method()<<std::endl;
    std::string body = "<html><body><h1>Hello World</h1></body></html>";
    conn->set_body(conn->get_request_body());
    //conn->set_body(body);
    //conn->append_header("Content-Type","text/html");
    conn->set_status(websocketpp::http::status_code::ok);
}
void open_callback(wsserver_t* wsserver,websocketpp::connection_hdl hdl)
{
    cout<<"握手成功"<<endl;
}

void close_callback(wsserver_t* wsserver,websocketpp::connection_hdl hdl)
{
    cout<<"连接断开"<<endl;
}

void message_callback(wsserver_t* wsserver,websocketpp::connection_hdl hdl,wsserver_t::message_ptr msg)
{
    wsserver_t::connection_ptr conn = wsserver->get_con_from_hdl(hdl);
    std::cout<<"wsserver message: "<<msg->get_payload()<<std::endl;
    std::string resp = "client say: "+msg->get_payload();
    conn->send(resp,websocketpp::frame::opcode::text);
}

int main()
{
    //使用websocket创建服务器
    wsserver_t wsserver;
    //设置日志等级
    wsserver.set_access_channels(websocketpp::log::alevel::none);
    //设置调度器asio
    wsserver.init_asio();
    //设置回调函数
    wsserver.set_http_handler(bind(http_callback,&wsserver,std::placeholders::_1));
    wsserver.set_open_handler(bind(open_callback,&wsserver,std::placeholders::_1));
    wsserver.set_close_handler(bind(close_callback,&wsserver,std::placeholders::_1));
    wsserver.set_message_handler(bind(message_callback,&wsserver,std::placeholders::_1,std::placeholders::_2));
    //监听
    wsserver.listen(3389);
    //建立新连接
    wsserver.start_accept();
    //启动服务器
    wsserver.run();
    return 0;
}

我们很容易就可以获得到通过上述代码获取到一个我们返回的html页面

html代码:

<meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Test Websocket</title>
 </head>
 <body>
 <input type="text" id="message">
 <button id="submit">提交</button>
 <script>
 let websocket = new WebSocket("ws://43.139.37.242:3389");
 
 // 处理连接打开的回调函数
 websocket.onopen = function() {
 console.log("连接建立");
 }
 // 处理收到消息的回调函数
 // 控制台打印消息
 websocket.onmessage = function(e) {
 console.log("收到消息: " + e.data);
 }
 // 处理连接异常的回调函数
 websocket.onerror = function() {
 console.log("连接异常");
 }
 // 处理连接关闭的回调函数
 websocket.onclose = function() {
 console.log("连接关闭");
 }
 // 实现点击按钮后, 通过 websocket实例 向服务器发送请求
 let input = document.querySelector('#message');
 let button = document.querySelector('#submit');
 button.onclick = function() {
 console.log("发送消息: " + input.value);
 websocket.send(input.value);
 }
 </script>
</body>
</html>

7.JsonCpp使用

Json是什么:Json 是⼀种数据交换格式,它采⽤完全独⽴于编程语⾔的⽂本格式来存储和表⽰数据。

数据存储格式:
对象:使⽤花括号 {} 括起来的表⽰⼀个对象
数组:使⽤中括号 [] 括起来的表⽰⼀个数组
字符串:使⽤常规双引号 "" 括起来的表⽰⼀个字符串
数字:包括整形和浮点型,直接使⽤

JsonCpp库中提供的数据对象表示:

class Json::Value{
 Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
 Value& operator[](const std::string& key);//简单的⽅式完成 val["name"] = "xx";
 Value& operator[](const char* key);
 Value removeMember(const char* key);//移除元素
 const Value& operator[](ArrayIndex index) const; //val["score"][0]
 Value& append(const Value& value);//添加数组元素val["score"].append(88);  ArrayIndex size() const;//获取数组元素个数 val["score"].size();
 bool isNull(); //⽤于判断是否存在某个字段
 std::string asString() const;//转string string name = 
val["name"].asString();
 const char* asCString() const;//转char* char *name = 
val["name"].asCString();
 Int asInt() const;//转int int age = val["age"].asInt();
 float asFloat() const;//转float float weight = val["weight"].asFloat(); bool asBool() const;//转 bool bool ok = val["ok"].asBool();
};

JsonCpp提供的序列化和反序列化接口:
序列化接口:

class JSON_API StreamWriter {
 virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
 virtual StreamWriter* newStreamWriter() const;
}

反序列化接口:

class JSON_API CharReader {
 virtual bool parse(char const* beginDoc, char const* endDoc, 
 Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
 virtual CharReader* newCharReader() const;
}

测试用例编写:

序列化步骤:构造一个streamwritebuilder对象来构造一个streamwrite对象,通过steamwrite中的write函数进行序列化
反序列化步骤:构造一个charreadbuilder对象来构造一个charread对象,通过steamwrite中的read函数进行反序列化最后输出结果(如果是数组要获取它的长度再进行打印)

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <jsoncpp/json/json.h>

std::string serialize()
{
    // 使用json::value对象存储数据
    Json::Value root;
    root["姓名"] = "小明";
    root["年龄"] = 20;
    root["成绩"].append(90);
    root["成绩"].append(80);
    root["成绩"].append(70);
    // 构建一个StreamWriterBuilder对象
    Json::StreamWriterBuilder swb;
    // 通过StreamWriterBuilder来构建一个StreamWriter对象
    Json::StreamWriter *sw = swb.newStreamWriter();
    // 使用json进行序列化
    std::stringstream ss;
    int ret = sw->write(root, &ss);
    if (ret != 0)
    {
        std::cerr << "json serilize failed!" << std::endl;
        return "";
    }
    std::cout << "序列化结果: " << ss.str() << std::endl;
    delete sw;
    return ss.str();
}

//反序列化
void unserialize(std::string& s)
{
    //构建一个CharReaderBuilder对象
    Json::CharReaderBuilder crb;
    //通过CharReaderBuilder构建出一个CharReader对象
    Json::CharReader* cb = crb.newCharReader();
    //构建json::value对象来接受反序列化结果
    Json::Value root;
    std::string err;
    //反序列化
    bool ret = cb->parse(s.c_str(),s.c_str()+s.size(),&root,&err);
    if(ret == false)
    {
        std::cerr<<"unserialize failed!"<<std::endl;
        return;
    }
    //输出结果 
    std::cout<<"姓名"<<root["姓名"].asString()<<std::endl;
    std::cout<<"年龄"<<root["年龄"].asInt()<<std::endl;
    int sz = root["成绩"].size();
    for(int i = 0;i<sz;++i)
    {
        std::cout<<"成绩"<<root["成绩"][i]<<std::endl;
    }
    delete cb;
}



int main()
{
    std::string s = serialize();
    unserialize(s);
    return 0;
}

结果:

8.MySQL API

MySQL 是 C/S 模式, C API 其实就是⼀个 MySQL 客⼾端,提供⼀种⽤ C 语⾔代码操作数据库的流程

其中的主要操作:
1.初始化MySQL操作句柄

2.连接服务器,对各个数据进行传入

3.设置字符集,为了保证编码格式相同,统一使用utf8编码格式

4.进行MySQL的操作,这里增删改操作都是对数据库的修改,我们可以通过数据库来查看的到结果,但是查就不行了,因为查是需要保存结果到本地的

5.如果有查,那么就要保存结果到本地,然后获取本地行数和列数,然后进行本地的查看即可

6.最后一定要记得关闭MySQL操作句柄,以防止资源泄露。

#include <stdio.h>
#include <string.h>
#include <mysql/mysql.h>

//定义连接服务器所需要的宏
#define HOST "127.0.0.1"
#define USER "root"
#define PASSWD NULL
#define DBNAME "gobang"
#define PORT 3306


int main()
{
    //1.初始化mysql操作句柄
    MYSQL* mysql = mysql_init(NULL);
    if(mysql == NULL)
    {
        printf("mysql init fail\n");
        mysql_close(mysql);
        return -1;
    }
    //2.连接服务器
    if(mysql_real_connect(mysql,HOST,USER,PASSWD,DBNAME,PORT,NULL,0) == NULL)
    {
        printf("mysql connect fail\n");
        mysql_close(mysql);
        return -1;
    }
    //3.设置字符集
    if(mysql_set_character_set(mysql,"utf8") != 0)
    {
        printf("set character fail\n");
        mysql_close(mysql);
        return -1;
    }   
    //4.连接数据库,在第二步已经完成
    //5.进行数据库的增删改操作
    const char* sql = "insert into stu values(1,18,'曹操')";
    //const char* sql = "update stu set age=20 where id = 1";
    //const char* sql = "delete from stu where id = 1";
    //const char* sql = "select * from stu";
    //const char* sql = "select * from user";
    int ret = mysql_query(mysql,sql);
    if(ret != 0)
    {
        printf("%s ",sql);
        printf("mysql query fail: %s",mysql_errno(mysql));
        mysql_close(mysql);
        return -1;
    }
    // //6.如果有读,那么就先保存查询结果到本地
    // MYSQL_RES* res = mysql_store_result(mysql);
    // if(res == NULL)
    // {
    //     printf("mysql store result fail\n");
    //     mysql_close(mysql);
    //     return -1;
    // }
    // //7.获取结果的行列数
    // int row = mysql_num_rows(res);
    // int field = mysql_num_fields(res);
    // //8.遍历结果集,得到结果
    // for(int i = 0;i<row;++i)
    // {
    //     //获取列
    //     MYSQL_ROW rows= mysql_fetch_row(res);
    //     for(int j = 0;j<field;++j)
    //     {
    //         printf("%s\t",rows[j]);
    //     }
    //     printf("\n");
    // }
    // //9.释放结果集
    // mysql_free_result(res);
    // //10.释放mysql操作句柄
    // mysql_close(mysql);
    return 0;
}

9.项目模块设计以及流程图

模块设计:

首先我们应该分成3大模块:

1.数据管理模块:我们在管理用户数据的时候,我们需要知道用户的游戏信息,必须要保存到MySQL数据库中以便访问

2.前端模块:通过JS实现前端页面(注册、登录、游戏大厅、游戏房间)动态控制以及与服务器进行通信来完成前后端的联合。

3.业务处理模块(后端处理):通过搭建WebSocket服务器也客户端进行通信,接受到客户端的请求并进行业务处理。

业务处理模块中的子模块

1.网络通信模块:通过WebSocket以及 HTTP服务器的搭建,来提供网络通信的功能以便实现前后端完成业务处理,这个模块是存在是必然的,没有这个模块无法完成网络通信。

2.会话管理模块:对客户端的连接进行cookie以及session管理,实现对用户是否登录进行管理,如果没有登录或者在http短连接中超时,那么就会销毁会话,保证客户端的身份识别,这个模块可以帮我们确定那些玩家是已经登录过了,同时可以帮我们在用户退出页面时更新session销毁时间,保证session的合理性。

3.在线管理模块:可以获取到该用户是否在线,方便我们在进入游戏大厅和游戏房间的管理,可以提供用户是否在线以及用户的连接。

4.房间管理模块:在匹配 成功后,为用户创建游戏房间,提供五子棋对战以及实时聊天的供功能。

5.用户匹配模块:在不同的分数段中,有不用的匹配队列,为分数在同一个阶段的玩家创建房间并加入房间。

1.用户角度的流程:

2.站在服务器角度的流程图:

10.封装日志宏

到这里就正式开始了项目的编写了,首先最重要的就是日志输出,因为我们在测试项目的时候可能并不清楚到底哪里有问题,我们就可以通过日志打印来确定大概的位置,方便调试

如何封装:

我们需要知道以下的信息:时间、哪个文件、行数

时间我们可以使用time函数来获取时间,那么如何把时间分割成时分秒呢?已经有库给我们提供了一个函数:strftime

怎么获取到这个struct tm的结构体呢?我们可以通过localtime函数来获取:

根据这两个函数我们就可以输出时间,那文件和行数呢?

在C语言中预定义符号就提供了方法:

同时我们还可以设置日志等级,实现如下:

#pragma once
#include <cstdio>
#include <ctime>

#define INF 0
#define DEG 1
#define ERR 2
#define DEFAULT_LEVEL INF

#define LOG(level,format,...) do{\
    if(DEFAULT_LEVEL > level) break;\
    time_t t = time(nullptr); \
    struct tm* st = localtime(&t);\
    char buf[32] = {0};\
    strftime(buf,31,"%H-%M-%S",st);\
    fprintf(stdout,"[%s-%s-%d]" format "\n",buf,__FILE__,__LINE__,##__VA_ARGS__);\
}while(0)

#define ILOG(format,...)  LOG(INF,format,##__VA_ARGS__);
#define DLOG(format,...)  LOG(DEG,format,##__VA_ARGS__);
#define ELOG(format,...)  LOG(ERR,format,##__VA_ARGS__);

11.封装util工具类

util工具类主要是为了实现在项目中所需要使用的到的工具,例如MySQL、Json、string、file

MySQL用来存储用户的数据,Json用来进行序列化和反序列化,string用于字符串切割(这个后面会使用到),file用于文件的读取,这里主要是在进行前后端交付的时候,就可以读取本地文件资源返回给前端。这里我们工具类的类成员函数都要设计成static,因为工具类是提供给外部使用的,设计成静态就可以不实例化出对象就可以直接使用该函数了。

MySQL工具类设计接口:

1.完成实现创建MySQL的操作句柄:通过传入多个MySQL必须参数其中主要进行初始化mysql操作句柄,连接服务器,设置字符集,最后返回mysql操作句柄。

static MYSQL *mysql_create(const std::string &host, const std::string &username, const std::string &password,const std::string &dbname, uint16_t port)

2.进行MySQL的CURD操作:主要使用一个函数——mysql_query,我们通过sql语言和mysql操作句柄就可以实现

static bool mysql_exec(MYSQL *mysql, const std::string &sql)

3.MySQL操作句柄的销毁,防止资源泄露

static void mysql_destroy(MYSQL *mysql)

Json工具类设计接口:
1.完成序列化 :通过工厂类对象创建StreamWriter然后使用write进行str序列化

static bool serialize(const Json::Value &root, std::string &str)

2.完成反序列化:通过工厂类对象创建CharReader然后通过prase进行反序列化

static bool unserialize(const std::string &s, Json::Value &root)

string工具类设计接口:

1.完成字符串的分割:给定一个分割串,对string中进行分割放入一个vector<string>容器中

要考虑重复字符的情况

static int spilt(const std::string& str,const std::string& sep,std::vector<std::string>& res)

file工具类设计接口:

1.读取文件:获取文件路径,然后获取文件大小,通过C++文件流来读取文件,最后关闭文件

static bool read(const std::string& filename,std::string& body)

实现方式见后面源码中的util.hpp文件中

12.数据管理模块的实现

数据管理模块主要负责对于数据库中数据进⾏统⼀的增删改查管理,其他模块要对数据操作都必须通过数据管理模块完成。

12.1数据库的设计

1.首先必须要有用户id,id可以设置成主键并且是自增的。

2.然后我们必须要设计名字,名字当然要唯一,设置成唯一键。

3.还有用户密码,以上内容都是不能为空的

4.我们需要设计用户得分,而得分的初始值我们可以设计成1000,每赢一局得30分,输一局扣30分

5.总场数,以供用户知道自己对局的场数

6.胜利场次,让用户知道自己赢了多少局

下面3个数据我们可以在初始化的时候完成

12.2数据库类(user_table)的设计

功能接口设计:
1.我们需要通过名字或者id来查找数据中的用户信息,因此我们需要select_by_name和select_by_id函数来获取用户信息

select_by_name方法实现:通过名字来组织sql语言,进行数据库的访问,通过获取到用户的信息,然后通过Json::Value对象来接收

bool select_by_name(const std::string &name, Json::Value &user)

select_by_id方法实现:通过id来组织sql语言,进行数据库的访问,通过获取到用户的信息,然后通过Json::Value对象来接收

bool select_by_id(uint64_t id, Json::Value &user)

2.我们在注册的时候需要新增用户,所以我们需要向数据库中插入用户

insert实现方法:通过参数传递进来的Json::Value对象中的用户名和密码来组织sql语言向数据库进行插入

bool insert(Json::Value &user)

3.我们在登录的时候查看用户是否在数据库中,所以我们login函数来查看是否有用户数据

login函数实现方法:通过参数传递进来的Json::Value对象中的用户名和密码来组织sql语言来查询数据库,如果不存在,那么就返回错误;如果存在,那么就把数据库中全部用户信息返回

bool login(Json::Value &user)

4.在进行游戏过后肯定要输的一方或者赢的一方,肯定要更新数据,那么就需要两个函数来更新数据库

win函数实现方法:通过传入的id组织sql语言来对数据库修改

bool win(uint64_t id)

lose函数实现方法:通过传入的id组织sql语言来对数据库修改

bool lose(uint64_t id)

其中赢了就增加30分,胜利场次加1,总场次加1;失败就扣30分,胜利场次不变,总场次加1

13.在线用户管理模块

在线用户管理,是对于当前游戏⼤厅和游戏房间中的用户进⾏管理,主要是建⽴起用户与Socket连接的映射关系。

这个模块的作用是:1.能够根据用户信息,找到能与用户进行通信socket连接,这样可以和用户进行通信。

2.同时可以判断一个用户是否在线,或者用户是否已经掉线

设计思想

我们既然需要游戏大厅和游戏房间的管理,那么我们就可以使用unordered_map来对用户id和用户的连接关联起来,这样就可以通过用户id快速找到用户的连接,但是数据结构使用时并不是线程安全的,我们我们需要锁来对操作数据时进行保护

成员函数设计:
1.当用户进入游戏大厅或者游戏房间的时候我们需要将其id和连接关联起来

我们只需要将用户id和用户的连接使用unordered_map关联起来即可:
 

void enter_game_hall(uint64_t id, wsserver_t::connection_ptr conn)
void enter_game_room(uint64_t id, wsserver_t::connection_ptr conn)

2.当用户退出游戏大厅或者游戏房间的时候我们需要将id和关联解除

我们可以使用id来取消它们的关联:

void exit_game_hall(uint64_t id)
void exit_game_room(uint64_t id)

3.我们需要判断一个用户是否在游戏大厅或者游戏房间中

通过id来遍历unordered_map,如果找得到就在其中,找不到就不在:
 

bool is_in_game_hall(uint64_t id)
bool is_in_game_room(uint64_t id)

4.当我们需要某个用户的连接时,我们可以通过用户id来获取用户连接

查找unordered_map,找到就返回,找不到返回空的智能指针对象:
 

wsserver_t::connection_ptr get_conn_from_hall(uint64_t id)
wsserver_t::connection_ptr get_conn_from_room(uint64_t id)

14.游戏房间管理模块

14.1游戏房间类的设计

游戏房间主要针对匹配成功的玩家建立一个关联关系,一个房间中的任意动作都广播给房间中的所有用户,其中房间中的动作有两个:1.五子棋对战 2.实时聊天

设计思想(类成员的设计)

1.每个房间一定有一个房间的id用于管理,然后我们需要设计游戏房间的状态,这样可以标识游戏是处理游戏进行中还是游戏已经结束,这个有利于其他地方的判读。

2.在游戏房间中我们有两个用户一个白棋用户,另一个黑棋用户,那么我们是一定是需要两个id,即白棋用户id和黑棋用户id。

3.这里我们需要添加一个游戏房间的总用户,目的是为了保证如果两个人都退出了游戏房间,那么游戏房间就可以销毁了。如果还有人没有退出游戏房间,那房间就不能销毁。

4.既然是五子棋,那么我们肯定是需要一个二维数组来充当棋盘。

5.那我们有没有用到其他的模块呢?当然有,我们需要知道游戏房间中的用户和连接,这样才能保证在下棋或者聊天的时候能发送给它们。

6.我们还需要使用到数据库管理模块,因为在下棋结束之后一定是有人胜利并且有人失败的,所以我们在游戏结束之后要更新数据库的数据。

因此类成员如下:

类成员函数的设计

1.首先我们肯定是需要知道游戏房间的id以及白棋和黑棋用户的id,所以我们需要设计函数,让外部能访问这些成员,另外外部还需要知道这个房间中还有多少人,在房间管理中,就可以判断是否需要销毁房间了。

uint64_t id()
room_statu statu()
int player_count() 
uint64_t get_white_user()
uint64_t get_black_user()

2.我们可以通过成员函数来添加白棋和黑棋用户,这样就可以通过房间管理模块来帮我们完成用户的添加

void add_white_user(uint64_t id)
void add_black_user(uint64_t id)

3.我们是一定需要处理用户下棋动作的:通过用户的请求来处理下棋动作

Json::Value handler_chess(Json::Value &req)

实现步骤以及细节处理

3.1对方掉线,我方不战而胜

3.2处理当前位置是否已经有棋子(这步其实可以放在前端页面来做)

3.3判断当前的是否已经有人胜利。

在3.3中我们需要单独封装一个函数来判断当前是否有人胜利。那如果去判断?

我们可以根据当前位置来判断横排、纵列、正斜、反斜来判断,判断方式如下:

我们就可以通过一个函数就可以判断输赢,这里给出判断输赢的代码:

// 判断是否有获胜情况
    bool five(int row, int col, int row_off, int col_off, int color)
    {
        int rows = row + row_off;
        int cols = col + col_off;
        int count = 1;
        while (rows < BROADROW && rows >= 0 && cols < BROADCOL && cols >= 0 && _broad[rows][cols] == color)
        {
            ++count;
            // 向后偏移
            rows += row_off;
            cols += col_off;
        }
        // 另外一个方向
        rows = row - row_off;
        cols = col - col_off;
        while (rows < BROADROW && rows >= 0 && cols < BROADCOL && cols >= 0 && _broad[rows][cols] == color)
        {
            ++count;
            // 向前偏移
            rows -= row_off;
            cols -= col_off;
        }
        return count >= 5;
    }

至于判断获胜,我们只需要分别对横排、纵列、正斜、反斜分别校验即可。最后我们返回一个响应。

4.如果正在游戏中,突然有玩家退出游戏,那么我们就需要对这种情况进行处理

我们需要一个退出游戏的处理方法:我们需要拿到退出用户的id,这样才能判断哪个人输赢,最后广播给房间中的所有人,最后记得将房间状态设置成结束,以便后续处理。

void hander_exit(uint64_t uid)

5.处理房间中聊天:我们拿到一个信息,我们只要判断信息中是否有敏感词,如果有就不能发送,最后返回一个响应。

Json::Value handler_chat(Json::Value &req)

6.我们需要一个综合处理所有请求的接口:在我们拿到一个请求时,需要校验房间号是否正确,我们可以通过查看其中第一个optype字段(这里是协议定制,后面会详细说明,这个字段是用于判断当前请求是什么请求,下棋还是聊天还是其他)根据不用的请求来进行不同的处理。最后将处理的结果广播给房间中的所有人。

void hander_request(Json::Value &req)

7.我们需要一个广播操作,对一个已经处理好的用户的响应我们需要通知房间中的所有人。

我们要将响应反序列化并通过在线管理模块获得用户的连接,通过连接发送给用户。

void broad_cast(Json::Value &resp)

14.2游戏房间管理类的设计

类的设计(类成员函数)

1.这个类主要是对所有房间进行管理,因此我们需要设计的一个关联:房间id和房间的指针的关联,这样我们可以通过房间id快速找到房间,从而对房间进行操作;

2.另外我们需要建立用户id和房间id的关联,因为我们肯定是会用到通过用户id来查找房间的操作,而我们本来建立了房间id和房间指针的联系,这里我们只需要再使用一个unordered_map来映射用户id和房间id,这样就可以把这三者的关系建立起来。

3.因为需要使用unorderded_map,为了保证线程安全,需要设计锁

4.房间id是不断增加的,我们需要一个next_rid让其不断自增,保证创建的每个房间id不同

5.在我们创建房间的时候是需要查看该用户是否在游戏大厅中,如果该用户没有游戏大厅中,那么我们就不能为其创建游戏房间。所以我们需要在线用户管理模块的指针来进行操作,所以需要online_user这个成员

6.因为我们在创建房间时需要使用到数据库,所以我们有数据库管理模块的指针。

所以我们需要以下成员:

类成员函数的设计以及步骤实现:
1.我们必须提供一个创建房间的成员函数:通过两个用户id来创建一个房间,返回房间指针,这里所有的指针都采用智能指针,防止资源泄露。

1.1首先判断用户是否在线,如果不在线就不能为其创建房间;

1.2创建房间,将两个用户添加到房间中;

1.3将房间信息管理起来,也就是维护好哈希表。

1.4最后一定要每次++next_rid

room_ptr room_create(uint64_t uid1, uint64_t uid2)

2.我们说肯定需要通过用户id或者房间id来查找房间:

2.1先加锁保证线程安全;2.2通过哈希表来查找房间,如果是用户id查找就需要查找两次。

room_ptr get_room_by_rid(uint64_t rid)
room_ptr get_room_by_uid(uint64_t uid)

3.当我们要销毁房间时,需要使用房间id来销毁房间:(这里相当于删除哈希表中的信息)
3.1首先我们要获取到房间指针;

3.2得到用户信息,并去除用户和房间id的关联;

3.3去除房间id和房间的关联。

void destroy_room(uint64_t rid)

4.当有一方用户退出时,那么我们要讲该用户从房间中移除,所以我们需要提供一个通过用户id来将该用户移除房间的函数:
4.1获取房间信息 ;

4.2通过房间指针调用房间类中的handler_exit函数进行退出,这里前面已经写过了;

4.3(将房间中用户数量--)这里不需要做,前面调用的函数已经处理了。如果用户数量为0就销毁房间。

void delete_room_user(uint64_t uid)

15.session管理模块

15.1session是什么

在web开发中,HTTP协议是⼀种⽆状态短链接的协议,这就导致⼀个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。
因此,服务器为每个用户浏览器创建⼀个会话对象(session对象),注意:⼀个浏览器独占⼀个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使⽤浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务

15.2session工作原理

第一次访问时服务器会生成ssid和session的映射关系,并返回ssid;当客户端再次访问时就会携带ssid,这样服务器就可以通过哈希表快速找到session。

15.3session模块设计

session类成员的设计:

1.我们需要一个可以标识session的字段:ssid

2.session是用来记录用户是否登录的,所以肯定是需要用户uid的,才知道用户是谁。

3.session在http连接下是短连接,但是在websocket下是长连接,所以我们需要设置session的状态,是长连接还是短连接。

4.因为在http短连接的情况下,我们需要定时销毁连接,所以我们需要一个定时器,这里采用websocket中的time_ptr。

因此类成员如下:

session类成员函数设计:
这里主要是可以使用成员函数返回成员变量,因为这个类是给后面的session管理类服务的,所以这些变量需要外面获得的到,下面直接给出代码以及实现方式:
 

class session
{
private:
    uint64_t _ssid;            // 标识
    uint64_t _uid;             // 用户id
    ss_statu _statu;           // 状态
    wsserver_t::timer_ptr _tp; // 定时器
public:
    session(uint64_t ssid):_ssid(ssid) { DLOG("session %p 被创建!", _ssid); }
    ~session() { DLOG("session %p 被销毁!", _ssid); }
    void set_user(uint64_t uid) { _uid = uid; }
    void set_statu(ss_statu statu) { _statu = statu; }
    void set_timer(const wsserver_t::timer_ptr &tp) { _tp = tp; }
    bool is_login() { return _statu == LOGIN; }
    uint64_t get_ssid() { return _ssid; }
    uint64_t get_uid() { return _uid; }
    wsserver_t::timer_ptr &get_timer() { return _tp; }
};

15.4session管理模块设计

session管理类成员设计:
1.和房间管理类相似,我们也需要next_sid来维护我们的session的管理

2.我们需要建立ssid和session_ptr的联系,因为我们需要通过ssid来查找session类中的变量

3.数据结构哈希表不是线程安全的,所以我们需要锁来保证线程安全

4.我们需要设置定时任务来保证session的长短连接的维护,所以我们需要websocketpp::server<websocketpp::config::asio>的指针,这里我已经将其重定义为wsserver_t;

所以类成员变量如下:

session管理类成员函数设计以及实现步骤:
1.创建session,我们可以通过用户id和session的状态来建立session,我们有短连接和长连接两种状态,所以需要传入session的状态来确定是短连接还是长连接。

1.1创建session,设置状态,用户,以及ssid和session的映射关系,++_next_sid;

session_ptr session_create(uint64_t uid, ss_statu statu)

2.通过sid来获取session

2.1在哈希表中找到session并返回,如果没找到就返回空的智能指针对象

session_ptr get_session_by_sid(uint64_t sid)

3.添加session以及删除session(分别对应登录以及退出两种情况)

3.1维护哈希表中的映射关系

void append_session(session_ptr sp)
void remove_session(uint64_t sid)

4.因为需要更新session信息(即更新长连接和短连接),所以我们需要封装一个函数就行更新,通过sid以及传进来的秒数来更新。http通信时为短连接,该通信是定时删除;websocket通信是长连接,session是永久存在的;注册或者登陆的时候是短连接通信,应该定时删除,当用户退出游戏大厅或者游戏房间的时候应该设置定时删除

4.1在session永久存在的时候设置永久存在 (也就是不需要做任何操作)

4.2在session永久存在的情况下设置定制删除

4.3在定时删除的情况下设置永久存在

4.4在定时删除的情况下,重新计算时间定时删除

其中我们需要注意的是websocketpp给我们提供的定制删除是删除后立即执行,并不是删除了这个任务,而是删除了时间,变成立刻执行该任务,所以我们在取消定制删除的之后应该将seession重新添加。并且需要设置定时添加,在执行完上个任务之后0秒添加(为了防止删除任务时可能会慢一点导致你刚添加的任务又删除了)

void set_session_expire_time(uint64_t sid, int ms)

16.玩家匹配管理模块

16.1玩家匹配队列

匹配队列中的数据可能会改变,所以我们这里使用模板类来保证其可拓展性。

类成员设计

1.我们需要一个匹配队列用来匹配,这里我们不使用stl中的queue是因为,我们有对指定元素的删除,因为当一个玩家不想匹配的时候可以随时取消,所以使用list来代替queue

2.既然有数据结构的使用,那我们一定要保证线程安全,所以需要使用到锁

3.当匹配队列中数量少于2的时候需要等待,那么我们应该在数据大于等于2的时候将线程唤醒,所以我们需要条件变量来维护

因此类成员如下:

类成员函数的设计:

1.判断队列是否为空,获取队列的大小

int size()
bool empty()

2.插入或者移除数据

2.1从队列的尾端插入数据,每次插入数据都需要唤醒线程;删除数据就从队列最后删除,每次插入和删除都必须加锁保护

void push(const T &data)
bool pop(T &data)

3.阻塞线程,当队列数据少于2的时候就需要阻塞线程,而阻塞线程直接调用条件变量中的wait接口即可

void Wait()

4.删除队列中的指定元素,当用户取消匹配的时候就需要这个接口

4.1只需要调用list中remove接口即可

 void remove(T &data)

16.2玩家匹配管理模块

类成员设计

1.我们分了3个段位,分别是普通,高水平以及超高水平,根据不同的分数在不同的匹配队列中匹配相同实力的对手。所以我们设计了3个队列

2.有3个队列,那么就一定要有3个线程来维护

3.因为这里是在游戏大厅进行匹配,所以我们需要知道该用户在不在游戏大厅中,所以需要使用到在线管理模块的指针

4.匹配完成之后我们还需要为他们创建房间,所以需要房间管理模块的指针来创建房间

5.我们需要查看该用户信息是否在数据库中,所以需要数据库管理的指针

故类成员如下:

类成员函数的设计

1.首先是构造函数初始化:我们需要给3个线程添加线程入口函数,线程执行函数我们把它们放到类内。

matcher(user_table* ut,online_user* ou,room_manager* rm)
    :_ut(ut),_ou(ou),_rm(rm),_th_normal(std::thread(&matcher::thread_normal_enter,this)),
    _th_high(std::thread(&matcher::thread_high_enter,this)),_th_super(std::thread(&matcher::thread_super_enter,this))
    {
        DLOG("matcher 创建完成!");
    }

2.因为3个线程执行的动作其实非常相似,所以我们可以将3个线程执行函数分别调用一个hanlder_task函数来完成线程该执行的动作

2.1如果队列中的数量少于2就阻塞线程;

2.2判断两个用户是否出队成功,如果失败,记得把另外一个出队的数据再放入队列中;

2.3判断两个用户是否在线,如果不在线,记得把另外一个出队的数据再放入队列中;

2.4为它们创建游戏房间,如果房间创建失败,那么就将这两个数据放回队列中;

2.5组织响应序列化之后给两个用户应答;

void handler_task(match_queue<uint64_t> &mq)
//3个线程执行函数分别调用上面的接口,即可完成任务
void thread_normal_enter() { return handler_task(_q_normal); }
void thread_high_enter() { return handler_task(_q_high); }
void thread_super_enter() { return handler_task(_q_super); }

3.通过uid将用户添加到队列中以及将用户从队列中移除

3.1添加:从数据库中得到用户的分数信息,然后根据分数来添加到不同的队列

3.2移除:从数据库中得到用户的分数信息,然后根据分数来移除指定队列中的数据

bool add(uint64_t uid)
bool del(uint64_t uid)

17.服务器模块

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

17.1通信接口的设计(协议定制)

17.1.1静态资源的请求

1. 注册页面请求
请求:GET /register.html HTTP/1.1
响应:
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html

2. 登录页面请求
请求:GET /login.html HTTP/1.1
3. ⼤厅页面请求
请求:GET /game_hall.html HTTP/1.1
4. 房间页面请求
请求:GET /game_room.html HTTP/1.1

17.1.2注册用户

请求:

POST /reg HTTP/1.1
Content-Type: application/json
Content-Length: ***
{"username":"xiaobai", "password":"123456"}

成功时的响应
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": "用户名已经被占⽤"}

17.1.3用户登录

请求:

POST /login HTTP/1.1
Content-Type: application/json
Content-Length: ***
{"username":"xiaobai", "password":"123456"}

成功时的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: ***
{"result":true}

失败时的响应
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: ***
{"result":false, "reason": "⽤⼾名或密码错误"}

17.1.4获取客户端信息

请求:

GET /userinfo HTTP/1.1
Content-Type: application/json
Content-Length: 0

成功时的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: ***
{"id":1, "username":"xiaobai", "score":1000, "total_count":4, "win_count":2}

失败时的响应
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: ***
{"result":false, "reason": "用户还未登录"}

17.1.5websocket长连接协议切换请求(进入游戏大厅)

请求:

GET /match HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......

响应:

HTTP/1.1 101 Switching
......

WebSocket握手成功后的回复:表示游戏大厅已经进入成功。

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

17.1.6开始对战匹配

客户端:

{
 "optype": "match_start"
}

服务器响应:

后台正确处理后回复
{
 "optype": "match_start",

 "result": true
}
后台处理出错回复
{
 "optype": "match_start"
 "result": false,
 "reason": "具体原因...."
}

匹配成功了给客户端的回复
{
 "optype": "match_success", //表⽰成匹配成功

 "result": true
}

17.1.7停⽌匹配

客户端:

{
 "optype": "match_stop"
}

服务器响应:

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

}

17.1.8websocket长连接协议切换请求(进入游戏房间)

客户端:

GET /game HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......

服务器:

HTTP/1.1 101 Switching

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

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

17.1.9走棋

客户端:

{
 "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",
 "result": true,
 "reason": "对⽅掉线,不战⽽胜!"/"对方获得胜利!",

"room_id": 1,
 "uid": 1,
 "row": 3,
 "col": 2,
 "winner": 0 // 0-未分胜负, !0-已分胜负 (uid是谁,谁就赢了)
}

17.1.9聊天

客户端:

{
 "optype": "chat",
 "room_id": 1,
 "uid": 1,
 "message": "赶紧点"
}

服务器:

{
 "optype": "chat",
 "result": false
 "reason": "聊天失败具体原因....⽐如有敏感词..."
}

或者:

{
 "optype": "chat",
 "result": true,
 "room_id": 1,
 "uid": 1,
 "message": "赶紧点"
}

17.2服务器模块实现

类成员设计:

1.因为这里我们需要使用到所有的模块,所以肯定是需要所有模块的指针的

2.因为我们还需要对接前端的资源文件,所以我们需要设置一个web根目录

类成员如下:

类成员函数设计:

1.我们初始化服务以及启动服务器,这两个接口在我们练习使用websocket通信的时候就已经完成了相关操作,这里不重复赘述

gobang_server(const std::string &host, const std::string &username, const std::string &password,const std::string &dbname, uint16_t port, const std::string &webroot = WWWROOT)
    : _ut(host, username, password, dbname, port), _rm(&_ou, &_ut), _sm(&_wssvr), 
      _match(&_ut, &_ou, &_rm), _webroot(webroot)
void start(int port = 3389)

2.在前端需要静态资源的时候,我们要返回静态资源页面

2.1获取请求

2.2获取静态资源路径,如果是根目录,那么我们就返回登录页面

2.3读取文件,如果请求的资源不存在,就返回404

2.4设置响应状态码以及响应正文

void file_handler(wsserver_t::connection_ptr &conn)

3.我们需要http_callback来处理注册请、登录请求以及信息获取:我们可以根据不同的uri来处理不同的场景,uri从通信连接中得到

void http_callback(websocketpp::connection_hdl hdl)

3.1注册请求

3.1.1获取请求

3.1.2获取请求正文,并反序列化,然后向数据库中插入(如果失败,那就返回错误的响应)

3.1.3返回成功的响应

void reg(wsserver_t::connection_ptr &conn)

3.2登录请求:
3.2.1获取请求正文并反序列化

3.2.2校验用户信息的完整性,用户名和密码不能为空

3.2.3建立session,并设置定制删除

3.2.4添加cookie头部字段并进行响应

void login(wsserver_t::connection_ptr &conn)

3.3获取用户信息:

3.3.1获取cookie字段然后在cookie中获得ssid

3.3.2根据ssid来获取会话信息

3.3.3在数据库中找到用户信息并进行序列化

3.3.4设置响应正文,同时刷新session删除时间

void info(wsserver_t::connection_ptr &conn)

4.我们需要通过cookie字段获取session信息,这个函数在多个地方使用到,所以我们单独写出来

4.1.获取请求正文中的Cookie信息,并通过cookie获得ssid

4.2通过ssid获得会话信息

session_ptr get_session_by_cookie(wsserver_t::connection_ptr conn)

5.websocket通信模块,其中我们需要设置3个回调函数,为了初始化服务器

5.1连接建立时调用的回调函数:通过判断uri是游戏大厅或者游戏房间来分别处理

void open_callback(websocketpp::connection_hdl hdl)

5.1.1游戏大厅处理:

1.通过session查看客户端是否登录;2.查看用户是否重新登陆;3.将用户添加到在线用户管理模块中;4.给客户端进行响应,进入游戏大厅;5.将短连接改成长连接

void ws_game_hall(wsserver_t::connection_ptr conn)

5.1.2游戏房间的处理:
1.获取用户的会话信息;2.查看用户是否重新登陆;3.查看是否给用户创建好房间;4.将用户添加到在线用户管理中的房间管理模块;5.将连接设置成长连接;6.给客户端发送响应

void ws_game_room(wsserver_t::connection_ptr conn)

5.2连接关闭调用的回调函数:通过判断uri是游戏大厅或者游戏房间来分别处理

void close_callback(websocketpp::connection_hdl hdl)

5.2.1游戏大厅处理:

1.获取session信息;2.将用户移除游戏大厅;3.将连接改成短连接

void wsclose_game_hall(wsserver_t::connection_ptr conn)

5.2.2游戏房间处理:

1.获取会话信息;2.将玩家从在线用户管理房间模块中移除;3.将session信息设置为定时删除;4.将该用户移除房间

void wsclose_game_room(wsserver_t::connection_ptr conn)

5.3连接时的信息处理的回调函数:通过判断uri是游戏大厅或者游戏房间来分别处理

void message_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg)

5.3.1游戏大厅处理:通过连接和发送过来的信息完成处理之后发送给客户端
1.获取用户信息;2.将信息反序列化;3.分别处理开始匹配和停止匹配的动作;4.给客户端响应

void wsmsg_game_hall(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg)

5.3.2游戏大厅处理:通过连接和发送过来的信息完成处理之后发送给客户端

1.获取用户session信息;2.获取用户房间信息;3.将信息反序列化;4.通过房间管理模块来对信息进行处理,并返回给客户端

void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg)

18.前端模块

在这个模块中我们主要说明的是使用js语言来对接前端

18.1注册页面

当客户端输入完用户名和密码之后我们需要做的工作如下:
1.给提交按钮添加点击事件;2.获取输入框中的信息,通过ajax来组织一个响应发送给服务器,如果成功那就跳转登录页面,如果失败就打印提示原因,并清空输入框中的内容

18.2登录页面

当客户端输入完用户名和密码之后我们需要做的工作如下:

1.给提交按钮添加点击事件;2.获取输入框中的信息,通过ajax来组织一个响应发送给服务器,如果成功那就跳转游戏大厅页面,如果失败就打印提示原因,并清空输入框中的内容

18.3游戏大厅页面

1.我们在进入游戏大厅页面之后要加载用户的信息,并将websocket通信连接建立好,设置好各个回调函数,如果发生错误就返回登录页面

2.我们要给开始匹配按钮添加点击事件,当收到客户端要开始匹配的时候就向后台发送信息,当用户停止匹配的时候也是如此

3.在信息处理的时候我们需要根据不同的请求字段来分别对消息进行处理,例如:开始匹配、匹配成功、停止匹配等

18.4游戏房间页面

首先进入这个页面就需要建立好websocket长连接,设置好4个回调函数,对于消息处理函数我们最后整体讲解

1.处理下棋动作

1.1首先给棋盘添加点击事件;1.2查看是否轮到我方走棋;1.3查看下棋的位置是否被占用

1.4组织一个请求发送给服务器,让服务器查看当次请求是否合理。

2.处理聊天动作
2.1给发送按钮添加点击事件;2.2发送给服务器后经过敏感词的排查,如果没有问题就在聊天板中添加一个控件

3.消息处理的回调函数
3.1如果是刚刚进入游戏房间,那么我们就要保存好房间用户的信息,以便后面在下棋或者聊天中使用

3.2下棋操作:绘制棋盘;然后判断是否有胜利者,没有胜利者就继续,有胜利者就将下面的控件改成获胜或者失败的信息,然后添加返回游戏大厅的控件

3.3聊天处理:在聊天板中添加发送的信息,并清空发送区域的聊天信息

19.项目扩展

19.1 实现局时/步时
局时:⼀局游戏中玩家能思考的总时间
步时:⼀步落子过程中,玩家能思考的时间

19.2 保存棋谱&录像回放
服务器可以把每⼀局对局、玩家轮流落子的位置都记录下来
玩家可以在游戏大厅页面选定某个曾经的比赛,在页面上回放出对局的过程
19.3观战功能• 在游戏大厅显示当前所有的对局房间
玩家可以选中某个房间以观众的形式加入到房间中,实时的看到选手的对局情况

19.4 虚拟对手&人机对战
如果当前长时间匹配不到选手,则自动分配⼀个AI对手,实现⼈机对战

20.项目源码

Gitee:网页版五子棋源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/972467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于单片机的太阳能热水器控制器设计

一、项目介绍 随着环保意识的逐渐增强&#xff0c;太阳能热水器作为一种清洁能源应用得越来越广泛。然而&#xff0c;传统的太阳能热水器控制器通常采用机械式或电子式温控器&#xff0c;存在精度低、控制不稳定等问题。为了解决这些问题&#xff0c;本项目基于单片机技术设计…

Qt鼠标点击事件处理:按Escape键退出程序

创建项目 Qt 入门实战教程&#xff08;目录&#xff09; 首先&#xff0c;创建一个名称为QtKeyEscape的Qt默认的窗口程序。 参考 &#xff1a;Qt Creator 创建 Qt 默认窗口程序 Qt响应键盘Escape事件 打开Qt Creator >>编辑 >> 项目 >> Headers>> …

服务运营 | MS文章精读:基于强化学习和可穿戴设备的帕金森治疗方案

作者信息&#xff1a;庞硕&#xff0c;李舒湉 编者按 帕金森疾病的治疗是一个备受关注的医疗问题。本文通过患者的可穿戴传感器收集数据&#xff0c;提出了一个基于强化学习的帕金森药物治疗方案。这是第一篇关于可穿戴治疗设备在慢性疾病管理中的应用研究。原文于2023年4月发…

如何在你的Android工程中启用K2编译器?

如何在你的Android工程中启用K2编译器&#xff1f; K2编译器是用于Kotlin代码编译的最新、高效编译器&#xff0c;你现在可以尝试使用了。 Kotlin编译器正在为Kotlin 2.0进行重写&#xff0c;新的编译器实现&#xff08;代号K2&#xff09;带来了显著的构建速度改进&#xff…

K210-调用自定义py库

调用自定义py库 导入py库文件调用py库 用过Python的朋友应该知道&#xff0c;Python是支持将自定义py库&#xff08;或者第三方py库&#xff09;放到同一个目录下调用的&#xff0c;MicroPython也是支持调用自定义py库的。在调用自定义py库之前&#xff0c;需要提前将py库文件导…

期货基础知识

一、期货是什么&#xff1f;  期货是与现货相对应&#xff0c;并由现货衍生而来。期货通常指期货合约&#xff0c;期货与现货完全不同&#xff0c;现货是实实在在可以交易的货&#xff08;商品&#xff09;&#xff0c;期货主要不是货&#xff0c;而是以某种大众产品如棉花、大…

影响Windows 和 macOS平台,黑客利用 Adobe CF 漏洞部署恶意软件

FortiGuard 实验室的网络安全研究人员发现了几个影响 Windows 和 Mac 设备的 Adobe ColdFusion 漏洞。 远程攻击者可利用Adobe ColdFusion 2021中的验证前RCE漏洞&#xff0c;获取受影响系統的控制权力。Adobe 已发布安全补丁来解决这些漏洞&#xff0c;但攻击者仍在利用这些漏…

leetcode:1941. 检查是否所有字符出现次数相同(python3解法)

难度&#xff1a;简单 给你一个字符串 s &#xff0c;如果 s 是一个 好 字符串&#xff0c;请你返回 true &#xff0c;否则请返回 false 。 如果 s 中出现过的 所有 字符的出现次数 相同 &#xff0c;那么我们称字符串 s 是 好 字符串。 示例 1&#xff1a; 输入&#xff1a;s…

鼠标悬停阴影的效果被旁边div挡住的解决办法

出现的问题 需求要求鼠标悬停某个图片上有阴影效果&#xff0c;但阴影被旁边相邻的div挡住了&#xff0c;如图所示 解决方案 给悬停的这块div增加2个css属性 $(this).css(position, relative); $(this).css(z-index, 200);新的效果如图所示 一直写后端&#xff0c;前端的…

国际网页短信软件平台搭建定制接口说明|移讯云短信系统

国际网页短信软件平台搭建定制接口说明|移讯云短信系统 通道路由功能介绍 支持地区通道分流&#xff0c;支持关键字&#xff0c;关键词通道分流&#xff0c;支持白名单独立通道&#xff0c;支持全网通道分流&#xff0c;支持通道可发地区设置&#xff0c;通道路由分组&#x…

redis 数据结构(二)

整数集合 整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素&#xff0c;并且元素数量不时&#xff0c;就会使用整数集这个数据结构作为底层实现。 整数集合结构设计 整数集合本质上是一块连续内存空间&#xff0c;它的结构定义如下&#xff1a; typed…

QT C++ 实现网络聊天室

一、基本原理及流程 1&#xff09;知识回顾&#xff08;C语言中的TCP流程&#xff09; 2&#xff09;QT中的服务器端/客户端的操作流程 二、代码实现 1&#xff09;服务器 .ui .pro 在pro文件中添加network库 .h #ifndef WIDGET_H #define WIDGET_H#include <QWidget>…

春秋云镜 CVE-2018-12530

春秋云镜 CVE-2018-12530 Metinfo 6.0.0任意文件删除 靶标介绍 Metinfo 6.0.0任意文件删除。后台密码&#xff1a;f2xWcke5KN6pfebu 启动场景 漏洞利用 /admin进入管理后台&#xff0c;admin/f2xWcke5KN6pfebu /admin/app/batch/csvup.php?fileFieldtest-1&fliename…

目标检测框架MMDetection训练自定义数据集实验记录

在上一篇博文中&#xff0c;博主完成了MMDetection框架的环境部署与推理过程&#xff0c;下面进行该框架的训练过程&#xff0c;训练的入口文件为tools/train.py&#xff0c;我们需要配置的内容如下&#xff1a; parser.add_argument(--config,default"/home/ubuntu/prog…

算法通关村16关 | 滑动窗口最长字串专题

1. 最长字串专题 1.1 无重复字符的最长字串 题目 LeetCode3 给定一个字符串s&#xff0c;请你找出其中不含有重复字符的最长字串的长度。 思路 找最长字串&#xff0c;需要知道所有无重复字串的首和尾&#xff0c;找出其中最长的&#xff0c;最少两个指针才可以完成&#xff…

冠达管理:创业板是二板市场吗?二板市场起什么作用?

说到股市的各买卖板块&#xff0c;适当一部分投资者简单被主板、二板、三板这些词绕晕&#xff0c;其中二板商场一词关于有些投资者来说是比较生疏的&#xff0c;但面对创业板这一个不是主板但也没有说明是哪个层级的板块却熟悉许多&#xff0c;那么&#xff0c;创业板是不是便…

MySQL忘记密码了怎么办?如何重置修改密码?(Windows图文教程)

1. 如果服务在启动中&#xff0c;先停止MySQL服务 打开cmd&#xff0c;在命令窗口中输入net stop mysql 2. 跳过密码登录MySQL服务 mysqld --console --skip-grant-tables --shared-memory 输入以上代码 再次打开一个新的cmd窗口&#xff0c;原来的窗口不能关闭 在新的cmd窗口中…

冠达管理:股票隔夜挂单是怎么回事?股票挂单规则?

股票的买卖时刻是周一至周五上午9:30-11:30&#xff0c;下午13:00-15:00&#xff0c;一般投资者都是买卖日早上挂单&#xff0c;但也有一些投资者会在晚上隔夜挂单.那么&#xff0c;股票隔夜挂单是怎么回事&#xff1f;股票挂单规矩是什么&#xff1f;冠达管理为我们准备了相关…

C语言malloc函数学习

malloc的全称是memory allocation&#xff0c;中文叫动态内存分配&#xff0c;用于申请一块连续的指定大小的内存块区域&#xff0c;以void*类型返回分配的内存区域地址&#xff1b; 函数原型为void *malloc(unsigned int size)&#xff0c;在内存的动态存储区中分配一个长度为…

Python 内置函数详解 (1) 数学运算

近期在外旅游,本篇是出发前定时发布的,不完整,旅游回来后再补充。 Python 内置函数 Python3.11共有75个内置函数,其来历和分类请参考:Python 新版本有75个内置函数,你不会不知道吧_Hann Yang的博客-CSDN博客 函数列表 abs aiter all …