目录
1.项目介绍
2.开发环境
3.核心技术
4. 环境搭建
5.websocketpp
5.1原理解析
5.2报文格式
5.3websocketpp常用接口介绍
5.4websocket服务器
6.JsonCpp使用
6.1Json数据格式
6.2JsonCpp介绍
7.MySQL API
7.1MySQL API介绍
7.2MySQL API使用
7.3实现增删改查操作
8.项目结构设计
8.1项目模块划分说明
8.2业务处理模块的子模块划分
8.3项目流程图
8.3.1玩家用户角度流程图:
8.3.2 服务器流程结构图:
9.实用工具类模块代码实现
9.1 日志宏封装
9.2 MySQL-API封装
9.3 Jsoncpp-API封装
9.4 String-Split封装:
9.5 File-read封装
10. 数据管理模块实现
10.1数据库设计
10.2创建user_table类
11.在线用户管理模块实现
12.游戏房间管理模块
12.1房间类实现
12.2房间管理类实现
13. session管理模块设计
13.1什么是session
13.2 session工作原理
13.3 session类设计实现
13.4 session管理设计实现
14.五子棋对战玩家匹配管理设计实现
14.1匹配队列实现
14.2 玩家匹配管理模块设计实现
15.整合封装服务器模块设计实现
15.1通信接口设计(Restful风格)
15.1.1静态资源请求
15.1.2注册用户
15.1.3用户登录
15.1.4获取客户端信息
15.1.5 websocket长连接协议切换请求(进入游戏大厅)
15.1.6 开始对战匹配
15.1.7 停止匹配
15.1.8 websocket长连接协议切换请求(进入游戏房间)
15.1.9 走棋
15.1.10 聊天
15.2 服务器模块实现
16.客户端开发
项目总结
五子棋实现源码链接
1.项目介绍
本项目主要实现一个网页版的五子棋对战游戏,其主要支持以下核心功能:
●用户管理:实现用户注册,用户登录、获取用户信息、用户天梯分数记录、用户比赛场次记录等
●匹配对战:实现两个玩家在网页端根据天梯分数匹配游戏对手,并进行五子棋游戏对战的功能
●聊天功能:实现两个玩家在下棋的同时可以进行实时聊天的功能
2.开发环境
Linux (Centos-7.6) .
VSCode/Vim
g++/gdb
Makefile
3.核心技术
●HTTP/WebSocket
●Websocketpp
●JsonCpp
●MySQL
●C++11
●BlockQueue
●HTML/CSS/JS/AJAX
4. 环境搭建
安装wget工具
[myl@localhost ~]$ sudo yum install wget
更换软件源
[myl@localhost ~]$ sudo mv /etc/yum.repos.d/CentOS-Base.repo
/etc/yum.repos.d/CentOS-Base.repo.bak
[myl@localhost ~]$ sudo wget -O /etc/yum.repos.d/CentOS-Base.repo
http://mirrors.aliyun.com/repo/Centos-7.repo
[myl@localhost ~]$ sudo yum clean all
Loaded plugins: fastestmirror
Cleaning repos: base extras updates
Cleaning up list of fastest mirrors
[myl@localhost ~]$ sudo yum makecache
...
安装scl软件源
[myl@localhost ~]$ sudo yum install centos-release-scl-rh centos-release-scl
安装lrzsz传输⼯具
[myl@localhost ~]$ sudo yum install lrzsz
...
[myl@localhost ~]$ rz --version
rz (lrzsz) 0.12.20
安装高版本gcc/g++编译器
[myl@localhost ~]$ sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
...
[myl@localhost ~]$ echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
[myl@localhost ~]$ source ~/.bashrc
[myl@localhost ~]$ g++ -v
安装gdb调试器
[myl@localhost ~]$ sudo yum install gdb
安装git
[myl@localhost ~]$ sudo yum install git
安装cmake
[myl@localhost ~]$ sudo yum install cmake
安装boost库
[myl@localhost ~]$ sudo yum install boost-devel.x86_64
安装Jsoncpp库
[myl@node ~]$ sudo yum install jsoncpp-devel
安装MySQL数据库服务及开发包
获取mysql官方yum源
[myl@VM-8-12-centos workspace]$ wget http://repo.mysql.com/mysql57-community-
release-el7-10.noarch.rpm
安装mysql官方yum源
[myl@VM-8-12-centos workspace]$ sudo rpm -ivh mysql57-community-release-el7-
10.noarch.rpm
安装MySQL数据库服务
[myl@VM-8-12-centos workspace]$ sudo yum install -y mysql-community-server
出错解决
如果因为GPG KEY的过期导致安装失败
[myl@VM-8-12-centos workspace]$ GPG Keys are configured as:
file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
则执⾏以下指令,然后重新安装
[myl@VM-8-12-centos workspace]$ sudo rpm --import https://repo.mysql.com/RPM-
GPG-KEY-mysql-2022
安装MySQL开发包
[myl@VM-8-12-centos workspace]$ sudo yum install -y mysql-community-devel
进行MySQL配置修改
配置/etc/my.cnf字符集
[myl@VM-8-12-centos workspace]$ sudo vim /etc/my.cnf
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
character-set-server=utf8
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
启动MySQL服务
[myl@VM-8-12-centos workspace]$ sudo systemctl start mysqld
[myl@VM-8-12-centos workspace]$ sudo systemctl status mysqld
注意,如果启动的时候遇到了以下情况,输⼊系统的root管理员密码即可
[myl@VM-8-12-centos workspace]$ systemctl start mysqld
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to manage system services or units.
Authenticating as:
Password:
获取MySQL临时密码
[myl@VM-8-12-centos workspace]$ sudo grep 'temporary password' /var/log/mysqld.l
设置mysql数据库密码
[myl@VM-8-12-centos workspace]$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.7.41 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> set global validate_password_policy=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set global validate_password_length=1;
Query OK, 0 rows affected (0.00 sec)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'XXX@YYY';
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
登录查看MySQL字符集是否正常
[myl@VM-8-12-centos workspace]$ mysql -uroot -p Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.7.41 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show variables like '%chara%';
+ --------------------------+----------------------------+
| Variable_name | Value |
+ --------------------------+----------------------------+
| character_set_client | utf8 | --客⼾端使⽤的字符集
| character_set_connection | utf8 | --客⼾端连接时使⽤的字符集
| character_set_database | utf8 | --数据库创建默认字符集
| character_set_filesystem | binary| --⽂件系统编码格式
| character_set_results | utf8 | --服务器返回结果时的字符集
| character_set_server | utf8 | --存储系统元数据的字符集
| character_set_system | utf8 | --系统使⽤的编码格式,不影响
| character_sets_dir | /usr/share/mysql/charsets/ |
+ --------------------------+----------------------------+
8 rows in set (0.00 sec)
mysql> quit
安装Websocketpp库
[myl@localhost ~]$ git clone https://github.com/zaphoyd/websocketpp.git
安装websocketpp
[myl@localhost ~]$ ls
websocketpp
[myl@localhost ~]$ cd websocketpp/
[myl@localhost websocketpp]$ mkdir build
[myl@localhost websocketpp]$ cd build
[myl@localhost build]$ cmake -DCMAKE_INSTALL_PREFIX=/usr ..
...
[myl@localhost build]$ sudo make install
验证websocketpp是否安装成功
[myl@localhost build]$ cd ../examples/echo_server
[myl@localhost echo_server]$ ls
CMakeLists.txt echo_handler.hpp echo_server.cpp SConscript
[myl@localhost echo_server]$ g++ -std=c++11 echo_server.cpp -o echo_server -
lpthread -lboost_system
[myl@localhost echo_server]$
编译成功,则表示安装成功了。
5.websocketpp
WebSocket是从HTML5开始支持的一种网页端和服务端保持长连接的消息推送机制。
●传统的web程序都是属于"一问一答"的形式,即客户端给服务器发送了一个HTTP请求,服务器给客户端返回一个HTTP响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应
●像网页即时聊天或者我们做的五子棋游戏这样的程序都是非常依赖"消息推送"的,即需要服务器主动推送消息到客户端。如果只是使用原生的HTTP协议,要想实现消息推送一般需要通过 "轮询"的方式实现,而轮询的成本比较高并且也不能及时的获取到消息的响应。
基于上述两个问题,就产生了WebSocket协议。WebSocket 更接近于TCP这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。
5.1原理解析
WebSocket协议本质上是一个基于TCP的协议。为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程并升级协议的过程。
5.2报文格式
报文字段比较多,我们重点关注这几个字段:
●FIN: WebSocket传输数据以消息为概念单位,一个消息有可能由一个或多个帧组成,FIN字段为1
表示末尾帧。
●RSV1~3: 保留字段,只在扩展时使用,若未启用扩展则应置1,若收到不全为0的数据帧,且未协
商扩展则立即终止连接。
●opcode:标志当前数据帧的类型
0x0:表示这是个延续帧,当opcode为0表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片
0x1:表示这是文本帧
0x2:表示这是二进制帧
0x3-0x7:保留,暂未使用
0x8:表示连接断开
0x9:表示ping帧
0xa:表示pong帧
0xb-0xf:保留,暂未使用
●mask: 表示Payload数据是否被编码,若为1则必有Mask-Key, 用于解码Payload数据。仅客户端
发送给服务端的消息需要设置。
Payload length:数据载荷的长度,单位是字节,有可能为7位、7+16位、 7+64位。 假设Payload
length=x
x为0~126: 数据的长度为x字节
x为126: 后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度
。x为127: 后续8个字节代表一个64位的无符号整数(最高位为0) , 该无符号整数的值为数据的
长度
Mask-Key:当mask为1时存在,长度为4字节,解码规则: DECODED[i] = ENCODED[i] ^ MASK[i
% 4]
Payload data:报文携带的载荷数据
WebSocketpp同时支持HTTP和Websocket两种网络协议,比较适用于我们本次的项目,所以我们选用该库作为项目的依赖库用来搭建HTTP和WebSocket服务器。
5.3websocketpp常用接口介绍
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::valueop = 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 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,};}}
}
5.4websocket服务器
#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
using namespace std;
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;
using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
// websocket连接成功的回调函数
void OnOpen(websocketsvr *server, websocketpp::connection_hdl hdl)
{cout << "连接成功" << endl;
}
// websocket连接成功的回调函数
void OnClose(websocketsvr *server, websocketpp::connection_hdl hdl)
{cout << "连接关闭" << endl;
}
// websocket连接收到消息的回调函数
void OnMessage(websocketsvr *server, websocketpp::connection_hdl hdl, message_ptr msg)
{cout << "收到消息" << msg->get_payload() << endl;// 收到消息将相同的消息发回给websocket客⼾端server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}
// websocket连接异常的回调函数
void OnFail(websocketsvr *server, websocketpp::connection_hdl hdl)
{cout << "连接异常" << endl;
}
// 处理http请求的回调函数 返回⼀个html欢迎⻚⾯
void OnHttp(websocketsvr *server, websocketpp::connection_hdl hdl)
{cout << "处理http请求" << endl;websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);std::stringstream ss;ss << "<!doctype html><html><head>"<< "<title>hello websocket</title><body>"<< "<h1>hello websocketpp</h1>"<< "</body></head></html>";con->set_body(ss.str());con->set_status(websocketpp::http::status_code::ok);
}
int main()
{// 使⽤websocketpp库创建服务器websocketsvr server;// 设置websocketpp库的⽇志级别// all表⽰打印全部级别⽇志// none表⽰什么⽇志都不打印server.set_access_channels(websocketpp::log::alevel::none);/*初始化asio*/server.init_asio();// 注册http请求的处理函数server.set_http_handler(bind(&OnHttp, &server, ::_1));// 注册websocket请求的处理函数server.set_open_handler(bind(&OnOpen, &server, ::_1));server.set_close_handler(bind(&OnClose, &server, _1));server.set_message_handler(bind(&OnMessage, &server, _1, _2));// 监听8888端⼝server.listen(8888);// 开始接收tcp连接server.start_accept();// 开始运⾏服务器server.run();return 0;
}
客户端
使用浏览器作为http客户端即可,访问服务器的8888端口
6.JsonCpp使用
6.1Json数据格式
Json是一种数据交换格式,它采用完全独立于编程语言的文本格式来存储和表示数据。
例如:我们想表示一一个同学的学生信息
C代码表示
char *name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};
Json 表示:
{
"姓名" : "xx",
"年龄" : 18,
"成绩" : [88.5, 99, 58]
}[
{"姓名":"⼩明", "年龄":18, "成绩":[23, 65, 78]},
{"姓名":"⼩红", "年龄":19, "成绩":[88, 95, 78]}
]
对象:使用花括号{}括起来的表示一一个对象
数组:使用中括号[]括起来的表示一个数组
字符串:使用常规双引号""括起来的表示一个字符串
数字:包括整形和浮点型,直接使用
6.2JsonCpp介绍
Jsoncpp库主要是用于实现Json格式数据的序列化和反序列化,它实现了将多个数据对象组织成
为Json格式字符串,以及将Json格式字符串解析得到多个数据对象的功能。
先看一下Json数据对象类的表示
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;
}
JsonCpp功能代码用例编写:
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{// 序列化Json::Value stu;stu["name"] = "zhangsan";stu["age"] = 19;stu["socre"].append(77.5);stu["socre"].append(88);stu["socre"].append(99.5);Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;int ret = sw->write(stu, &ss);if (ret != 0){std::cout << "Serialize failed!\n";return -1;}std::cout << "序列化结果:\n"<< ss.str() << std::endl;// 反序列化std::string str = ss.str();Json::Value root;Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());bool ret1 = cr->parse(str.c_str(), str.c_str() + str.size(), &root,nullptr);if (!ret1){std::cout << "UnSerialize failed!" << std::endl;return -1;}std::cout << "反序列化结果:\n"<< "name:" << root["name"].asString() << "\n"<< "age:" << root["age"].asInt() << "\n"<< "socre:" << root["socre"][0].asFloat() << " " << root["socre"][1].asInt()<< " " << root["socre"][2].asFloat() << "\n";return 0;
}
序列化和反序列化的结果:
[myl@node test_jsoncpp]$ g++ test_json.cpp -o test_json -ljsoncpp -std=c++11
[myl@node test_jsoncpp]$ ./test_json
序列化结果:
{"age" : 19,"name" : "zhangsan","socre" :[77.5,88,99.5]
}
反序列化结果:
name:zhangsan
age:19
socre:77.5 88 99.5
7.MySQL API
7.1MySQL API介绍
• MySQL 是 C/S 模式, C API 其实就是⼀个 MySQL 客户端,提供⼀种⽤C语⾔代码操作数据
库的流程
// 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// 返回值:成功返回句柄指针,失败返回NULLMYSQL *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, 失败返回⾮0int mysql_select_db(MYSQL *mysql, const char *db)// 执⾏sql语句// 参数说明:// mysql--初始化完成的句柄// stmt_str--要执⾏的sql语句// 返回值:成功返回0, 失败返回⾮0int mysql_query(MYSQL *mysql, const char *stmt_str)// 保存查询结果到本地// 参数说明:// mysql--初始化完成的句柄// 返回值:成功返回结果集的指针, 失败返回NULLMYSQL_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)
7.2MySQL API使用
创建测试数据库:
create database if not exists test_db;
use test_db;
create table stu(
id int primary key auto_increment, -- 学⽣id
age int, -- 学⽣年龄
name varchar(32) -- 学⽣姓名
);
连接MySQL服务,进⼊shell并执行sql语句:
[myl@node test_mysql]$ mysql -uroot -p123456
MariaDB [(none)]> create database if not exists test_db;
Query OK, 1 row affected (0.01 sec)
MariaDB [(none)]> use test_db;
Database changed
MariaDB [test_db]> create table stu(
-> id int primary key auto_increment, -- 学⽣id
-> age int, -- 学⽣年龄
-> name varchar(32) -- 学⽣姓名
-> );
Query OK, 0 rows affected (0.02 sec)
7.3实现增删改查操作
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <mysql/mysql.h>
#define HOST "127.0.0.1"
#define USER "root"
#define PASSWD "123456" //例
#define DBNAME "test_db"
void add(MYSQL *mysql)
{char *sql = "insert into stu values(null, 18, '张三'), (null, 17, '李四');";int ret = mysql_query(mysql, sql);if (ret != 0){printf("mysql query error:%s\n", mysql_error(mysql));return;}return;
}
void del(MYSQL *mysql)
{char *sql = "delete from stu where name='张三';";int ret = mysql_query(mysql, sql);if (ret != 0){printf("mysql query error:%s\n", mysql_error(mysql));return;}return;
}
void mod(MYSQL *mysql)
{char *sql = "update stu set age=15 where name='张三';";int ret = mysql_query(mysql, sql);if (ret != 0){printf("mysql query error:%s\n", mysql_error(mysql));return;}return;
}
void get(MYSQL *mysql)
{char *sql = "select * from stu;";int ret = mysql_query(mysql, sql);if (ret != 0){printf("mysql query error:%s\n", mysql_error(mysql));return;}MYSQL_RES *res = mysql_store_result(mysql);if (res == NULL){printf("mysql store result error:%s\n", mysql_error(mysql));return;}int row = mysql_num_rows(res);int col = mysql_num_fields(res);printf("%10s%10s%10s\n", "ID", "年龄", "姓名");for (int i = 0; i < row; i++){MYSQL_ROW row_data = mysql_fetch_row(res);for (int i = 0; i < col; i++){printf("%10s", row_data[i]);}printf("\n");}mysql_free_result(res);return;
}
int main()
{MYSQL *mysql = mysql_init(NULL);if (mysql == NULL){printf("init mysql handle failed!\n");return -1;}if (mysql_real_connect(mysql, HOST, USER, PASSWD, DBNAME, 0, NULL, 0) ==NULL){printf("mysql connect error:%s\n", mysql_error(mysql));return -1;}mysql_set_character_set(mysql, "utf8");printf("===================== add =========================\n");add(mysql);get(mysql);printf("===================== mod =========================\n");mod(mysql);get(mysql);printf("===================== del =========================\n");del(mysql);get(mysql);mysql_close(mysql);return 0;
}
验证结果:
[myl@node test_mysql]$ g++ test_mysql.cpp -o test_mysql -L/usr/lib64/mysql -lmys
[myl@node test_mysql]$ ./test_mysql
===================== add =========================ID 年龄 姓名11 18 张三12 17 李四
===================== mod =========================ID 年龄 姓名11 15 张三12 17 李四
===================== del =========================ID 年龄 姓名12 17 李四
8.项目结构设计
8.1项目模块划分说明
项目的实现,将其划分为三个大模块来进行:
数据管理模块:基于MySQL数据库进行用户数据的管理
前端界面模块:基于JS实现前端页面(注册,登录,游戏大厅,游戏房间)的动态控制以及与服务器
的通信。
业务处理模块:搭建WebSocket服务器与客户端进行通信,接收请求并进行业务处理。
8.2业务处理模块的子模块划分
网络通信模块:基于websocketpp库实现Http&WebSocket服务器的搭建,提供网络通信功能。
会话管理模块:对客户端的连接进行cookie&session管理,实现http短连接时客户端身份识别功
能。
在线管理模块:对进入游戏大厅与游戏房间中用户进行管理,提供用户是否在线以及获取用户连接
的功能。
房间管理模块:为匹配成功的用户创建对战房间,提供实时的五子棋对战与聊天业务功能。
用户匹配模块:根据天梯分数不同进行不同层次的玩家匹配,为匹配成功的玩家创建房间并加入房
间。
8.3项目流程图
8.3.1玩家用户角度流程图:
8.3.2 服务器流程结构图:
9.实用工具类模块代码实现
9.1 日志宏封装
#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include<stdio.h>
#include<time.h>#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL DBG
#define LOG(level,format,...) do{\if(level < DEFAULT_LOG_LEVEL) break;\time_t t = time(NULL);\struct tm* lt = localtime(&t);\char buf[32] = { 0 };\strftime(buf,31,"%H:%M:%S",lt);\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(DBG,format,##__VA_ARGS__);
#define ELOG(format,...) LOG(ERR,format,##__VA_ARGS__);
#endif
9.2 MySQL-API封装
class mysql_util
{
public:static MYSQL* mysql_create(const std::string& host,const std::string& username,const std::string& password,const std::string& dbname,uint16_t port = 3306){MYSQL* mysql = mysql_init(NULL);if(mysql == NULL){ELOG("mysql_init failed!");return NULL;}//连接服务器:if(mysql_real_connect(mysql,host.c_str(),username.c_str(),password.c_str(),dbname.c_str(),port,NULL,0) == NULL){ELOG("connect mysql server fail:%s",mysql_error(mysql));mysql_close(mysql);return NULL;}//设置客户端字符集:if(mysql_set_character_set(mysql,"utf8") != 0){ELOG("set client character fail:%s",mysql_error(mysql));mysql_close(mysql);return NULL;}return mysql;}static bool mysql_exec(MYSQL*mysql,const std::string& sql){int ret = mysql_query(mysql,sql.c_str());if(ret != 0){ELOG("%s\n",sql.c_str());ELOG("mysql query failed:%s\n",mysql_error(mysql));return false;}return true;}static void mysql_destroy(MYSQL*mysql){if(mysql != NULL){mysql_close(mysql);}return;}
};
9.3 Jsoncpp-API封装
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 serialize failed!");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;bool ret = cr->parse(str.c_str(),str.c_str()+str.size(),&root,&err);if(ret == false) {ELOG("json unserialize falied!!");return false;}return true;}
};
9.4 String-Split封装:
class string_util
{
public:static int split(const std::string& src,const std::string& sep,std::vector<std::string>& res) {//123,123,,,456size_t pos = 0;size_t idx = 0;while(idx < src.size()) {pos = src.find(sep,idx);if(pos == std::string::npos) {res.push_back(src.substr(idx));break;}if(pos == idx) {idx += sep.size();continue;}res.push_back(src.substr(idx,pos-idx));idx += pos + sep.size();}return res.size();}
};
9.5 File-read封装
class file_util
{
public:static bool read(std::string& filename,std::string& body) {//打开文件:std::ifstream ifs(filename,std::ios::binary);if(ifs.is_open() == false) {ELOG("%s file open failed!",filename.c_str());return false;}//获取文件大小:size_t fsize = 0;ifs.seekg(0,std::ios::end);//获取偏移量fsize = ifs.tellg();ifs.seekg(0,std::ios::beg);body.resize(fsize);//将文件所有的数据读取出来:ifs.read(&body[0],fsize);if(ifs.good() == false) {ELOG("read %s file content failed!",filename.c_str());ifs.clear();return false;}ifs.close();return true;}
};
10. 数据管理模块实现
数据管理模块主要负责对于数据库中数据进行统一的增删改查管理,其它模块要对数据操作都必须通过数据管理模块完成。
10.1数据库设计
创建user表, 用来表示用户信息及积分信息
用户信息,用来实现登录、注册、游戏对战数据管理等功能
积分信息,用来实现匹配功能
drop database if exists gobang;
create database if not exists gobang;
use gobang;
create table if not exists user(id int primary key auto_increment,username varchar(32) unique key not null,password varchar(128) not null,score int,total_count int,win_count int
);
10.2创建user_table类
数据库中有可能存在很多张表,每张表中管理的数据又有不同,要进行的数据操作也各不相同,因此我们可以为每一张表中的数据操作都设计--个类,通过类实例化的对象来访问这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。
创建user_ table类,该类的作用是负责通过MySQL接口管理用户数据。主要提供了四个方法:
●select_by_name:根据用户名查找用户信息,用于实现登录功能
●insert: 新增用户,用户实现注册功能
●login:登录验证,并获取完整的用户信息
●win:用于给获胜玩家修改分数
●lose:用户给失败玩家修改分数
#ifndef __M_DB_H__
#define __M_DB_H__#include"util.hpp"
#include <mutex>
#include <cassert>
class user_table
{
public:user_table(const std::string& host,const std::string& username,const std::string& password,const std::string& dbname,uint16_t port = 3306){_mysql = mysql_util::mysql_create(host,username,password,dbname,port);assert(_mysql != NULL);}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql = NULL; }//注册时新增用户:bool insert(Json::Value& user){
#define INSERT_USER "insert user values(null,'%s',password('%s'),1000,0,0);"//sprintf(void* buf,format,...)if(user["password"].isNull() || user["username"].isNull()) {DLOG("INPUT PASSWORD OR USERNAMR\n");return false;}char sql[4096] = { 0 };sprintf(sql,INSERT_USER,user["username"].asCString(),user["password"].asCString());bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false) {DLOG("insert user info failed!!\n");return false;}return true;}//登录验证,并返回用户的详细信息:bool login(Json::Value& user){if(user["password"].isNull() || user["username"].isNull()) {DLOG("INPUT PASSWORD OR USERNAMR\n");return false;}//以用户和密码共同作为查询过滤条件,查询到数据,用户名和密码一致,没有信息则用户名和密码错误#define LOGIN_USER "select id,score,total_count,win_count from user where username='%s' and password=password('%s');"char sql[4096] = { 0 };sprintf(sql,LOGIN_USER,user["username"].asCString(),user["password"].asCString());MYSQL_RES* res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false) {DLOG("user login info failed!!\n");return false;}//按理说要么有数据,要么没有数据,就算有数据也只能有一条数据:res = mysql_store_result(_mysql);if(res == NULL) {DLOG("have no login user info!");return false;}int row_num = mysql_num_rows(res);if(row_num != 1) {DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stol(row[0]);user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}}//通过用户名获取用户信息:bool select_by_name(const std::string&name,Json::Value& user){#define USER_BY_NAME "select id,score,total_count,win_count from user where username='%s';"char sql[4096] = { 0 };sprintf(sql,USER_BY_NAME,name.c_str());MYSQL_RES* res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false) {DLOG("get user by name failed!!\n");return false;}//按理说要么有数据,要么没有数据,就算有数据也只能有一条数据res = mysql_store_result(_mysql);if (res == NULL) {DLOG("have no user info!!");return false;}}int row_num = mysql_num_rows(res);if(row_num != 1) {DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stol(row[0]);user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}//通过id获取用户信息:bool select_by_id(uint64_t id,Json::Value& user){#define USER_BY_ID "select id,score,total_count,win_count from user where id='%d';"char sql[4096] = { 0 };sprintf(sql,USER_BY_ID,id);MYSQL_RES* res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false) {DLOG("get user by name failed!!\n");return false;}//按理说要么有数据,要么没有数据,就算有数据也只能有一条数据res = mysql_store_result(_mysql);if (res == NULL) {DLOG("have no user info!!");return false;}}int row_num = mysql_num_rows(res);if(row_num != 1) {DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stol(row[0]);user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}//胜利时天梯分数增加30,战斗场次增加1,胜利场次增加1:bool win(uint64_t id){#define USER_WIN "update user set score=score+30,total_count=total_count+1,win_count=win_count+1 where id=%d;"char sql[4096] = { 0 };sprintf(sql,USER_WIN,id);bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false) {DLOG("update win user info failed!!\n");return false;}return true;}//失败时天梯分数减少30,战斗场次增加1:bool lose(uint64_t id){#define USER_LOSE "update user set score=score-30,total_count=total_count+1 where id='%d;"char sql[4096] = { 0 };sprintf(sql,USER_LOSE,id);bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false) {DLOG("update lose user info failed!!\n");return false;}return true;}
private:MYSQL* _mysql; //mysql操作句柄std::mutex _mutex;//互斥锁保护数据库的访问操作};#endif
11.在线用户管理模块实现
在线用户管理,是对于当前游戏大厅和游戏房间中的用户进行管理,主要是建立起用户与Socket连接的映射关系,这个模块具有两个功能:
1.能够让程序中根据用户信息,进而找到能够与用户客户端进行通信的Socket连接,进而实现与客户端的通信。
2.判断一个用户是否在线,或者判断用户是否已经掉线。
#ifndef __M_ONLINE_H__
#define __M_ONLINE_H__
#include "util.hpp"
#include <mutex>
#include <unordered_map>
class online_manager
{
private:std::mutex _mutex;//用于建立游戏大厅的用户的用于ID与通信连接的关系:std::unordered_map<uint64_t,wssever_t::connection_ptr> _hall_user;//用于建立游戏房间的用户ID与通信连接的关系std::unordered_map<uint64_t,wssever_t::connection_ptr> _room_user;
public://websocket建立连接的时候才会加入游戏大厅和游戏房间在线用户管理:void enter_game_hall(uint64_t uid,wssever_t::connection_ptr&conn) {std::unique_lock<std::mutex> lock(_mutex);_hall_user.insert(std::make_pair(uid,conn));}void enter_game_room(uint64_t uid,wssever_t::connection_ptr& conn) {std::unique_lock<std::mutex> lock(_mutex);_room_user.insert(std::make_pair(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在游戏大厅/游戏房间用户管理中获取对应的通信连接:wssever_t::connection_ptr get_conn_from_hall(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _hall_user.find(uid);if(it == _hall_user.end()) {return wssever_t::connection_ptr();}return it->second;}wssever_t::connection_ptr get_conn_from_room(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _room_user.find(uid);if(it == _room_user.end()) {return wssever_t::connection_ptr();}return it->second;}
};
#endif
12.游戏房间管理模块
12.1房间类实现
首先,需要设计一个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建立一个小范围的关联关系,一个房间中任意一个用户发生的任何动作,都会被广播给房间中的其他用户。
而房间中的动作主要包含两类:
1.棋局对战
2.实时聊天
#ifndef __M_ROOM_H__
#define __M_ROOM_H__
#include "util.hpp"
#include "logger.hpp"
#include "online.hpp"
#include "db.hpp"
#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2
typedef enum { GAME_START, GAME_OVER }room_statu;
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) {//从下棋位置的四个方向检测是否出现了五个相同颜色的棋子: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)){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_user_white(uint64_t uid) {_white_id = uid;_player_count++;}void add_user_black(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_rsep = req;/*判断房间中的两个玩家是否都在先,一方不在线,就是另一方胜利*/int chess_row = req["row"].asInt();int chess_col = req["col"].asInt();uint64_t cur_uid = req["uid"].asUInt64();if(_online_user->is_in_game_room(_white_id) == false) {json_rsep["result"] = true;json_rsep["reason"] = "运气真好,对方掉线了,不战而胜!";json_rsep["winner"] = (Json::UInt64)_black_id;return json_rsep;}if(_online_user->is_in_game_room(_black_id) == false) {json_rsep["result"] = true;json_rsep["reason"] = "运气真好,对方掉线了,不战而胜!";json_rsep["winner"] = (Json::UInt64)_white_id;return json_rsep;}/*获取走棋位置,判断当前位置是否被占用*/if(_board[chess_row][chess_col] != 0) {json_rsep["result"] = false;json_rsep["reason"] = "当前位置已经有了其它棋子";return json_rsep;}/*判断是否有玩家胜利,(五星连珠则胜利)*/int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;_board[chess_col][chess_row] = cur_color;uint64_t win_id = check_win(chess_row,chess_col,cur_color);if(win_id != 0) {json_rsep["reason"] = "五星连珠,战无敌";}json_rsep["result"] = true;json_rsep["winner"] = (Json::UInt64)win_id;return json_rsep;}/*处理聊天动作*/Json::Value handle_chat(Json::Value& req) {Json::Value json_resp = req;/*检测消息中是否包含敏感词*/std::string msg = req["message"].asString();size_t pos = msg.find("垃圾");if(pos != std::string::npos) {json_resp["result"] = false;json_resp["reason"] = "消息中含有敏感词,不能发送";return json_resp;}//广播消息->返回消息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;boardcast(json_resp);}//房间中玩家的数量-1_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"] = "房间号不匹配!";return boardcast(json_resp);}//2.根据不同的请求调用不同的处理函数:if(req["optype"].asString() == "put_chess") {json_resp = handle_chat(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"] = "未知请求类型";}std::string body;json_util::serialize(json_resp,body);DLOG("房间-广播动作%s",body.c_str());return boardcast(json_resp);}//将指定的消息广播给房间中的其它玩家:void boardcast(Json::Value& req) {//1.对要响应的消息序列化,将Json::Value中的数据序列化成为Json格式字符串:std::string body;json_util::serialize(req,body);//2.获取房间中所有用户的通信连接//3.发送响应信息:wssever_t::connection_ptr wconn = _online_user->get_conn_from_room(_white_id);if(wconn.get() != nullptr) {wconn->send(body);}else {DLOG("房间-白棋玩家连接获取失败!");}wssever_t::connection_ptr bconn = _online_user->get_conn_from_room(_black_id);if(bconn.get() != nullptr) {bconn->send(body);}else {DLOG("房间-黑棋玩家连接获取失败!");}return;}
};
12.2房间管理类实现
using room_ptr = std::shared_ptr<room>;
class room_manager
{
private:uint64_t _next_rid; //房间IDstd::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* om):_next_rid(1),_tb_user(ut),_online_user(om) {DLOG("房间管理模块初始化完毕!");}~room_manager(){DLOG("房间管理模块即将被销毁!");}/*为两个用户创建房间,并返回房间的智能指针管理对象*/room_ptr create_room(uint64_t uid1,uint64_t uid2) {//两个用户在大厅中匹配,匹配成功后创建房间//1.校验两个用户是否都还在大厅中,只有都在才需要创建房间:if(_online_user->is_in_game_hall(uid1) == false) {DLOG("用户:%lu 不再大厅中,房间创建失败!",uid1);return room_ptr();}if(_online_user->is_in_game_hall(uid2) == false) {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_user_white(uid1);rp->add_user_black(uid2);//3.将房间信息管理起来:_rooms.insert(std::make_pair(_next_rid,rp));_users.insert(std::make_pair(uid1,_next_rid));_users.insert(std::make_pair(uid2,_next_rid));_next_rid++;return rp;}/*通过房间ID获取房间信息*/room_ptr get_room_by_rid(uint64_t rid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _rooms.find(rid);if(it == _rooms.end()) {return room_ptr();}return it->second;}//通过用户ID获取房间信息:room_ptr get_room_by_uid(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);//1.通过用户ID获取房间IDauto uit = _users.find(uid);if(uit == _users.end()) {return room_ptr();}uint64_t rid = uit->second;//2.通过房间ID获取房间信息:auto it = _rooms.find(rid);if(it == _rooms.end()) {return room_ptr();}return it->second;}//通过房间ID销毁房间:void remove_room(uint64_t rid) {//因为房间信息是通过shared_ptr进行管理的,当引用计数减为0,外界不要做任何操作//自动会进行释放://1.通过房间ID,获取房间信息room_ptr rp = get_room_by_rid(rid);if(rp.get() == nullptr) {return;}//2.通过房间信息,获取房间中所有用户的IDuint64_t uid1 = rp->get_black_user();uint64_t uid2 = rp->get_white_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;}
};
#endif
13. session管理模块设计
13.1什么是session
在WEB开发中,HTTP协议是一种无状态短链接的协议,这就导致一个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。
因此,服务器为每个用户浏览器创建一个会话对象(session对象) ,注意: 一个浏览器独占一个
session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。
13.2 session工作原理
13.3 session类设计实现
这里我们简单的设计一个session类,但是session对象不能一 直存在,这样是一种资源泄漏,因此需要时使用定时器对每个创建的session对象进行定时销毁(一个客户端连接断开后,一段时间内都没有重新连接则销毁session)。
_ssid使用时间戳填充。实际上,我们通常使用唯一id生成器生成一个唯一的id
_user保存当前用户的信息
timer_ptr_tp保存当前session对应的定时销毁任务
typedef enum {UNLOGIN, LOGIN} ss_statu;
class session {private:uint64_t _ssid;//标识符uint64_t _uid;//session对应的用户IDss_statu _statu;//用户状态:未登录,已登录wssever_t::timer_ptr _tp;//session关联的定时器public:session(uint64_t ssid): _ssid(ssid){ DLOG("SESSION %p 被创建!!", this); }~session() { DLOG("SESSION %p 被释放!!", this); }uint64_t ssid() { return _ssid; }void set_statu(ss_statu statu) { _statu = statu; }void set_user(uint64_t uid) { _uid = uid; }uint64_t get_user() { return _uid; }bool is_login() { return (_statu == LOGIN); }void set_timer(const wssever_t::timer_ptr &tp) { _tp = tp;}wssever_t::timer_ptr& get_timer() { return _tp; }
};
13.4 session管理设计实现
session的管理主要包含以下几个点:
1.创建一个新的session
2.通过ssid获取session
3.通过ssid判断session是 否存在
4.销毁session。
5.为session设置过期时间,过期后session被销毁
#define SESSION_TIMEOUT 30000
#define SESSION_FOREVER -1
using session_ptr = std::shared_ptr<session>;
class session_manager {private:uint64_t _next_ssid;std::mutex _mutex;std::unordered_map<uint64_t, session_ptr> _session;wssever_t *_server;public:session_manager(wssever_t *srv): _next_ssid(1), _server(srv){DLOG("session管理器初始化完毕!");}~session_manager() { DLOG("session管理器即将销毁!"); }session_ptr create_session(uint64_t uid, ss_statu statu) {std::unique_lock<std::mutex> lock(_mutex);session_ptr ssp(new session(_next_ssid));ssp->set_statu(statu);ssp->set_user(uid);_session.insert(std::make_pair(_next_ssid, ssp));_next_ssid++;return ssp;}void append_session(const session_ptr &ssp) {std::unique_lock<std::mutex> lock(_mutex);_session.insert(std::make_pair(ssp->ssid(), ssp));}session_ptr get_session_by_ssid(uint64_t ssid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _session.find(ssid);if (it == _session.end()) {return session_ptr();}return it->second;}void remove_session(uint64_t ssid) {std::unique_lock<std::mutex> lock(_mutex);_session.erase(ssid);}void set_session_expire_time(uint64_t ssid, int ms) {//依赖于websocketpp的定时器来完成session生命周期的管理。// 登录之后,创建session,session需要在指定时间无通信后删除// 但是进入游戏大厅,或者游戏房间,这个session就应该永久存在// 等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除session_ptr ssp = get_session_by_ssid(ssid);if (ssp.get() == nullptr) {return;}wssever_t::timer_ptr tp = ssp->get_timer();if (tp.get() == nullptr && ms == SESSION_FOREVER) {// 1. 在session永久存在的情况下,设置永久存在return ;}else if (tp.get() == nullptr && ms != SESSION_FOREVER) {// 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务wssever_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tmp_tp);}else if (tp.get() != nullptr && ms == SESSION_FOREVER) {// 3. 在session设置了定时删除的情况下,将session设置为永久存在// 删除定时任务--- stready_timer删除定时任务会导致任务直接被执行tp->cancel();//因为这个取消定时任务并不是立即取消的//因此重新给session管理器中,添加一个session信息, 且添加的时候需要使用定时器,而不是立即添加ssp->set_timer(wssever_t::timer_ptr());//将session关联的定时器设置为空_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));}else if (tp.get() != nullptr && ms != SESSION_FOREVER) {// 4. 在session设置了定时删除的情况下,将session重置删除时间。tp->cancel();//因为这个取消定时任务并不是立即取消的ssp->set_timer(wssever_t::timer_ptr());_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));//重新给session添加定时销毁任务wssever_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssp->ssid()));//重新设置session关联的定时器ssp->set_timer(tmp_tp);}}
};
14.五子棋对战玩家匹配管理设计实现
14.1匹配队列实现
五子棋对战的玩家匹配是根据自己的天梯分数进行匹配的,而服务器中将玩家天梯分数分为三个档
次:
1.青铜:天梯分数小于2000分
2.白银:天梯分数介于2000~3000分之间
3.黄金:天梯分数大于3000分
而实现玩家匹配的思想非常简单,为不同的档次设计各自的匹配队列,当一个队列中的玩家数量大于等于2的时候,则意味着同一档次中,有2个及以上的人要进行实战匹配,则出队队列中的前两个用户,相当于队首2个玩家匹配成功,这时候为其创建房间,并将两个用户信息加入房间中。
template<class T>
class match_queue
{
private:/*用链表而不直接使用queue是因为我们有中间删除数据的需要*/std::list<T> _list;/*实现线程安全*/std::mutex _mutex;/*这个条件变量主要为了阻塞消费者,后边使用的时候:队列中元素个数<2则阻塞*/std::condition_variable _cond;
public:/*获取元素个数*/int size() {std::unique_lock<std::mutex> lock(_mutex);return _list.size();}/*判断是否为空*/bool empty() {std::unique_lock<std::mutex> lock(_mutex);return _list.empty();}/*阻塞线程*/void wait() {std::unique_lock<std::mutex> lock(_mutex);_cond.wait(lock);}/*入队数据,并唤醒线程*/void push(T& data) {std::unique_lock<std::mutex> lock(_mutex);_list.push_back(data);_cond.notify_all();}/*出队数据*/bool pop(T& data) {std::unique_lock<std::mutex> lock(_mutex);if(_list.empty()) {return false;}data = _list.front();_list.pop_front();return true;}/*移除指定的数据*/void remove(T&data) {std::unique_lock<std::mutex> lock(_mutex);_list.remove(data);}
};
14.2 玩家匹配管理模块设计实现
template<class T>
class matcher
{
private:/*普通选手匹配队列*/match_queue<uint64_t> _q_normal;/*高手匹配队列*/match_queue<uint64_t> _q_high;/*大神匹配队列*/match_queue<uint64_t> _q_super;/*对应三个匹配队列的处理线程*/std::thread _th_normal;std::thread _th_high;std::thread _th_super;room_manager* _rm;user_table* _ut;online_manager* _om;
private:void handle_match(mathch_queue<uint64_t>& mq) {while(1) {//1. 判断队列人数是否大于2,<2则阻塞等待while(mq.size() < 2)mq.wait();//2. 走下来代表人数够了,出队两个玩家uint64_t uid1,uid2;bool ret = mq.pop(uid1);if(ret == false) continue;ret = mq.pop(uid2) continue;//3. 校验两个玩家是否在线,如果有人掉线,则要吧另一个人重新添加入队列wssever_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);if(conn1.get() == nullptr) {this->add(uid2);continue;}wssever_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);if(conn2.get() == nullptr) {this->add(uid1);continue;}//4. 为两个玩家创建房间,并将玩家加入房间中room_ptr rp = _rm->create_room(uid1,uid2);if(rp.get() == nullptr) {this->add(uid1);this->add(uid2);continue;}//5.对两个玩家进行响应:Json::Value resp;resp["optype"] = "match_success";resp["result"] = true;std::string body;json_util::serialize(resp,body);conn1->send(body);conn2->send(body);}}void th_normal_entry() { return handle_match(_q_normal); }void th_high_entry() { return handle_match(_q_high); }void th_super_entry() { return handle_match(_q_super); }
public:matcher(room_manager* rm,user_table* ut,online_manager* om):_rm(rm),_ut(ut),_om(om),_th_normal(std::thread(&matcher::th_normal_entry,this)),_th_high(std::thread(&matcher::th_high_entry,this)),_th_super(std::thread(&matcher::th_super_entry,this)){DLOG("游戏匹配模块初始化完毕!");}//根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列bool add(uint64_t id){// 1. 根据用户ID,获取玩家信息Json::Value user;bool ret = _ut->select_by_id(id,user);if(ret == false) {DLOG("获取玩家:%d 信息失败",id);return false;}int score = user["score"].asInt();//2.添加到指定的队列中:if(score < 2000) _q_normal.push(id);else if(score >= 2000 && score < 3000) _q_high.push(id);else _q_super.push(id);return true;}bool del(uint64_t id){Json::Value user;bool ret = _ut->select_by_id(id,user);if(ret == false) {DLOG("获取玩家:%d 信息失败",id);return false;}int score = user["score"].asInt();//2.删除队列中指定的元素:if(score < 2000) _q_normal.remove(id);else if(score >= 2000 && score < 3000) _q_high.push(id);else _q_super.push(id);return true;}
};
15.整合封装服务器模块设计实现
服务器模块,是对当前所实现的所有模块的一个整合,并进行服务器搭建的一个模块,最终封装实现出一个gobang_ server的服务器模块类,向外提供搭建五子棋对战服务器的接口。通过实例化的对象可以简便的完成服务器的搭建。
15.1通信接口设计(Restful风格)
15.1.1静态资源请求
静态资源页面,在后台服务器上就是个html/css/js文件
静态资源请求的处理,其实就是将文件中的内容发送给客户端
1.注册页面请求
请求: GET /register .html HTTP/1.1
响应:
HTTP/1.1 200 0K
Content-L ength: 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
15.1.2注册用户
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": "用户名已经被占用"}
15.1.3用户登录
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": "用户名或密码错误"}
15.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: 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": "用户还未登录"}
15.1.5 websocket长连接协议切换请求(进入游戏大厅)
/* ws://localhost:9000/match */
GET /match HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......
HTTP/1.1 101 Switching
......
WebSocket握⼿成功后的回复:表示游戏⼤厅已经进入成功。
{
"optype": "hall_ready",
"uid": 1
}
15.1.6 开始对战匹配
{
"optype": "match_start"
}/*后台正确处理后回复*/
{
"optype": "match_start", //表⽰成功加⼊匹配队列
"result": true
}
/*后台处理出错回复*/
{
"optype": "match_start"
"result": false,
"reason": "具体原因...."
}/*匹配成功了给客⼾端的回复*/
{
"optype": "match_success", //表⽰成匹配成功
"result": true
}
15.1.7 停止匹配
{"optype": "match_stop"
}
/*后台正确处理后回复*/
{"optype": "match_stop""result": true
}
/*后台处理出错回复*/
{"optype": "match_stop""result": false,"reason": "具体原因...."
}
15.1.8 websocket长连接协议切换请求(进入游戏房间)
/* ws://localhost:9000/game */
GET /game 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
}
15.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": 222,
"uid": 1,
"row": 3,
"col": 2,
"winner": 0 // 0-未分胜负, !0-已分胜负 (uid是谁,谁就赢了)
}
15.1.10 聊天
{
"optype": "chat",
"room_id": 222,
"uid": 1,
"message": "赶紧点"
}
{
"optype": "chat",
"result": false
"reason": "聊天失败具体原因....⽐如有敏感词..."
}
{
"optype": "chat",
"result": true,
"room_id": 222,
"uid": 1,
"message": "赶紧点"
}
15.2 服务器模块实现
登陆页面:login.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>登录</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/login.css">
</head>
<body><div class="nav">网络五子棋对战游戏</div><div class="login-container"><!-- 登录界面的对话框 --><div class="login-dialog"><!-- 提示信息 --><h3>登录</h3><!-- 这个表示一行 --><div class="row"><span>用户名</span><input type="text" id="user_name"></div><!-- 这是另一行 --><div class="row"><span>密码</span><input type="password" id="password"></div><!-- 提交按钮 --><div class="row"><button id="submit" onclick="login()">提交</button></div></div></div><script src="./js/jquery.min.js"></script><script>//1. 给按钮添加点击事件,调用登录请求函数//2. 封装登录请求函数function login() {// 1. 获取输入框中的用户名和密码,并组织json对象var login_info = {username: document.getElementById("user_name").value,password: document.getElementById("password").value};// 2. 通过ajax向后台发送登录验证请求$.ajax({url: "/login",type: "post",data: JSON.stringify(login_info),success: function(result) {// 3. 如果验证通过,则跳转游戏大厅页面alert("登录成功");window.location.assign("/game_hall.html");},error: function(xhr) {// 4. 如果验证失败,则提示错误信息,并清空输入框alert(JSON.stringify(xhr));document.getElementById("user_name").value = "";document.getElementById("password").value = "";}})}</script>
</body>
</html>
注册页面:register.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>注册</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/login.css">
</head>
<body><div class="nav">网络五子棋对战游戏</div><div class="login-container"><!-- 登录界面的对话框 --><div class="login-dialog"><!-- 提示信息 --><h3>注册</h3><!-- 这个表示一行 --><div class="row"><span>用户名</span><input type="text" id="user_name" name="username"></div><!-- 这是另一行 --><div class="row"><span>密码</span><input type="password" id="password" name="password"></div><!-- 提交按钮 --><div class="row"><button id="submit" onclick="reg()">提交</button></div></div></div> <script src="js/jquery.min.js"></script><script>//1. 给按钮添加点击事件,调用注册函数//2. 封装实现注册函数function reg() {// 1. 获取两个输入框空间中的数据,组织成为一个json串var reg_info = {username: document.getElementById("user_name").value,password: document.getElementById("password").value};console.log(JSON.stringify(reg_info));// 2. 通过ajax向后台发送用户注册请求$.ajax({url : "/reg",type : "post",data : JSON.stringify(reg_info),success : function(res) {if (res.result == false) {// 4. 如果请求失败,则清空两个输入框内容,并提示错误原因document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(res.reason);}else {// 3. 如果请求成功,则跳转的登录页面alert(res.reason);window.location.assign("/login.html");}},error : function(xhr) {document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(JSON.stringify(xhr));}})}</script>
</body>
</html>
游戏大厅页面:game_hall.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>游戏大厅</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/game_hall.css">
</head>
<body><div class="nav">网络五子棋对战游戏</div><!-- 整个页面的容器元素 --><div class="container"><!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 --><div><!-- 展示用户信息 --><div id="screen"></div><!-- 匹配按钮 --><div id="match-button">开始匹配</div></div></div><script src="./js/jquery.min.js"></script><script>var ws_url = "ws://" + location.host + "/hall";var ws_hdl = null;window.onbeforeunload = function() {ws_hdl.close();}//按钮有两个状态:没有进行匹配的状态,正在匹配中的状态var button_flag = "stop";//点击按钮的事件处理:var be = document.getElementById("match-button");be.onclick = function() {if (button_flag == "stop") {//1. 没有进行匹配的状态下点击按钮,发送对战匹配请求var req_json = {optype: "match_start"}ws_hdl.send(JSON.stringify(req_json));}else {//2. 正在匹配中的状态下点击按钮,发送停止对战匹配请求var req_json = {optype: "match_stop"}ws_hdl.send(JSON.stringify(req_json));}}function get_user_info() {$.ajax({url: "/info",type: "get",success: function(res) {var info_html = "<p>" + "用户:" + res.username + " 积分:" + res.score + "</br>" + "比赛场次:" + res.total_count + " 获胜场次:" + res.win_count + "</p>";var screen_div = document.getElementById("screen");screen_div.innerHTML = info_html;ws_hdl = new WebSocket(ws_url);ws_hdl.onopen = ws_onopen;ws_hdl.onclose = ws_onclose;ws_hdl.onerror = ws_onerror;ws_hdl.onmessage = ws_onmessage;},error: function(xhr) {alert(JSON.stringify(xhr));location.replace("/login.html");}})}function ws_onopen() {console.log("websocket onopen");}function ws_onclose() {console.log("websocket onopen");}function ws_onerror() {console.log("websocket onopen");}function ws_onmessage(evt) {var rsp_json = JSON.parse(evt.data);if (rsp_json.result == false) {alert(evt.data);location.replace("/login.html");return;}if (rsp_json["optype"] == "hall_ready") {alert("游戏大厅连接建立成功!");}else if (rsp_json["optype"] == "match_success") {//对战匹配成功alert("对战匹配成功,进入游戏房间!");location.replace("/game_room.html");}else if (rsp_json["optype"] == "match_start") {console.log("玩家已经加入匹配队列");button_flag = "start";be.innerHTML = "匹配中....点击按钮停止匹配!";return;}else if (rsp_json["optype"] == "match_stop"){console.log("玩家已经移除匹配队列");button_flag = "stop";be.innerHTML = "开始匹配";return;}else {alert(evt.data);location.replace("/login.html");return;}}get_user_info();</script>
</body>
</html>
游戏房间页面:game_room.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>游戏房间</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/game_room.css">
</head>
<body><div class="nav">网络五子棋对战游戏</div><div class="container"><div id="chess_area"><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvas id="chess" width="450px" height="450px"></canvas><!-- 显示区域 --><div id="screen"> 等待玩家连接中... </div></div><div id="chat_area" width="400px" height="300px"><div id="chat_show"><p id="self_msg">你好!</p></br><p id="peer_msg">你好!</p></br><p id="peer_msg">leihoua~</p></br></div><div id="msg_show"><input type="text" id="chat_input"><button id="chat_button">发送</button></div></div></div><script>let chessBoard = [];let BOARD_ROW_AND_COL = 15;let chess = document.getElementById('chess');let context = chess.getContext('2d');//获取chess控件的2d画布var ws_url = "ws://" + location.host + "/room";var ws_hdl = new WebSocket(ws_url);var room_info = null;//用于保存房间信息 var is_me;function initGame() {initBoard();context.strokeStyle = "#BFBFBF";// 背景图片let logo = new Image();logo.src = "image/sky.jpeg";logo.onload = function () {// 绘制图片context.drawImage(logo, 0, 0, 450, 450);// 绘制棋盘drawChessBoard();}}function initBoard() {for (let i = 0; i < BOARD_ROW_AND_COL; i++) {chessBoard[i] = [];for (let j = 0; j < BOARD_ROW_AND_COL; j++) {chessBoard[i][j] = 0;}}}// 绘制棋盘网格线function drawChessBoard() {for (let i = 0; i < BOARD_ROW_AND_COL; i++) {context.moveTo(15 + i * 30, 15);context.lineTo(15 + i * 30, 430); //横向的线条context.stroke();context.moveTo(15, 15 + i * 30);context.lineTo(435, 15 + i * 30); //纵向的线条context.stroke();}}//绘制棋子function oneStep(i, j, isWhite) {if (i < 0 || j < 0) return;context.beginPath();context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);context.closePath();var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);// 区分黑白子if (!isWhite) {gradient.addColorStop(0, "#0A0A0A");gradient.addColorStop(1, "#636766");} else {gradient.addColorStop(0, "#D1D1D1");gradient.addColorStop(1, "#F9F9F9");}context.fillStyle = gradient;context.fill();}//棋盘区域的点击事件chess.onclick = function (e) {// 1. 获取下棋位置,判断当前下棋操作是否正常// 1. 当前是否轮到自己走棋了// 2. 当前位置是否已经被占用// 2. 向服务器发送走棋请求if (!is_me) {alert("等待对方走棋....");return;}let x = e.offsetX;let y = e.offsetY;// 注意, 横坐标是列, 纵坐标是行// 这里是为了让点击操作能够对应到网格线上let col = Math.floor(x / 30);let row = Math.floor(y / 30);if (chessBoard[row][col] != 0) {alert("当前位置已有棋子!");return;}//oneStep(col, row, true);//向服务器发送走棋请求,收到响应后,再绘制棋子send_chess(row, col);}function send_chess(r, c) {var chess_info = {optype : "put_chess",room_id: room_info.room_id,uid: room_info.uid,row: r,col: c};ws_hdl.send(JSON.stringify(chess_info));console.log("click:" + JSON.stringify(chess_info));}window.onbeforeunload = function() {ws_hdl.close();}ws_hdl.onopen = function() {console.log("房间长连接建立成功");}ws_hdl.onclose = function() {console.log("房间长连接断开");}ws_hdl.onerror = function() {console.log("房间长连接出错");}function set_screen(me) {var screen_div = document.getElementById("screen");if (me) {screen_div.innerHTML = "轮到己方走棋...";}else {screen_div.innerHTML = "轮到对方走棋...";}}ws_hdl.onmessage = function(evt) {//1. 在收到room_ready之后进行房间的初始化// 1. 将房间信息保存起来var info = JSON.parse(evt.data);console.log(JSON.stringify(info));if (info.optype == "room_ready") {room_info = info;is_me = room_info.uid == room_info.white_id ? true : false;set_screen(is_me);initGame();}else if (info.optype == "put_chess"){console.log("put_chess" + evt.data);//2. 走棋操作// 3. 收到走棋消息,进行棋子绘制if (info.result == false) {alert(info.reason);return;}//当前走棋的用户id,与我自己的用户id相同,就是我自己走棋,走棋之后,就轮到对方了is_me = info.uid == room_info.uid ? false : true;//绘制棋子的颜色,应该根据当前下棋角色的颜色确定isWhite = info.uid == room_info.white_id ? true : false;//绘制棋子if (info.row != -1 && info.col != -1){oneStep(info.col, info.row, isWhite);//设置棋盘信息chessBoard[info.row][info.col] = 1;}//是否有胜利者if (info.winner == 0) {return;}var screen_div = document.getElementById("screen");if (room_info.uid == info.winner) {screen_div.innerHTML = info.reason;}else {screen_div.innerHTML = "你输了";}var chess_area_div = document.getElementById("chess_area");var button_div = document.createElement("div");button_div.innerHTML = "返回大厅";button_div.onclick = function() {ws_hdl.close();location.replace("/game_hall.html");}chess_area_div.appendChild(button_div);} else if (info.optype == "chat") {//收到一条消息,判断result,如果为true则渲染一条消息到显示框中if(info.result == false) {alert(info.reason);return;}var msg_div = document.createElement("p");msg_div.innerHTML = info.message;if (info.uid == room_info.uid) {msg_div.setAttribute("id", "self_msg");}else {msg_div.setAttribute("id", "peer_msg");}var br_div = document.createElement("br");var msg_show_div = document.getElementById("chat_show");msg_show_div.appendChild(msg_div);msg_show_div.appendChild(br_div);document.getElementById("chat_input").value = "";}}//3. 聊天动作// 1. 捕捉聊天输入框消息// 2. 给发送按钮添加点击事件,点击俺就的时候,获取到输入框消息,发送给服务器var cb_div = document.getElementById("chat_button");cb_div.onclick = function() {var send_msg = {optype : "chat",room_id : room_info.room_id,uid : room_info.uid,message : document.getElementById("chat_input").value};ws_hdl.send(JSON.stringify(send_msg));}</script>
</body>
</html>
16.客户端开发
#ifndef __M_SRV_H__
#define __M_SRV_H__
#include "db.hpp"
#include "match.hpp"
#include "online.hpp"
#include "room.hpp"
#include "session.hpp"
#include "util.hpp"#define WWWROOT "./wwwroot/"
class gobang_server
{
private:std::string _web_root; // 静态资源根目录 ./wwwroot/ /register.html -> ./wwwroot/register.htmlwssever_t _wssrv;user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;private:void file_handler(wssever_t::connection_ptr &conn){// 静态资源请求的处理// 1. 获取到请求uri-资源路径,了解客户端请求的页面文件名称websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 2. 组合出文件的实际路径 相对根目录 + uristd::string realpath = _web_root + uri;// 3. 如果请求的是个目录,增加一个后缀 login.html, / -> /login.htmlif (realpath.back() == '/'){realpath += "login.html";}// 4. 读取文件内容Json::Value resp_json;std::string body;bool ret = file_util::read(realpath, body);// 1. 文件不存在,读取文件内容失败,返回404if (ret == false){body += "<html>";body += "<head>";body += "<meta charset='UTF-8'/>";body += "</head>";body += "<body>";body += "<h1> Not Found </h1>";body += "</body>";conn->set_status(websocketpp::http::status_code::not_found);conn->set_body(body);return;}// 5. 设置响应正文conn->set_body(body);conn->set_status(websocketpp::http::status_code::ok);}void http_resp(wssever_t::connection_ptr &conn, bool result,websocketpp::http::status_code::value code, const std::string &reason){Json::Value resp_json;resp_json["result"] = result;resp_json["reason"] = reason;std::string resp_body;json_util::serialize(resp_json, resp_body);conn->set_status(code);conn->set_body(resp_body);conn->append_header("Content-Type", "application/json");return;}void reg(wssever_t::connection_ptr &conn){// 用户注册功能请求的处理websocketpp::http::parser::request req = conn->get_request();// 1. 获取到请求正文std::string req_body = conn->get_request_body();// 2. 对正文进行json反序列化,得到用户名和密码Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DLOG("反序列化注册信息失败");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");}// 3. 进行数据库的用户新增操作if (login_info["username"].isNull() || login_info["password"].isNull()){DLOG("用户名密码不完整");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");}ret = _ut.insert(login_info);if (ret == false){DLOG("向数据库插入数据失败");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名已经被占用!");}// 如果成功了,则返回200return http_resp(conn, true, websocketpp::http::status_code::ok, "注册用户成功");}void login(wssever_t::connection_ptr &conn){// 用户登录功能请求的处理// 1. 获取请求正文,并进行json反序列化,得到用户名和密码std::string req_body = conn->get_request_body();Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DLOG("反序列化登录信息失败");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");}// 2. 校验正文完整性,进行数据库的用户信息验证if (login_info["username"].isNull() || login_info["password"].isNull()){DLOG("用户名密码不完整");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");}ret = _ut.login(login_info);if (ret == false){// 1. 如果验证失败,则返回400DLOG("用户名密码错误");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名密码错误");}// 3. 如果验证成功,给客户端创建sessionuint64_t uid = login_info["id"].asUInt64();session_ptr ssp = _sm.create_session(uid, LOGIN);if (ssp.get() == nullptr){DLOG("创建会话失败");return http_resp(conn, false, websocketpp::http::status_code::internal_server_error, "创建会话失败");}_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);// 4. 设置响应头部:Set-Cookie,将sessionid通过cookie返回std::string cookie_ssid = "SSID=" + std::to_string(ssp->ssid());conn->append_header("Set-Cookie", cookie_ssid);return http_resp(conn, true, websocketpp::http::status_code::ok, "登录成功");}bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val){// Cookie: SSID=XXX; path=/;// 1. 以 ; 作为间隔,对字符串进行分割,得到各个单个的cookie信息std::string sep = "; ";std::vector<std::string> cookie_arr;string_util::split(cookie_str, sep, cookie_arr);for (auto str : cookie_arr){// 2. 对单个cookie字符串,以 = 为间隔进行分割,得到key和valstd::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2){continue;}if (tmp_arr[0] == key){val = tmp_arr[1];return true;}}return false;}void info(wssever_t::connection_ptr &conn){// 用户信息获取功能请求的处理Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){// 如果没有cookie,返回错误:没有cookie信息,让客户端重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到cookie信息,请重新登录");}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false){// cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到ssid信息,请重新登录");}// 2. 在session管理中查找对应的会话信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() == nullptr){// 没有找到session,则认为登录已经过期,需要重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, "登录过期,请重新登录");}// 3. 从数据库中取出用户信息,进行序列化发送给客户端uint64_t uid = ssp->get_user();Json::Value user_info;ret = _ut.select_by_id(uid, user_info);if (ret == false){// 获取用户信息失败,返回错误:找不到用户信息return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到用户信息,请重新登录");}std::string body;json_util::serialize(user_info, body);conn->set_body(body);conn->append_header("Content-Type", "application/json");conn->set_status(websocketpp::http::status_code::ok);// 4. 刷新session的过期时间_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}void http_callback(websocketpp::connection_hdl hdl){wssever_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string method = req.get_method();std::string uri = req.get_uri();if (method == "POST" && uri == "/reg"){return reg(conn);}else if (method == "POST" && uri == "/login"){return login(conn);}else if (method == "GET" && uri == "/info"){return info(conn);}else{return file_handler(conn);}}void ws_resp(wssever_t::connection_ptr conn, Json::Value &resp){std::string body;json_util::serialize(resp, body);conn->send(body);}session_ptr get_session_by_cookie(wssever_t::connection_ptr conn){Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){// 如果没有cookie,返回错误:没有cookie信息,让客户端重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到cookie信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false){// cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到SSID信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}// 2. 在session管理中查找对应的会话信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() == nullptr){// 没有找到session,则认为登录已经过期,需要重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到session信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}return ssp;}void wsopen_game_hall(wssever_t::connection_ptr conn){// 游戏大厅长连接建立成功Json::Value resp_json;// 1. 登录验证--判断当前客户端是否已经成功登录session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2. 判断当前客户端是否是重复登录if (_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())){resp_json["optype"] = "hall_ready";resp_json["reason"] = "玩家重复登录!";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 3. 将当前客户端以及连接加入到游戏大厅_om.enter_game_hall(ssp->get_user(), conn);// 4. 给客户端响应游戏大厅连接建立成功resp_json["optype"] = "hall_ready";resp_json["result"] = true;ws_resp(conn, resp_json);// 5. 记得将session设置为永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);}void wsopen_game_room(wssever_t::connection_ptr conn){Json::Value resp_json;// 1. 获取当前客户端的sessionsession_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2. 当前用户是否已经在在线用户管理的游戏房间或者游戏大厅中---在线用户管理if (_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())){resp_json["optype"] = "room_ready";resp_json["reason"] = "玩家重复登录!";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 3. 判断当前用户是否已经创建好了房间 --- 房间管理room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr){resp_json["optype"] = "room_ready";resp_json["reason"] = "没有找到玩家的房间信息";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 4. 将当前用户添加到在线用户管理的游戏房间中_om.enter_game_room(ssp->get_user(), conn);// 5. 将session重新设置为永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);// 6. 回复房间准备完毕resp_json["optype"] = "room_ready";resp_json["result"] = true;resp_json["room_id"] = (Json::UInt64)rp->id();resp_json["uid"] = (Json::UInt64)ssp->get_user();resp_json["white_id"] = (Json::UInt64)rp->get_white_user();resp_json["black_id"] = (Json::UInt64)rp->get_black_user();return ws_resp(conn, resp_json);}void wsopen_callback(websocketpp::connection_hdl hdl){// websocket长连接建立成功之后的处理函数wssever_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建立了游戏大厅的长连接return wsopen_game_hall(conn);}else if (uri == "/room"){// 建立了游戏房间的长连接return wsopen_game_room(conn);}}void wsclose_game_hall(wssever_t::connection_ptr conn){// 游戏大厅长连接断开的处理// 1. 登录验证--判断当前客户端是否已经成功登录session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 1. 将玩家从游戏大厅中移除_om.exit_game_hall(ssp->get_user());// 2. 将session恢复生命周期的管理,设置定时销毁_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}void wsclose_game_room(wssever_t::connection_ptr conn){// 获取会话信息,识别客户端session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 1. 将玩家从在线用户管理中移除_om.exit_game_room(ssp->get_user());// 2. 将session回复生命周期的管理,设置定时销毁_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);// 3. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间_rm.remove_room_user(ssp->get_user());}void wsclose_callback(websocketpp::connection_hdl hdl){// websocket连接断开前的处理wssever_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建立了游戏大厅的长连接return wsclose_game_hall(conn);}else if (uri == "/room"){// 建立了游戏房间的长连接return wsclose_game_room(conn);}}void wsmsg_game_hall(wssever_t::connection_ptr conn, wssever_t::message_ptr msg){Json::Value resp_json;std::string resp_body;// 1. 身份验证,当前客户端到底是哪个玩家session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2. 获取请求信息std::string req_body = msg->get_payload();Json::Value req_json;bool ret = json_util::unserialize(req_body, req_json);if (ret == false){resp_json["result"] = false;resp_json["reason"] = "请求信息解析失败";return ws_resp(conn, resp_json);}// 3. 对于请求进行处理:if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start"){// 开始对战匹配:通过匹配模块,将用户添加到匹配队列中_mm.add(ssp->get_user());resp_json["optype"] = "match_start";resp_json["result"] = true;return ws_resp(conn, resp_json);}else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop"){// 停止对战匹配:通过匹配模块,将用户从匹配队列中移除_mm.del(ssp->get_user());resp_json["optype"] = "match_stop";resp_json["result"] = true;return ws_resp(conn, resp_json);}resp_json["optype"] = "unknow";resp_json["reason"] = "请求类型未知";resp_json["result"] = false;return ws_resp(conn, resp_json);}void wsmsg_game_room(wssever_t::connection_ptr conn, wssever_t::message_ptr msg){Json::Value resp_json;// 1. 获取客户端session,识别客户端身份session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){DLOG("房间-没有找到会话信息");return;}// 2. 获取客户端房间信息room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr){resp_json["optype"] = "unknow";resp_json["reason"] = "没有找到玩家的房间信息";resp_json["result"] = false;DLOG("房间-没有找到玩家房间信息");return ws_resp(conn, resp_json);}// 3. 对消息进行反序列化Json::Value req_json;std::string req_body = msg->get_payload();bool ret = json_util::unserialize(req_body, req_json);if (ret == false){resp_json["optype"] = "unknow";resp_json["reason"] = "请求解析失败";resp_json["result"] = false;DLOG("房间-反序列化请求失败");return ws_resp(conn, resp_json);}DLOG("房间:收到房间请求,开始处理....");// 4. 通过房间模块进行消息请求的处理return rp->handle_request(req_json);}void wsmsg_callback(websocketpp::connection_hdl hdl, wssever_t::message_ptr msg){// websocket长连接通信处理wssever_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建立了游戏大厅的长连接return wsmsg_game_hall(conn, msg);}else if (uri == "/room"){// 建立了游戏房间的长连接return wsmsg_game_room(conn, msg);}}public:/*进行成员初始化,以及服务器回调函数的设置*/gobang_server(const std::string &host,const std::string &user,const std::string &pass,const std::string &dbname,uint16_t port = 3306,const std::string &wwwroot = WWWROOT) : _web_root(wwwroot), _ut(host, user, pass, dbname, port),_rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om){_wssrv.set_access_channels(websocketpp::log::alevel::none);_wssrv.init_asio();_wssrv.set_reuse_addr(true);_wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1));_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1));_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));}/*启动服务器*/void start(int port){_wssrv.listen(port);_wssrv.start_accept();_wssrv.run();}
};
#endif
注:必须使用两个浏览器或者⼀个浏览器的无痕模式打开两个标签页,避免cookie和session相互影响导致检测到多开
项目总结
通过实现五子棋服务器,能够让用户通过浏览器访问服务器,进行用户的注册,登录,对战匹配,实时对战,实时聊天等功能,项目实现包含数据管理模块,网络服务模块,session管理模块,在线用户管理模块,游戏房间管理模块,对战匹配管理模块,业务处理。