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

目录

1.项目介绍

2.开发环境

3.核心技术

4.环境搭建

5.知识点与代码用例

Websocket介绍

websocket报文格式​编辑

Websocketpp介绍

websocketpp常⽤接⼝介绍:

[Simple http/websocket服务器](#Simple http/websocket服务器)

封装Json工具类

[MySQL API介绍](#MySQL API介绍)

封装MySQL工具类

6.项目结构设计

项⽬模块划分说明

业务处理模块的⼦模块划分

项⽬流程图

7.实⽤⼯具类模块代码实现

⽇志宏封装

Mysql-API封装

Jsoncpp-API封装

String-Split封装

File-read封装

8.数据管理模块实现

数据库设计

创建user_table类

9.在线⽤⼾管理模块实现

[10. 游戏房间管理模块](#10. 游戏房间管理模块)

房间类实现

房间管理类实现

11.总结


1.项目介绍

本项⽬主要实现⼀个⽹⻚版的五⼦棋对战游戏, 其主要⽀持以下核⼼功能:

  • ⽤⼾管理: 实现⽤⼾注册, ⽤⼾登录、获取⽤⼾信息、⽤⼾天梯分数记录、⽤⼾⽐赛场次记录等
  • 匹配对战: 实现两个玩家在⽹⻚端根据天梯分数匹配游戏对⼿,并进⾏五⼦棋游戏对战的功能
  • 聊天功能: 实现两个玩家在下棋的同时可以进⾏实时聊天的功能

2.开发环境

  • Linux(Ubuntu-22.04)
  • VSCode/Vim
  • g++/gdb
  • Makefile

3.核心技术

  • HTTP/WebSocket
  • Websocket++
  • JsonCpp
  • Mysql
  • C++11
  • BlockQueue
  • HTML/CSS/JS/AJAX

4.环境搭建

5.知识点与代码用例

Websocket介绍

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

  • 传统的 web 程序都是属于 "⼀问⼀答" 的形式,即客⼾端给服务器发送了⼀个 HTTP 请求,服务器给客⼾端返回⼀个 HTTP 响应。这种情况下服务器是属于被动的⼀⽅,如果客⼾端不主动发起请求服务器就⽆法主动给客⼾端响应
  • 像⽹⻚即时聊天或者我们做的五⼦棋游戏这样的程序都是⾮常依赖 "消息推送" 的, 即需要服务器主动推动消息到客⼾端。如果只是使⽤原⽣的 HTTP 协议,要想实现消息推送⼀般需要通过 "轮询" 的⽅式实现, ⽽轮询的成本⽐较⾼并且也不能及时的获取到消息的响应。轮询成本高的原因一个是网络带宽是非常昂贵的,也浪费用户的流量,一个是服务器接收的请求激增,降低程序所能接受的在线用户数量;不及时就是因为需要至少一次请求和响应才能达到目的。

基于上述两个问题, 就产⽣了WebSocket协议。WebSocket 更接近于 TCP 这种级别的通信⽅式,⼀旦连接建⽴完成客⼾端或者服务器都可以主动的向对⽅发送数据。

原理解析

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

具体协议升级的过程如下:

这里让人比较疑惑的就是这个钥匙,这个钥匙是客户端浏览器自己生成的,具体说就是我们会在前端代码中发起websocket升级,发起后浏览器就自己生成了这个钥匙添加到请求头之中;然后服务端拿到这个钥匙之后,对它进行SHA-1哈希、Base64编码,得到服务端返回客户端的钥匙;客户端拿到这个钥匙之后,会拿原本的钥匙做相同的SHA-1哈希、Base64编码,通过比对查看通过相同规则转化之后的钥匙是否相同,如果相同,则升级握手成功,反之则升级握手失败。

websocket报文格式

报⽂字段⽐较多,我们重点关注这⼏个字段:

FIN: WebSocket传输数据以消息为概念单位,⼀个消息有可能由⼀个或多个帧组成,FIN字段为1 表⽰末尾帧。在用Websocket通信时,传送的消息首先在WebSocket这个应用层被分成一个一个的帧,也就是一个一个的WebSocket报文格式,接收端根据这个FIN判断这个帧是不是末尾帧,是的话就将这个完整的消息交给应用程序,不是则缓存下来等待后续的帧。

RSV1~3:保留字段,只在扩展时使⽤,若未启⽤扩展则应置0,若收到不全为0的数据帧,且未协 商扩展则⽴即终⽌连接。
opcode: 标志当前数据帧的类型

  • 0x0: 表⽰这是个延续帧,当 opcode 为 0 表⽰本次数据传输采⽤了数据分⽚,当前收到的帧为 其中⼀个分⽚
  • 0x1: 表⽰这是⽂本帧
  • 0x2: 表⽰这是⼆进制帧
  • 之后的值不关心

opcode与FIN结合起来使用,它们俩结合起来表示的意思是:

  • FIN == 0 && opcode == 0x1 表示这是开头帧,且是文本格式;
  • FIN == 0 && opcode == 0x2 表示这是开头帧,且是二进制格式;
  • FIN == 0 && opcode == 0x0 表示这是延续帧;
  • FIN == 1 表示这是结尾帧 若opcode == 0x1 || opcode == 0x2则它同时是开头帧。

mask:表⽰Payload数据是否被编码,若为1则必有Mask-Key,⽤于解码Payload数据。仅客⼾端发送给服务端的消息需要设置。编码和Mask-Key的生成都是浏览器自己完成的。

Payload length:数据载荷的⻓度,单位是字节, 有可能为7位、7+16位、7+64位。假设Payload length = x,这是一种分层编码设计,小数据不浪费额外的字节,大数据也能够表示出来

  • x为0~126:数据的⻓度为x字节
  • x为126:后续2个字节代表⼀个16位的⽆符号整数,该⽆符号整数的值为数据的⻓度
  • x为127:后续8个字节代表⼀个64位的⽆符号整数(最⾼位为0),该⽆符号整数的值为数据的⻓度,图中简化为了32位

Mask-Key:当mask为1时存在,⻓度为4字节,解码规则: DECODED[i] = ENCODED[i] ^ MASK[i % 4]

Payload data: 报⽂携带的载荷数据

Websocketpp介绍

WebSocketpp是⼀个跨平台的开源(BSD许可证)头部专⽤C++库,它实现了RFC6455(WebSocket协议)和RFC7692(WebSocketCompression Extensions)。它允许将WebSocket客⼾端和服务器功能集成到C++程序中。在最常⻅的配置中,全功能⽹络I/O由Asio⽹络库提供。

WebSocketpp的主要特性包括:

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

WebSocketpp同时⽀持HTTP和Websocket两种⽹络协议, ⽐较适⽤于我们本次的项⽬, 所以我们选⽤该库作为项⽬的依赖库⽤来搭建HTTP和WebSocket服务器。

下⾯是该项⽬的⼀些常⽤⽹站。

websocketpp常⽤接⼝介绍:

cpp 复制代码
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);
				std::string const& get_body();
				typedef std::map<std::string, std::string, utility::ci_less > header_list;
				header_list const& get_headers();
			}
			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 log 
	{
		struct alevel 
		{
			static level const none = 0x0;
			static level const connect = 0x1;
			static level const disconnect = 0x2;
			static level const control = 0x4;
			static level const frame_header = 0x8;
			static level const frame_payload = 0x10;
			static level const message_header = 0x20;
			static level const message_payload = 0x40;
			static level const endpoint = 0x80;
			static level const debug_handshake = 0x100;
			static level const debug_close = 0x200;
			static level const devel = 0x400;
			static level const app = 0x800;
			static level const http = 0x1000;
			static level const fail = 0x2000;
			static level const access_core = 0x00003003;
			static level const all = 0xffffffff;
		};
	}
 
	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
			};
		}
	}
	namespace frame 
	{
		namespace opcode 
		{
			enum value 
			{
				continuation = 0x0,
				text = 0x1,
				binary = 0x2,
				rsv3 = 0x3,
				rsv4 = 0x4,
				rsv5 = 0x5,
				rsv6 = 0x6,
				rsv7 = 0x7,
				close = 0x8,
				ping = 0x9,
				pong = 0xA,
				control_rsvb = 0xB,
				control_rsvc = 0xC,
				control_rsvd = 0xD,
				control_rsve = 0xE,
				control_rsvf = 0xF,
			};
		}
	}
}

Simple http/websocket服务器

使⽤Websocketpp实现⼀个简单的http和websocket服务器

cpp 复制代码
#include <iostream>
#include <functional>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>

// using namespace std;
typedef websocketpp::server<websocketpp::config::asio> server_t; // WebSocket 服务器的核心类
typedef server_t::message_ptr message_ptr;

void print(const std::string& str)
{
    std::cout << str << std::endl;
}

// websocketpp::connection_hdl是客户端与服务器的连接句柄
void onOpen(server_t* server, websocketpp::connection_hdl)
{
    std::cout << "websocket连接成功" << std::endl;
}

void onClose(server_t* server, websocketpp::connection_hdl)
{
    std::cout << "websocket连接断开" << std::endl;
}

void onHttp(server_t* server, websocketpp::connection_hdl hdl)
{
    auto conn = server->get_con_from_hdl(hdl);
    std::cout << "body : " + conn->get_request_body() << std::endl;
    auto req = conn->get_request();
    std::cout << "method : " + req.get_method() << std::endl;
    std::cout << "uri : " + req.get_uri() << std::endl;

    std::string resp = "<html><body><h1>nihao</h1></body></html>";
    conn->set_body(resp);
    conn->set_status(websocketpp::http::status_code::ok);
    conn->append_header("Content-Type", "text/html");
    auto timePtr = conn->set_timer(5000, std::bind(print, "hello,hi!"));
    timePtr->cancel(); // 取消后会立即执行绑定的任务
}

// message_ptr msg是长连接建立之后的消息指针
void onMessage(server_t* server, websocketpp::connection_hdl hdl, message_ptr msg)
{
    auto conn = server->get_con_from_hdl(hdl);
    std::cout << msg->get_payload() << std::endl;
    std::string str = "client say : " + msg->get_payload();
    conn->send(str);
}

int main()
{
    // 创建server对象
    server_t server;
    // 初始化日志等级
    server.set_access_channels(websocketpp::log::alevel::none);
    // 初始化asio调度器
    server.init_asio();
    // 启用地址重用
    server.set_reuse_addr(true);
    // 绑定回调函数
    server.set_open_handler(std::bind(&onOpen, &server, std::placeholders::_1));
    server.set_close_handler(std::bind(&onClose, &server, std::placeholders::_1));
    server.set_http_handler(std::bind(&onHttp, &server, std::placeholders::_1));
    server.set_message_handler(std::bind(onMessage, &server, std::placeholders::_1, std::placeholders::_2));
    // 绑定端口
    server.listen(8888);
    // 接收http请求
    server.start_accept();
    // 运行服务器
    server.run();

    return 0;
}

Http客⼾端

使⽤浏览器作为http客⼾端即可, 访问服务器的8888端⼝。

WS客⼾端

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<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>
// 创建 websocket 实例
// ws://106.55.155.75:8888
// 类⽐http
// ws表⽰websocket协议
// 106.55.155.75 表⽰服务器地址
// 8888表⽰服务器绑定的端⼝
let websocket = new WebSocket("ws://106.55.155.75:8888");

// 处理连接打开的回调函数
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>

在控制台中我们可以看到连接建⽴、客⼾端和服务器通信以及断开连接的过程(关闭服务器就会看到断开连接的现象)

封装Json工具类

Json以及Jsoncpp的使用与介绍在我的其它博客里已经讲述了很多了,这里直接展示封装类。

cpp 复制代码
class json_util
{
public:
    static bool serialize(const Json::Value& root, std::string& str)
    {
        Json::StreamWriterBuilder swb;
        std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
        std::stringstream ss;
        int ret = sw->write(root, &ss);
        if(ret != 0)
        {
            ELOG("Json序列化失败");
            return false;
        } 
        str = ss.str();
        return true;
    }
    static bool unserialize(const std::string& str, Json::Value& root)
    {
        Json::CharReaderBuilder crb;
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
        std::string err;
        if(!cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err))
        {
            ELOG("Json反序列化失败,原因:%s" , err.c_str());
            return false;
        }
        return true;
    }
};

通过json_util类的静态函数来将Json::Value对象序列化为字符串,或者将字符串反序列化为Json::Value对象。在这个项目的网络传输中,正文的传输使用的就是这个序列化和反序列化规则。容易发现,Jsoncpp使用了工厂模式。

MySQL API介绍

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

首先在ubuntu命令行中安装客户端开发包。

bash 复制代码
sudo apt update && sudo apt install libmysqlclient-dev

api介绍

cpp 复制代码
// Mysql操作句柄初始化
// 参数说明:
// mysql为空则动态申请句柄空间进⾏初始化
// 返回值: 成功返回句柄指针, 失败返回NULL
MYSQL *mysql_init(MYSQL *mysql);

// 连接mysql服务器
// 参数说明:
// mysql--初始化完成的句柄
// host---连接的mysql服务器的地址
// user---连接的服务器的⽤⼾名
// passwd-连接的服务器的密码
// db ----默认选择的数据库名称
// port---连接的服务器的端⼝: 默认0是3306端⼝
// unix_socket---通信管道⽂件或者socket⽂件,通常置NULL
// client_flag---客⼾端标志位,通常置0
// 返回值:成功返回句柄指针,失败返回NULL
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user,
                          const char *passwd,const char *db, unsigned int,
                          port, const char *unix_socket, unsigned long client_flag);

// 设置当前客⼾端的字符集
// 参数说明:
// mysql--初始化完成的句柄
// csname--字符集名称,通常:"utf8"
// 返回值:成功返回0, 失败返回⾮0
int mysql_set_character_set(MYSQL *mysql, const char *csname);

// 选择操作的数据库
// 参数说明:
// mysql--初始化完成的句柄
// db-----要切换选择的数据库名称
// 返回值:成功返回0, 失败返回⾮0
int mysql_select_db(MYSQL *mysql, const char *db);

// 执⾏sql语句
// 参数说明:
// mysql--初始化完成的句柄
// stmt_str--要执⾏的sql语句
// 返回值:成功返回0, 失败返回⾮0
int mysql_query(MYSQL *mysql, const char *stmt_str);

// 保存查询结果到本地
// 参数说明:
// mysql--初始化完成的句柄
// 返回值:成功返回结果集的指针, 失败返回NULL
MYSQL_RES *mysql_store_result(MYSQL *mysql)
 
// 获取结果集中的⾏数
// 参数说明:
// result--保存到本地的结果集地址
// 返回值:结果集中数据的条数
uint64_t mysql_num_rows(MYSQL_RES *result);

// 获取结果集中的列数
// 参数说明:
// result--保存到本地的结果集地址
// 返回值:结果集中每⼀条数据的列数
unsigned int mysql_num_fields(MYSQL_RES *result);

// 遍历结果集, 并且这个接⼝会保存当前读取结果位置,每次获取的都是下⼀条数据
// 参数说明:
// result--保存到本地的结果集地址
// 返回值:实际上是⼀个char **的指针,将每⼀条数据做成了字符串指针数组 
// row[0]-第0列 row[1]-第1列 ...
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);

// 释放结果集
// 参数说明:
// result--保存到本地的结果集地址
void mysql_free_result(MYSQL_RES *result);

// 关闭数据库客⼾端连接,销毁句柄
// 参数说明:
// mysql--初始化完成的句柄
void mysql_close(MYSQL *mysql)
 
// 获取mysql接⼝执⾏错误原因
// 参数说明:
// mysql--初始化完成的句柄
const char *mysql_error(MYSQL *mysql)

封装MySQL工具类

cpp 复制代码
class mysql_util
{
public:
    static MYSQL *mysql_create(const std::string &host, const std::string &user,
                               const std::string &passwd, const std::string 
                               &dbname, u_int16_t port = 3306)
    {
        MYSQL *mysql = mysql_init(NULL);

        mysql = mysql_real_connect(mysql, host.c_str(), user.c_str(), passwd.c_str(), dbname.c_str(), port, NULL, 0);
        if (mysql == NULL) ELOG("connect MySQL failed!: %s", mysql_error(mysql));

        int ret = mysql_set_character_set(mysql, "utf8");
        if (ret != 0) ELOG("set character failed: %s", mysql_error(mysql));

        return mysql;
    }

    static bool mysql_exec(MYSQL *const mysql, const std::string &sql) 
    {
        int ret = mysql_query(mysql, sql.c_str());
        if(ret != 0)
        {
            ELOG("mysql query failed! reaseon : %s ::: sql is : %s", mysql_error(mysql), sql.c_str());
            return false;
        }
        return true;
    }
    static void mysql_destroy(MYSQL *mysql) 
    {
        if(mysql) mysql_close(mysql);
    }
};

另外这个项目还需要前端知识,可以参考我的三篇博客:

6.项目结构设计

项⽬模块划分说明

项⽬的实现,咱们将其划分为三个⼤模块来进⾏:

  • 数据管理模块:基于Mysql数据库进⾏⽤⼾数据的管理
  • 前端界⾯模块:基于JS实现前端⻚⾯(注册,登录,游戏⼤厅,游戏房间)的动态控制以及与服务器的通信。
  • 业务处理模块:搭建WebSocket服务器与客⼾端进⾏通信,接收请求并进⾏业务处理。

在这⾥回顾⼀下我们要实现的项⽬功能,我们要实现的是⼀个在线五⼦棋对战服务器,提供⽤⼾通过浏览器进⾏⽤⼾注册,登录,以及实时匹配,对战,聊天等功能。

⽽如果要实现这些功能,那么就需要对业务处理模块再次进⾏细分为多个模块来实现各个功能。

业务处理模块的⼦模块划分

  • ⽹络通信模块:基于websocketpp库实现Http&WebSocket服务器的搭建,提供⽹络通信功能。
  • 会话管理模块:对客⼾端的连接进⾏cookie&session管理,实现http短连接时客⼾端⾝份识别功能。
  • 在线管理模块:对进⼊游戏⼤厅与游戏房间中⽤⼾进⾏管理,提供⽤⼾是否在线以及获取⽤⼾连接的功能。
  • 房间管理模块:为匹配成功的⽤⼾创建对战房间,提供实时的五⼦棋对战与聊天业务功能。
  • ⽤⼾匹配模块:根据天梯分数不同进⾏不同层次的玩家匹配,为匹配成功的玩家创建房间并加⼊房间。

项⽬流程图

玩家⽤⼾⻆度流程图:

服务器流程结构图:

7.实⽤⼯具类模块代码实现

实⽤⼯具类模块主要是负责提前实现⼀些项⽬中会⽤到的边缘功能代码,提前实现好了就可以在项⽬中⽤到的时候直接使⽤了。

⽇志宏封装

cpp 复制代码
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <stdio.h>
#include <time.h>

#define INF 0
#define DEG 1
#define ERR 2
#define DEFAULT INF
#define LOG(level, format, ...) do{\
    if(level < DEFAULT) break;\
    time_t t = time(NULL);\
    struct tm* lt = localtime(&t);\
    char buffer[32] = { 0 };\
    strftime(buffer, 31, "%H:%M:%S", lt);\
    fprintf(stdout, "[%s][%s:%d]  " format "\n", buffer, __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__)

#endif

Mysql-API封装

cpp 复制代码
class mysql_util
{
public:
    static MYSQL *mysql_create(const std::string &host, const std::string &user,
                               const std::string &passwd, const std::string 
                               &dbname, u_int16_t port = 3306)
    {
        MYSQL *mysql = mysql_init(NULL);

        mysql = mysql_real_connect(mysql, host.c_str(), user.c_str(), passwd.c_str(), dbname.c_str(), port, NULL, 0);
        if (mysql == NULL) ELOG("connect MySQL failed!: %s", mysql_error(mysql));

        int ret = mysql_set_character_set(mysql, "utf8");
        if (ret != 0) ELOG("set character failed: %s", mysql_error(mysql));

        return mysql;
    }

    static bool mysql_exec(MYSQL *const mysql, const std::string &sql) 
    {
        int ret = mysql_query(mysql, sql.c_str());
        if(ret != 0)
        {
            ELOG("mysql query failed! reaseon : %s ::: sql is : %s", mysql_error(mysql), sql.c_str());
            return false;
        }
        return true;
    }
    static void mysql_destroy(MYSQL *mysql) 
    {
        if(mysql) mysql_close(mysql);
    }
};

由于这个项目是多线程的,各个线程会对数据库进行如上操作,所以在读写时都需要加锁,避免读写不一致问题。

Jsoncpp-API封装

cpp 复制代码
class json_util
{
public:
    static bool serialize(const Json::Value& root, std::string& str)
    {
        Json::StreamWriterBuilder swb;
        std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
        std::stringstream ss;
        int ret = sw->write(root, &ss);
        if(ret != 0)
        {
            ELOG("Json序列化失败");
            return false;
        } 
        str = ss.str();
        return true;
    }
    static bool unserialize(const std::string& str, Json::Value& root)
    {
        Json::CharReaderBuilder crb;
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
        std::string err;
        if(!cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err))
        {
            ELOG("Json反序列化失败,原因:%s" , err.c_str());
            return false;
        }
        return true;
    }
};

String-Split封装

cpp 复制代码
class string_util
{
public:
    static int split(const std::string& str, const std::string& sep,
        std::vector<std::string>& arr)
    {
        int pos, idx = 0;
        while(idx < str.size())
        {
            pos = str.find(sep, idx);
            if(pos == std::string::npos)
            {
                arr.push_back(str.substr(idx));
                break;
            }
            if(pos == idx) 
            {
                idx += sep.size();
                continue;
            }
            arr.push_back(str.substr(idx, pos - idx));
            idx = pos + sep.size();
        }
        return sep.size();
    }
};

用sep分割str,将子串放入arr中。

File-read封装

cpp 复制代码
class file_util
{
public:
    static bool read(const std::string& filename, std::string& body)
    {
        std::ifstream ifs(filename, std::ios::binary);
        if(!ifs.is_open())
        {
            ELOG("%s file open failed!", filename.c_str());
            return false;
        }

        ifs.seekg(0, std::ios::end);
        size_t fileSz = ifs.tellg();
        body.resize(fileSz);
        ifs.seekg(0, std::ios::beg);

        ifs.read(&body[0], fileSz);
        if(!ifs.good())
        {
            ELOG("%s file read failed!", filename.c_str());
            return false;
        }
        return true;
    }
};

一个文件读取类,通过一个通用的方法获取文件大小,然后写入分配好空间的string。

body[0] 返回的是 string 第一个字符的引用(char& ,取它的地址 &body[0] 就能得到 char*(指向 string 内部可写缓冲区的起始地址)。

8.数据管理模块实现

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

数据库设计

创建user表, ⽤来表⽰⽤⼾信息及积分信息

  • 创建user表, ⽤来表⽰⽤⼾信息及积分信息,用于在游戏大厅中展示

  • 积分信息, ⽤来实现匹配功能

sql 复制代码
drop database if exists gobang;
create database if not exists gobang;
use gobang;
create table user (
    id int primary key auto_increment,
    username varchar(32) unique key not null,
    password varchar(64) not null,
    score int,
    total_count int,
    win_count int
)

创建user_table类

数据库中有可能存在很多张表,每张表中管理的数据⼜有不同,要进⾏的数据操作也各不相同,因此我们可以为每⼀张表中的数据操作都设计⼀个类,通过类实例化的对象来访问这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使⽤哪个类实例化的对象即可。
创建user_table类, 该类的作⽤是负责通过 MySQL 接⼝管理⽤⼾数据。主要提供了四个⽅法:

  • select_by_name: 根据⽤⼾名查找⽤⼾信息, ⽤于实现登录功能
  • insert: 新增⽤⼾,⽤⼾实现注册功能
  • login: 登录验证,并获取完整的⽤⼾信息
  • win: ⽤于给获胜玩家修改分数
  • lose: ⽤⼾给失败玩家修改分数
cpp 复制代码
#pragma once 
#include "util.hpp"

class user_table
{
private:
    MYSQL* _mysql;
    std::mutex _mutex;
public:
    user_table(const std::string &host, const std::string &user,
                               const std::string &passwd, const std::string 
                               &dbname, u_int16_t port = 3306)
    {
        _mysql = mysql_util::mysql_create(host, user, passwd, dbname, port);
        assert(_mysql);
    }
    ~user_table()
    {
         mysql_util::mysql_destroy(_mysql);
    }
    // 新增用户时调用
    bool insert(const Json::Value& user)
    {
        if(user["username"].isNull() || user["password"].isNull())
        {
            DLOG("用户名和密码都必须需要");
            return false;
        }
#define INSERT_USER "insert user values(NULL, '%s', password(%s), 1000, 0, 0)"
        char sql[1024] = { 0 };
        sprintf(sql, INSERT_USER, user["username"].asCString(), user["password"].asCString());
        bool ret = mysql_util::mysql_exec(_mysql, sql);
        if(!ret)
        {
            ELOG("新增用户插入数据库失败");
            return false;
        }
        return true;
    }
    // 用户登录时使用,并且返回完整用户信息
    bool login(Json::Value& user)
    {
#define SELECT_USER "select id, score, total_count, win_count from user where username = '%s' and password = password(%s);"
        char sql[1024] = { 0 };
        sprintf(sql, SELECT_USER, user["username"].asCString(), user["password"].asCString());
        MYSQL_RES* res = NULL;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            if(mysql_util::mysql_exec(_mysql, sql) == false)
            {
                DLOG("登录用户时查询数据失败");
                return false;
            }
            res =  mysql_store_result(_mysql);
            if(res == NULL)
            {
                DLOG("查询后保存数据失败");
                return false;
            }
        }
        uint64_t row =  mysql_num_rows(res);
        if(row != 1)
        {
            DLOG("登录用户时用户数据不是一,而是:%ld", row);
            return false;
        }
        MYSQL_ROW data =  mysql_fetch_row(res);
        user["id"] = (Json::UInt64)std::stol(data[0]);
        user["score"] = (Json::UInt64)std::stol(data[1]);
        user["total_count"] = std::stoi(data[2]);
        user["win_count"] = std::stoi(data[3]);

        return true;
    }
    // 用户注册之前查看注册名字是否重复
    bool selectByName(const std::string& name, Json::Value& user)
    {
#define SELECT_USER_BY_NAME "select id, password, score, total_count, win_count from user where username = '%s';"
        char sql[1024] = { 0 };
        sprintf(sql, SELECT_USER_BY_NAME, name.c_str());
        MYSQL_RES* res = NULL;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            if(mysql_util::mysql_exec(_mysql, sql) == false)
            {
                DLOG("用名字查询用户时查询数据失败");
                return false;
            }
            res =  mysql_store_result(_mysql);
            if(res == NULL)
            {
                DLOG("查询后保存数据失败");
                return false;
            }
        }
        uint64_t row =  mysql_num_rows(res);
        if(row != 1)
        {
            DLOG("用名字查询用户时用户数据不唯一");
            return false;
        }
        MYSQL_ROW data =  mysql_fetch_row(res);
        user["id"] = (Json::UInt64)std::stol(data[0]);
        user["username"] = name;
        user["password"] = data[1];
        user["score"] = (Json::UInt64)std::stol(data[2]);
        user["total_count"] = std::stoi(data[3]);
        user["win_count"] = std::stoi(data[4]);

        return true;
    }
    // 使用id获取用户信息
    bool selectById(uint64_t id, Json::Value& user)
    {
#define SELECT_USER_BY_ID "select username, password, score, total_count, win_count from user where id = %ld;"
        char sql[1024] = { 0 };
        sprintf(sql, SELECT_USER_BY_ID, id);
        MYSQL_RES* res = NULL;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            if(mysql_util::mysql_exec(_mysql, sql) == false)
            {
                DLOG("用id查询用户失败");
                return false;
            }
            res =  mysql_store_result(_mysql);
            if(res == NULL)
            {
                DLOG("查询后保存数据失败");
                return false;
            }
        }
        uint64_t row =  mysql_num_rows(res);
        if(row != 1)
        {
            DLOG("用id查询用户时用户数据不为1,为:%ld", row);
            return false;
        }
        MYSQL_ROW data =  mysql_fetch_row(res);
        user["id"] = (Json::UInt64)id;
        user["username"] = data[0];
        user["password"] = data[1];
        user["score"] = (Json::UInt64)std::stol(data[2]);
        user["total_count"] = std::stoi(data[3]);
        user["win_count"] = std::stoi(data[4]);

        return true;        
    }
    // 玩家胜利加30分
    bool win(int id)
    {
#define UPDATE_WIN "update user set score = score + 30, total_count = total_count + 1, win_count = win_count + 1 where id = %d;"
        char sql[1024] = { 0 };
        sprintf(sql, UPDATE_WIN, id);
        if(mysql_util::mysql_exec(_mysql, sql) == false)
        {
            ELOG("获胜时更新数据失败");
            return false;
        }
        return true;
    }
    // 玩家失败扣30分
    bool lose(int id)
    {
#define UPDATE_LOSE "update user set score = score - 30, total_count = total_count + 1 where id = %d;"
        char sql[1024] = { 0 };
        sprintf(sql, UPDATE_LOSE, id);
        if(mysql_util::mysql_exec(_mysql, sql) == false)
        {
            ELOG("失败时更新数据失败");
            return false;
        }
        return true;
    }
};

9.在线⽤⼾管理模块实现

在线⽤⼾管理,是对于当前游戏⼤厅和游戏房间中的⽤⼾进⾏管理,主要是建⽴起⽤⼾与Socket连接的映射关系,这个模块具有两个功能:

  1. 能够让程序中根据⽤⼾信息,进⽽找到能够与⽤⼾客⼾端进⾏通信的Socket连接,进⽽实现与客⼾端的通信。
  2. 判断⼀个⽤⼾是否在线,或者判断⽤⼾是否已经掉线。
cpp 复制代码
#ifndef __ONLIEN_HPP__
#define __ONLIEN_HPP__
#include "util.hpp"
#include <mutex>
#include <unordered_map>

class online_manager
{
private:
    std::mutex _mutex;
    // 用于建立游戏大厅用户的用户ID与通信连接的关系
    std::unordered_map<uint64_t, server_t::connection_ptr> _hall_user;
    // 用于建立游戏房间用户的用户ID与通信连接的关系
    std::unordered_map<uint64_t, server_t::connection_ptr> _room_user;
public:
    // websocket连接建立的时候才会加入游戏大厅&游戏房间在线用户管理
    void enter_game_hall(uint64_t uid, server_t::connection_ptr conn)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _hall_user[uid] = conn;
    }
    void enter_game_room(uint64_t uid, server_t::connection_ptr conn)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _room_user[uid] = conn;
    }
    // websocket连接断开的时候,才会移除游戏大厅&游戏房间在线用户管理
    void exit_game_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _hall_user.erase(uid);
    }
    void exit_game_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _room_user.erase(uid);
    }
    // 判断当前指定用户是否在游戏大厅/游戏房间
    bool is_in_game_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _hall_user.find(uid);
        if(it == _hall_user.end()) return false;
        return true;
    }
    bool is_in_game_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _room_user.find(uid);
        if(it == _room_user.end()) return false;
        return true;
    }
    // 通过用户ID在游戏大厅/游戏房间用户管理中获取对应的通信连接
    server_t::connection_ptr get_conn_from_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if(_hall_user.count(uid)) return _hall_user[uid];
        return server_t::connection_ptr();
    }
    server_t::connection_ptr get_conn_from_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if(_room_user.count(uid)) return _room_user[uid];
        return server_t::connection_ptr();
    }
};

#endif

10. 游戏房间管理模块

房间类实现

⾸先,需要设计⼀个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建⽴⼀个⼩范围的关联关系,⼀个房间中任意⼀个⽤⼾发⽣的任何动作,都会被⼴播给房间中的其他⽤⼾。

⽽房间中的动作主要包含两类:

  1. 棋局对战
  2. 实时聊天
cpp 复制代码
#define BOARD_ROW 15
#define BOARD_COL 15

#define CHESS_WHITE 1
#define CHESS_BLACK 2

enum room_statu
{
    GAME_START,
    GAME_OVER
};

class room
{
private:
    uint64_t _room_id;
    room_statu _statu;
    int _player_count;
    uint64_t _white_id;
    uint64_t _black_id;
    user_table* _tb_user;
    online_manager* _online_user;
    std::vector<std::vector<int>> _board;
private:
    bool five(int row, int col, int row_off, int col_off, int color) 
    {
        //row和col是下棋位置,  row_off和col_off是偏移量,也是方向
        int count = 1;
        int search_row = row + row_off;
        int search_col = col + col_off;
        while(search_row >= 0 && search_row < BOARD_ROW &&
                search_col >= 0 && search_col < BOARD_COL &&
                _board[search_row][search_col] == color) 
        {
            //同色棋子数量++
            count++;
            //检索位置继续向后偏移
            search_row += row_off;
            search_col += col_off;
        }
        search_row = row - row_off;
        search_col = col - col_off;
        while(search_row >= 0 && search_row < BOARD_ROW &&
                search_col >= 0 && search_col < BOARD_COL &&
                _board[search_row][search_col] == color) 
        {
            //同色棋子数量++
            count++;
            //检索位置继续向后偏移
            search_row -= row_off;
            search_col -= col_off;
        }
        return (count >= 5);
    }
    uint64_t check_win(int row, int col, int color) 
    {
        // 从下棋位置的四个不同方向上检测是否出现了5个及以上相同颜色的棋子(横行,纵列,正斜,反斜)
        if (five(row, col, 0, 1, color) || 
            five(row, col, 1, 0, color) ||
            five(row, col, -1, 1, color)||
            five(row, col, -1, -1, color)) 
        {
            //任意一个方向上出现了true也就是五星连珠,则设置返回值
            return color == CHESS_WHITE ? _white_id : _black_id;
        }
        return 0;
    }
public:
    room(uint64_t room_id, user_table* tb_user, online_manager* online_user):
        _room_id(room_id), _statu(GAME_START), _player_count(0),
        _tb_user(tb_user), _online_user(online_user),
        _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
    {
        DLOG("%lu 房间创建成功!!", _room_id);
    }
    ~room()
    {
        DLOG("%lu 房间销毁成功!!", _room_id);
    }
    uint64_t id() { return _room_id; }
    room_statu statu() { return _statu; }
    int player_count() { return _player_count; }
    void add_white_user(uint64_t uid) { _white_id = uid; _player_count++; }
    void add_black_user(uint64_t uid) { _black_id = uid; _player_count++; }
    uint64_t get_white_user() { return _white_id; }
    uint64_t get_black_user() { return _black_id; }

    // 处理下棋动作
    Json::Value handle_chess(Json::Value& req)
    {
        Json::Value json_resp;
        json_resp["optype"] = "put_chess";
        uint64_t room_id = req["room_id"].asUInt64();
        uint64_t cur_uid = req["uid"].asUInt64();
        int chess_row = req["row"].asInt();
        int chess_col = req["col"].asInt();
        // 1.判断房间id是否是当前的房间id
        if(_room_id != room_id)
        {
            json_resp["result"] = false;
            json_resp["reason"] = "房间号不匹配";
            return json_resp;
        }
        // 2.判断是否有玩家掉线,掉线则对方获胜
        if(!_online_user->is_in_game_room(_white_id))
        {
            json_resp["room_id"] = (Json::UInt64)room_id;
            json_resp["uid"] = (Json::UInt64)cur_uid;
            json_resp["row"] = chess_row;
            json_resp["col"] = chess_col;
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜";
            json_resp["winner"] = (Json::UInt64)_black_id;
            return json_resp;
        }
        if(!_online_user->is_in_game_room(_black_id))
        {
            json_resp["room_id"] = (Json::UInt64)room_id;
            json_resp["uid"] = (Json::UInt64)cur_uid;
            json_resp["row"] = chess_row;
            json_resp["col"] = chess_col;
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜";
            json_resp["winner"] = (Json::UInt64)_white_id;
            return json_resp;
        }
        // 3.判断走棋是否合理
        if(_board[chess_row][chess_col])
        {
            json_resp["result"] = false;
            json_resp["reason"] = "当前位置已经有棋子了";
            return json_resp;
        }
        int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;
        _board[chess_row][chess_col] = cur_color;
        // 4.判断是否正常获胜
        uint64_t winner_id = check_win(chess_row, chess_col, cur_color);
        json_resp["result"] = true;
        json_resp["reason"] = "五星连珠,战无敌";
        json_resp["room_id"] = _room_id;
        json_resp["uid"] = (Json::UInt64)cur_uid;
        json_resp["row"] = chess_row;
        json_resp["col"] = chess_col;
        json_resp["winner"] = (Json::UInt64)winner_id;
        return json_resp;
    }
    // 处理聊天动作
    Json::Value handle_chat(Json::Value& req)
    {
        Json::Value json_resp;
        json_resp["optype"] = "chat";
        //1.检查房间号是否一致
        uint64_t room_id = req["room_id"].asInt64();
        if(_room_id != room_id)
        {
            json_resp["result"] = false;
            json_resp["reason"] = "房间号不匹配";
            return json_resp;
        }
        //2.检查是否包含敏感词
        std::string message = req["message"].asString();
        if(message.find("垃圾") != std::string::npos)
        {
            json_resp["result"] = false;
            json_resp["reason"] = "包含敏感词";
            return json_resp;
        }
        //3.广播消息到房间
        json_resp = req;
        json_resp["result"] = true;
        return json_resp;
    }
    // 处理玩家退出房间动作
    void handle_exit(uint64_t uid)
    {
        //如果是下棋中退出,则对方获胜,否则下棋结束了退出,则是正常退出
        Json::Value json_resp;
        if(_statu == GAME_START)
        {
            uint64_t winner_id = (Json::UInt64)(uid == _white_id ? _black_id : _white_id);
            json_resp["optype"] = "put_chess";
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜!";
            json_resp["room_id"] = (Json::UInt64)_room_id;
            json_resp["uid"] = (Json::UInt64)uid;
            json_resp["row"] = -1;
            json_resp["col"] = -1;
            json_resp["winner"] = (Json::UInt64)winner_id;
            uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
            _tb_user->win(winner_id);
            _tb_user->lose(loser_id);
            _statu = GAME_OVER;
            broadcast(json_resp);
        }
        //房间中玩家数量--
        _player_count--;
        return;
    }
    // 总的请求处理函数,在函数内部,区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播
    void handle_request(Json::Value& req)
    {
        //1.校验房间号是否匹配
        Json::Value json_resp;
        uint64_t room_id = req["room_id"].asUInt64();
        if(room_id != _room_id)
        {
            json_resp["optype"] = req["optype"].asString();
            json_resp["result"] = false;
            json_resp["reason"] = "房间号不匹配";
        }
        //2.根据不同的请求类型调用不同的处理函数
        if(req["optype"].asString() == "put_chess")
        {
            json_resp = handle_chess(req);
            if(json_resp["winner"].asUInt64() != 0)
            {
                uint64_t winner_id = json_resp["winner"].asUInt64();
                uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
                _tb_user->win(winner_id);
                _tb_user->lose(loser_id);
                _statu = GAME_OVER;
            }
        }
        else if(req["optype"].asString() == "chat")
        {
            json_resp = handle_chat(req);
        }
        else
        {
            json_resp["optype"] = req["optype"].asString();
            json_resp["result"] = false;
            json_resp["reason"] = "未知请求类型";
        }
        broadcast(json_resp);
        return;
    }
    // 将指定的信息广播给房间中所有玩家
    void broadcast(Json::Value& rsp)
    {
        //1.对要响应的信息进行序列化,将Json::Value中的数据序列化成为json格式字符串
        std::string body;
        json_util::serialize(rsp, body);
        //2.获取房间中所有用户的通信连接并发送响应信息
        server_t::connection_ptr wconn = _online_user->get_conn_from_room(_white_id);
        if(wconn.get() != nullptr)
        {
            wconn->send(body);
        }
        server_t::connection_ptr bconn = _online_user->get_conn_from_room(_black_id);
        if(bconn.get() != nullptr)
        {
            bconn->send(body);
        }
        return;
    }
};

每个room有一个唯一的_room_id作为唯一标识来进行管理;_statu表示两种状态,游戏进行中和游戏结束,用于标识用户退出时是正常退出还是中途退出;_player_count表示房间中的用户数量,在玩家加入房间时加1;white_id表示白棋玩家的id,black_id表示黑棋玩家的id;_tb_user表示数据库操作句柄,_online_user表示在线用户管理操作句柄;_board表示棋盘。

在成员函数方面,check_win和five判断棋局是否胜利;以及对于不同的请求有不同的函数进行处理,根据不同的情况设置返回的Json::Value对象;另外还有负责广播的函数,给客户端发送序列化之后的信息。

房间管理类实现

实现对所有的游戏房间进⾏管理。

cpp 复制代码
using room_ptr = std::shared_ptr<room>;

class room_manager
{
private:
    uint64_t _next_rid;
    std::mutex _mutex;
    user_table* _tb_user;
    online_manager* _online_user;
    std::unordered_map<uint64_t, room_ptr> _rooms;
    std::unordered_map<uint64_t, uint64_t> _users;
public:
    // 初始化房间ID计数器
    room_manager(user_table* ut, online_manager* _rooms):
        _next_rid(1), _tb_user(ut), _online_user(_rooms)
    {
        DLOG("%s", "房间管理类创建成功");
    }
    ~room_manager()
    {
        DLOG("%s", "房间管理类已经销毁");
    }
    // 为两个用户创建房间,并返回房间的智能指针对象
    room_ptr create_room(uint64_t uid1, uint64_t uid2)
    {
        //两个用户在游戏大厅中进行对战匹配,匹配成功后创建房间
        //1.校验两个用户是否都还在游戏大厅中,只有都在才需要创建房间
        if(!_online_user->is_in_game_hall(uid1))
        {
            DLOG("用户:%lu 不在大厅中,创建房间失败!", uid1);
            return room_ptr();
        }
        if(!_online_user->is_in_game_hall(uid2))
        {
            DLOG("用户:%lu 不在大厅中,创建房间失败!", uid2);
            return room_ptr();
        }
        //2.创建房间,将用户添加到房间中
        std::unique_lock<std::mutex> lock(_mutex);
        room_ptr rp(new room(_next_rid, _tb_user, _online_user));
        rp->add_white_user(uid1);
        rp->add_black_user(uid2);
        //3.将房间信息管理起来
        _rooms[_next_rid] = rp;
        _users[uid1] = _next_rid;
        _users[uid2] = _next_rid;
        //4.返回房间信息
        _next_rid++;
        
        return rp;
    }
    // 通过房间ID获取房间信息
    room_ptr get_room_by_rid(uint64_t rid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if(_rooms.count(rid)) return _rooms[rid];
        return room_ptr();
    }
    // 通过用户ID获取房间信息
    room_ptr get_room_by_uid(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        uint64_t rid = 0;
        if(_users.count(uid)) rid = _users[uid];
        else return room_ptr();

        if(_rooms.count(rid)) return _rooms[rid];
        return room_ptr();
    }
    // 通过房间ID销毁房间
    void remove_room(uint64_t rid)
    {
        //1.通过房间ID,获取房间信息
        room_ptr rp = get_room_by_rid(rid);
        if(rp.get() == nullptr) return;
        //2.通过房间信息,获取房间中所有用户的ID
        uint64_t uid1 = rp->get_white_user();
        uint64_t uid2 = rp->get_black_user();
        //3.移除房间管理中的用户信息
        std::unique_lock<std::mutex> lock(_mutex);
        _users.erase(uid1);
        _users.erase(uid2);
        //4.移除房间
        _rooms.erase(rid);
    }
    // 删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
    void remove_room_user(uint64_t uid)
    {
        room_ptr rp = get_room_by_uid(uid);
        if(rp.get() == nullptr) return;
        //处理房间中玩家退出动作
        rp->handle_exit(uid);
        //房间中没有玩家了,则销毁房间
        if(rp->player_count() == 0)
        {
            remove_room(rp->id());
        }
        return;
    }
};

一个进行游戏房间管理的类,为两个用户创建房间,处理玩家退出,处理房间销毁,根据房间id获取房间智能指针,根据用户id获取房间智能指针。

11.总结

文章很长了,后面的内容写到下一篇博客中。写到这里网页已经有点卡了哈哈。

相关推荐
脏脏a39 分钟前
【初阶数据结构】栈与队列:定义、核心操作与代码解析
c语言·开发语言
济宁雪人40 分钟前
Java安全基础——序列化/反序列化
java·开发语言
q***017741 分钟前
Java进阶--IO流
java·开发语言
lsx20240641 分钟前
C语言中的枚举(enum)
开发语言
csbysj20201 小时前
PHP Math
开发语言
小画家~1 小时前
第三十四:golang 原生 pgsql 对应操作
android·开发语言·golang
ulias2121 小时前
初步了解STL和string
开发语言·c++·mfc
二川bro1 小时前
字符串格式化进阶:Python f-string性能优化
开发语言·python
LitchiCheng1 小时前
Mujoco 机械臂 OMPL 进行 RRT 关节空间路径规划避障、绕障
开发语言·人工智能·python