网站后台管理系统,wordpress分类页首页调用分类描述,wordpress注册页面自动生成,wordpress不能播放wmv文章目录 一、项目介绍1. 项目简介2. 开发环境3. 核心技术4. 开发阶段 二、环境搭建1. 安装 wget 工具2. 更换 yum 源3. 安装 lrzsz 传输工具4. 安装⾼版本 gcc/g 编译器5. 安装 gdb 调试器6. 安装分布式版本控制工具 git7. 安装 cmake8. 安装 boost 库9. 安装 Jsoncpp 库10. 安… 文章目录 一、项目介绍1. 项目简介2. 开发环境3. 核心技术4. 开发阶段 二、环境搭建1. 安装 wget 工具2. 更换 yum 源3. 安装 lrzsz 传输工具4. 安装⾼版本 gcc/g 编译器5. 安装 gdb 调试器6. 安装分布式版本控制工具 git7. 安装 cmake8. 安装 boost 库9. 安装 Jsoncpp 库10. 安装 MySQL 数据库服务及开发包11. 安装 WebSocketpp 库 三、前置知识了解1. WebSocketpp1.1 WebSocket 协议1.2 WebSocketpp 2. JsonCpp3. C114. GDB5. MySQL C API6. HTML/CSS/JS/AJAX6.1 HTML 简单了解6.2 CSS 简单了解6.3 JS 简单了解6.4 AJAX 简单了解 四、框架设计1. 项目模块划分1.1 总体模块划分1.2 业务处理子模块划分 2. 项目流程图2.1 用户角度流程图2.2 服务器角度流程图 五、模块开发1. 实用工具类模块1.1 日志宏封装1.2 MySQL C API 封装1.3 JsonCpp 封装1.4 String Split 封装1.5 File Read 封装 2. 用户数据管理模块2.1 用户信息表2.2 用户数据管理类 3. 在线用户管理模块4. 游戏房间管理模块5. 用户 session 信息管理模块6. 匹配对战管理模块7. 整合封装服务器模块7.1 网络通信接口设计7.1.1 静态资源请求7.1.2 动态功能请求7.1.3 WebSocket 通信格式 7.2 服务器模块实现 8. 前端界面模块8.1 用户注册界面8.2 用户登录界面8.3 游戏大厅界面8.4 游戏房间界面 六、项目演示七、项目扩展八、项目总结 一、项目介绍
1. 项目简介
本项目主要是实现一个网页版的在线五子棋对战游戏它主要支持以下核心功能
用户数据管理实现用户注册与登录、用户session信息管理、用户比赛信息 (天梯分数、比赛场次、获胜场次) 管理等。匹配对战功能实现两个在线玩家在网页端根据天梯分数进行对战匹配匹配成功后在游戏房间中进行五子棋对战的功能。实时聊天功能实现两个玩家在游戏过程中能够进行实时聊天的功能。
2. 开发环境
本项目的开发环境如下
Linux在 Centos7.6 环境下进行数据库部署与开发环境搭建。VSCode/Vim通过 VSCode 远程连接服务器或直接使用 Vim 进行代码编写与功能测试。g/gdb通过 g/gdb 进行代码编译与调试。Makefile通过 Makefile 进行项目构建。
3. 核心技术
本项目所使用到的核心技术如下
HTTP/WebSocket使用 HTTP/WebSocket 完成客户端与服务器的短连接/长连接通信。WebSocketpp使用 WebSocketpp 实现 WebSocket 协议的通信功能。JsonCpp封装 JsonCpp 完成网络数据的序列与反序列功能。MySQL C API封装 MySQL C API 完成在 C 程序中访问和操作 MySQL 数据库的功能。C11使用 C11 中的某些新特性完成代码的编写例如 bind/shared_ptr/thread/mutex。BlockQueue为不同段位的玩家设计不同的阻塞式匹配队列来完成游戏的匹配功能。HTML/CSS/JS/AJAX通过 HTML/CSS/JS 来构建与渲染游戏前端页面以及通过 AJAX来向服务器发送 HTTP 客户端请求。
4. 开发阶段
本项目一共分为四个开发阶段 环境搭建在 Centos7.6 环境下安装本项目会使用到的各种工具以及第三方库。 前置知识了解对项目中需要用到的一些知识进行了解学会它们的基本使用比如 bind/WebSocketpp/HTML/JS/AJAX 等。 框架设计进行项目模块划分确定每一个模块需要实现的功能。 模块开发 功能测试对各个子模块进行开发与功能测试最后再将这些子模块进行整合并进行整体功能测试。 二、环境搭建
1. 安装 wget 工具
sudo yum install wget2. 更换 yum 源
备份之前的 yum 源
sudo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak更换 yum 源为国内阿里的镜像 yum 源
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.reposudo yum clean allsudo yum makecache安装 scl 软件源
sudo yum install centos-release-scl-rh centos-release-scl安装 epel 软件源
sudo yum install epel-release3. 安装 lrzsz 传输工具
sudo yum install lrzsz4. 安装⾼版本 gcc/g 编译器
安装 devtoolset 高版本 gcc/g 编译器
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c将 devtoolset 加载配置指令添加到终端初始化配置文件中使其在以后的所有新打开终端中有效
echo source /opt/rh/devtoolset-7/enable ~/.bashrc重新加载终端配置文件
source ~/.bashrc5. 安装 gdb 调试器
sudo yum install gdb6. 安装分布式版本控制工具 git
sudo yum install git7. 安装 cmake
sudo yum install cmake8. 安装 boost 库
sudo yum install boost-devel.x86_64 9. 安装 Jsoncpp 库
sudo yum install jsoncpp-devel10. 安装 MySQL 数据库服务及开发包
安装 MySQL 环境【MySQL】Linux 中 MySQL 环境的安装与卸载
设置 MySQL 用户与密码【MySQL】用户与权限管理
11. 安装 WebSocketpp 库
从 github 官方仓库克隆 WebSocketpp 库
git clone https://github.com/zaphoyd/websocketpp.git由于 github 服务器在国外所以可能会出现 clone 失败的情况此时可以从 gitee 仓库克隆 WebSocketpp 库
git clone https://gitee.com/freeasm/websocketpp.gitclone 成功后执行如下指令来安装 WebSocketpp 库 (执行 git clone 语句的目录下)
cd websocketpp/
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX/usr ..
sudo make install验证 websocketpp 是否安装成功 (build 目录下)
cd ../examples/echo_server当前目录下 ls 显示
CMakeLists.txt echo_handler.hpp echo_server.cpp SConscriptg 编译 echo_server.cpp如果编译成功则说明安装成功
g -stdc11 echo_server.cpp -o echo_server -lpthread -lboost_system三、前置知识了解
1. WebSocketpp
1.1 WebSocket 协议
WebSocket 介绍
WebSocket 是从 HTML5 开始支持的⼀种网页端和服务端保持长连接的消息推送机制
传统的 web 程序都是属于 “⼀问⼀答” 的形式即客户端给服务器发送⼀个 HTTP 请求然后服务器给客⼾端返回⼀个 HTTP 响应。这种情况下服务器是属于被动的一方即如果客户端不主动发起请求那么服务器也就⽆法主动给客户端响应。但是像网页即时聊天或者五子棋游戏这样的程序都是非常依赖 “消息推送” 的即需要服务器主动推动消息到客户端 (将一个客户端发送的消息或下棋的动作主动发送给另一个客户端)。那么如果只是使⽤原⽣的 HTTP 协议要想实现消息推送⼀般就需要通过 “Ajax 轮询” 的方式来实现而轮询的成本是比较高的并且客户端也不能及时的获取到消息的响应。
为了解决上述两个问题有大佬就设计了一种新的应用层协议 – WebSocket 协议。WebSocket 更接近于 TCP 这种级别的通信⽅式⼀旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。
原理解析
WebSocket 协议本质上是⼀个基于 TCP 的协议。为了建⽴⼀个 WebSocket 连接客户端浏览器会通过 JavaScript 向服务器发出建立 WebSocket 连接的请求这个连接请求本质上仍然是一个 HTTP 请求但它包含了⼀些附加头部信息比如协议升级Upgrade: WebSocket服务器端解析这些附加的头信息然后产生应答信息返回给客户端客户端和服务器端的 WebSocket 连接就建立起来了双方就可以通过这个连接通道自由的传递信息并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。。
同时当客户端浏览器获取到 Web Socket 连接后之后的通信就不再通过 Ajax 构建客户端请求发送给服务器了而是直接使用 WebSocket 的 send() 方法方法来向服务器发送数据并通过 onmessage 事件来接收服务器返回的数据。
报文格式
WebSocket 报文格式如下大家了解即可
WebSocket 相关接口
创建 WebSocket 对象
var Socket new WebSocket(url, [protocol]);WebSocket 对象的相关事件
WebSocket 对象的相关方法
参考资料
https://www.runoob.com/html/html5-websocket.html https://www.bilibili.com/video/BV1684y1k7VP/?buvidZC4691C539D91BA74044%E2%80%A6vd_sourcecbc46a2fc528c4362ce79ac44dd49e2c
1.2 WebSocketpp
WebSocketpp 介绍
WebSocketpp 是⼀个跨平台的开源 (BSD许可证) 头部专⽤C库它实现了RFC6455 (WebSocket协议) 和 RFC7692 (WebSocketCompression?Extensions)。它允许将 WebSocket 客户端和服务器功能集成到 C 程序中。在最常见的配置中全功能网络 I/O 由 Asio 网络库提供。
WebSocketpp 如要有以下特性
事件驱动的接口。⽀持HTTP/HTTPS、WS/WSS、IPv6。灵活的依赖管理 – Boost库/C11标准库。可移植性 – Posix/Windows、32/64bit、Intel/ARM。线程安全。
WebSocketpp 同时支持 HTTP 和 Websocket 两种网络协议比较适用于我们本次的项目所以我们选用该库作为项目的依赖库用来搭建 HTTP 和 WebSocket 服务器。
以下是 WebSocketpp 的一些相关网站
githubhttps://github.com/zaphoyd/websocketpp
用户手册http://docs.websocketpp.org/
官网http://www.zaphoyd.com/websocketpp
WebSocketpp 的使用
WebSocketpp 常用接口及其功能介绍如下
namespace websocketpp
{typedef lib::weak_ptrvoid connection_hdl;template typename configclass endpoint : public config::socket_type{typedef lib::shared_ptrlib::asio::steady_timer timer_ptr;typedef typename connection_type::ptr connection_ptr;typedef typename connection_type::message_ptr message_ptr;typedef lib::functionvoid(connection_hdl) open_handler;typedef lib::functionvoid(connection_hdl) close_handler;typedef lib::functionvoid(connection_hdl) http_handler;typedef lib::functionvoid(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 configclass server : public endpointconnectionconfig, config{/*初始化并启动服务端监听连接的accept事件处理*/void start_accept();};template typename configclass 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,};}}
}使用 WebSocketpp 搭建一个简单服务器的流程如下
实例化一个 websocketpp::server 对象。设置日志等级。(本项目中我们使用自己封装的日志函数所以这里设置日志等级为 none)初始化 asio 调度器。设置处理 http 请求、websocket 握手成功、websocket 连接关闭以及收到 websocket 消息的回调函数。设置监听端口。开始获取 tcp 连接。启动服务器。
示例代码如下
#include iostream
#include string
#include functional
#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpp
using std::cout;
using std::endl;typedef websocketpp::serverwebsocketpp::config::asio wsserver_t;void http_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {wsserver_t::connection_ptr conn srv-get_con_from_hdl(hdl);std::cout body: conn-get_request_body() std::endl;websocketpp::http::parser::request req conn-get_request();std::cout method: req.get_method() std::endl;std::cout uri: req.get_uri() std::endl;// 响应一个hello world页面std::string body htmlbodyh1Hello World/h1/body/html;conn-set_body(body);conn-append_header(Content-Type, text/html);conn-set_status(websocketpp::http::status_code::ok);
}
void wsopen_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {cout websocket握手成功 std::endl;
}
void wsclose_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {cout websocket连接关闭 endl;
}
void wsmessage_callback(wsserver_t *srv, websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {wsserver_t::connection_ptr conn srv-get_con_from_hdl(hdl);cout wsmsg: msg-get_payload() endl;std::string rsp [server]# msg-get_payload();conn-send(rsp, websocketpp::frame::opcode::text);
}int main()
{// 1. 实例化server对象wsserver_t wssrv;// 2. 设置日志等级wssrv.set_access_channels(websocketpp::log::alevel::none);// 3. 初始化asio调度器wssrv.init_asio();// 4. 设置回调函数wssrv.set_http_handler(std::bind(http_callback, wssrv, std::placeholders::_1));wssrv.set_open_handler(std::bind(wsopen_callback, wssrv, std::placeholders::_1));wssrv.set_close_handler(std::bind(wsclose_callback, wssrv, std::placeholders::_1));wssrv.set_message_handler(std::bind(wsmessage_callback, wssrv, std::placeholders::_1, std::placeholders::_2));// 5. 设置监听端口wssrv.listen(8080);wssrv.set_reuse_addr(true);// 6. 开始获取tcp连接wssrv.start_accept();// 7. 启动服务器wssrv.run();return 0;
}2. JsonCpp
Json 数据格式
Json 是⼀种数据交换格式它采⽤完全独立于编程语⾔的⽂本格式来存储和表示数据。
比如们想表示⼀个同学的学⽣信息。在 C/C 中我们可能使用结构体/类来表示
typedef struct {char *name XXX;int age 18;float score[3] { 88.5, 99, 58 };
} stu;而用 Json 数据格式表示如下
{姓名 : xxX,年龄 : 18,成绩 : [88.5, 99, 58]
}Json 的数据类型包括对象数组字符串数字等
对象使用花括号 {} 括起来的表示⼀个对象。数组使用中括号 [] 括起来的表示⼀个数组。字符串使用常规双引号 “” 括起来的表示⼀个字符串。数字包括整形和浮点型直接使用。
JsonCpp 介绍
Jsoncpp 库主要是⽤于实现 Json 格式数据的序列化和反序列化它实现了将多个数据对象组织成为 json 格式字符串以及将 Json 格式字符串解析得到多个数据对象的功能。
Json 数据对象类的部分表示如下
class Json::Value{/*Value重载了[]和因此所有的赋值和获取数据都可以通过简单的⽅式完成 val[name] xx*/Value operator(const Value other); Value operator[](const std::string key); Value operator[](const char* key);/*移除元素*/Value removeMember(const char* key); /*val[score][0]*/const Value operator[](ArrayIndex index) const; /*添加数组元素 -- val[score].append(88)*/Value append(const Value value);/*获取数组元素个数 -- val[score].size()*/ArrayIndex size() const; /*⽤于判断是否存在某个字段*/bool isNull(); /*json格式数据转string类型 -- string name val[name].asString()*/std::string asString() const; /*json格式数据转C语言格式的字符串即char*类型 -- char *name val[name].asCString()*/const char* asCString() const;/*转int -- int age val[age].asInt()*/Int asInt() const; /*转无符号长整型uint64_t -- uint64_t id val[id].asUInt64()*/Uint64 asUint64() const;/*转浮点数 -- float weight val[weight].asFloat()*/float asFloat() const; /*转bool类型 -- bool ok val[ok].asBool()*/bool asBool() const;
};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 将数据序列化的步骤如下
将需要序列化的数据存储在Json::Value对象中。实例化StreamWriterBuilder工厂类对象。使用StreamWriterBuilder工厂类对象实例化StreamWriter对象。使用StreamWriter对象完成Json::Value中数据的序列化工作并将序列化结果存放到ss中。
使用 JsonCpp 将数据反序列化的步骤如下
实例化一个 CharReaderBuilder 工厂类对象。使用CharReaderBuilder对象实例化一个CharReader对象。创建一个Json::Value对象用于保存json格式字符串反序列化后的结果。使用CharReader对象完成json格式字符串的反序列化工作。
示例代码如下
#include iostream
#include string
#include sstream
#include jsoncpp/json/json.h
using std::cout;
using std::endl;/*使用jsonCpp完成数据的序列化工作*/
std::string serialize()
{// 1. 将需要序列化的数据存储在Json::Value对象中Json::Value root;root[姓名] 小明;root[年龄] 18;root[成绩].append(80); //成绩是数组类型root[成绩].append(90);root[成绩].append(100);// 2. 实例化StreamWriterBuilder工厂类对象Json::StreamWriterBuilder swb;// 3. 使用StreamWriterBuilder工厂类对象实例化StreamWriter对象Json::StreamWriter *sw swb.newStreamWriter();// 4. 使用StreamWriter对象完成Json::Value中数据的序列化工作并将序列化结果存放到ss中std::stringstream ss;int n sw-write(root, ss);if(n ! 0){cout json serialize fail endl;delete sw;return ; }delete sw;return ss.str();
}/*使用JsonCpp完成序列化数据的反序列化工作*/
void deserialize(const std::string str)
{// 1. 实例化一个CharReaderBuilder工厂类对象Json::CharReaderBuilder crb;// 2. 使用CharReaderBuilder对象实例化一个CharReader对象Json::CharReader *cr crb.newCharReader();// 3. 创建一个Json::Value对象用于保存json格式字符串反序列化后的结果Json::Value root;// 4. 使用CharReader对象完成json格式字符串的反序列化工作std::string errmsg;bool ret cr-parse(str.c_str(), str.c_str() str.size(), root, errmsg);if(ret false){cout json deserialize fail: errmsg endl;delete cr;return;}// 5. 依次打印Json::Value中的数据cout 姓名: root[姓名].asString() endl;cout 年龄: root[年龄].asInt() endl; int size root[成绩].size();for(int i 0; i size; i){cout 成绩: root[成绩][i].asFloat() endl;}
}int main()
{std::string str serialize();cout str endl;deserialize(str);return 0;
}3. C11
C11 bind 参考文章
std::bind一包装普通函数
std::bind二包装成员函数
C11 智能指针参考文章
【C】智能指针
C11 线程库/互斥锁/条件变量参考文章
【C】C11 线程库
4. GDB
GDB 是一个强大的命令行式的源代码级调试工具可以用于分析和调试 C/C 等程序在程序运行时检查变量的值、跟踪函数调用、设置断点以及其他调试操作。GDB 在服务器开发中使用非常广泛一个合格的后台开发/服务器开发程序员应该能够使用 GDB 来调试程序。
由于 GDB 是纯命令行的所以我们需要学习 GDB 相关的一些基本指令下面是陈皓大佬编写的关于 GDB 调试技巧的博客供大家参考
https://so.csdn.net/so/search?qgdbtbloguhaoel
https://coolshell.cn/articles/3643.html
5. MySQL C API
参考文章
【MySQL】C语言连接数据库
6. HTML/CSS/JS/AJAX
本项目中与前端有关的技术分别是HTML、CSS、JavaScript 和 AJAX
HTML标签语言用于渲染前端网页。CSS层叠样式表对 HTML 标签进行样式修饰使其更加好看。JavaScript脚本语言在 web 前端中主要用于控制页面的渲染使得前端静态页面能够根据数据的变化而变化。AJAX一个异步的 HTTP 客户端它可以异步的向服务器发送 HTTP 请求获取响应并进行处理。
注意本项目中只是对上述这些前端技术进行一个最基本的使用目的是能够通过它们做出一个简单的前端页面。
6.1 HTML 简单了解
HTML 标签HTML 代码是由 “标签” 构成的
标签名 (body) 放到 中。大部分标签成对出现 为开始标签 为结束标签。少数标签只有开始标签, 称为 “单标签”。开始标签和结束标签之间, 写的是标签的内容。(hello)开始标签中可能会带有 “属性” id 属性相当于给这个标签设置了一个唯一的标识符。
bodyhello/body
body idmyIdhello/bodyHTML 文件基本结构
html 标签是整个 html 文件的根标签(最顶层标签)。head 标签中写页面的属性。body 标签中写的是页面上显示的内容。title 标签中写的是页面的标题。
htmlheadtitle第一个页面/title/headbodyhello world/body
/htmlHTML 常见标签
注释标签注释不会显示在界面上. 目的是提高代码的可读性。
!-- 我是注释 --标题标签标题标签一共有六个 – h1-h6数字越大, 则字体越小。
h1hello/h1
h2hello/h2
!-- ... --段落标签p 标签表示一个段落。
p这是一个段落/p换行标签br 是 break 的缩写表示换行。br 是一个单标签(不需要结束标签)。
br/图片标签 imgimg 标签必须带有 src 属性表示图片的路径。
img src./tmp.jpgimg srcrose.jpg alt鲜花 title这是一朵鲜花 width500px height800px border5px超链接标签 aa 标签必须具备 href表示点击后会跳转到哪个页面。同时可以指定 target 打开方式。(默认是 _self如果是 _blank 则用新的标签页打开)
!-- 外部链接 --
a hrefhttp://www.github.comgithub/a
!-- 内部链接: 网站内部页面之间的链接 --
a href2.html点我跳转到 2.html/a
!-- 下载链接: href 对应的路径是一个文件 --
a hreftest.zip下载文件/a列表标签ul li 表示无序列表ol li 表示有序列表dl (总标签) dt (小标题) dd (围绕标题来说明) 表示自定义列表。
h3无序列表/h3
ulliHTML/liliCSS/liliJS/li
/ul
h3有序列表/h3
olliHTML/liliCSS/liliJS/li
/ol
h3自定义列表/h3
dldt前端相关/dtddHTML/ddddCSS/ddddJS/dd
/dl表单标签 (重要)表单是让用户输入信息的重要途径分成两个部分 – 表单域和表单控件其中表单域是包含表单元素的区域重点是 form 标签表单控件是输入框、提交按钮等重点是 input 标签。 form 标签描述了要把数据按照什么方式, 提交到哪个页面中。 form actiontest.html... [form 的内容]
/forminput 标签各种输入控件, 单行文本框, 按钮, 单选框, 复选框等。 !-- 文本框 --
input typetext
!-- 密码框 --
input typepassword
!-- 单选框 --
input typeradio namesex男
input typeradio namesex checkedchecked女
!-- 普通按钮 --
input typebutton value我是个按钮
!-- 提交按钮 --
form actiontest.htmlinput typetext nameusernameinput typesubmit value提交
/form无语义标签 div spandiv 标签, division 的缩写, 含义是分割span 标签, 含义是跨度。它们是两个盒子一般搭配 CSS 用于网页布局。(div 是独占一行的, 是一个大盒子而span 不独占一行, 是一个小盒子。)
divspanHTML/spanspanCSS/spanspanJS/span
/div参考资料
HTML 教程 – 菜鸟教程
6.2 CSS 简单了解
CSS (层叠样式表) 能够对网页中元素位置的排版进行像素级精确控制, 实现美化页面的效果, 能够做到页面的样式和结构分离。
CSS 基本语法规范是 选择器 {一条/N条声明}
选择器决定针对谁修改。声明决定修改什么内容。声明的属性是键值对使用 “;” 区分键值对, 使用 “:” 区分键和值。
/*对段落标签进行样式修饰*/
stylep {/* 设置字体颜色 */color: red;/* 设置字体大小 */font-size: 30px;}
/style
phello/p选择器的功能是选中页面中指定的标签元素然后对其进行修饰。选择器有很多种类这里我们主要介绍基础选择器 标签选择器标签选择器的优点是能快速为同一类型的标签都选择出来缺点是不能差异化选择。 !-- 对段落标签p进行样式修饰 --
style
p {color: red;
}
/stylepdemo/p类选择器类选择器的优点是可以差异化表示不同的标签同时一个类可以被多个标签使用。类选择器就类似于我们给标签取了一个名字然后对这个名字的所有标签统一进行样式修饰。 style.blue {color: blue;}
/stylediv classbluedemo1/div
p classbluedemo2/pid 选择器和类选择器类似不同的是 id 是唯一的, 不能被多个标签使用。 style#ha {color: red;}
/stylediv idhademo/div通配符选择器使用 * 的定义, 对所有的标签都有效。
CSS 的引入方式一般有三种 内部样式表直接写在 style 标签中嵌入到 html 内部。(style 一般都是放到 head 标签中) 这样做的优点是能够让样式和页面结构分离缺点是分离的不够彻底在实际开发中并不常用。 stylediv {color: red;}
/style行内样式表通过 style 属性, 来指定某个标签的样式。 这种方法只适合于写简单样式并且只针对某个标签生效在实际开发中也不常用。 div stylecolor:green想要生活过的去, 头上总得带点绿/div外部样式表 (重要)先 创建一个 css 文件然后使用 link 标签引入 css。 这样做能够让让样式和页面结构彻底分离即使是在 css 内容很多的时候这也是实际开发中最常用的方式。 link relstylesheet href[CSS文件路径]参考资料
css 教程 – 菜鸟教程
css 选择器参考手册 – W3school
6.3 JS 简单了解
JavaScript 的基本语法和 java 类似所以我们不再单独学习。这里我们主要学习如何使用 JavaScript 去渲染前端页面具体内容如下
如何使用 js 给按钮添加点击事件。如何使用 js 去获取以及设置一个页面控件的内容。
bodyinput typetext iduser_nameinput typepassword idpassword!--为button按钮添加点击事件调用登录函数--button idsubmit onclicklogin()提交/buttondivspanhello world/spanspanhello world/span/div
/bodyjavascriptfunction login() {//获取输入框中的内容var username document.getElementById(user_name).value;var password document.getElementById(password).value;//服务器用户信息验证成功后提示登录成功alert(登录成功);//服务器用户信息验证失败后提示登录失败并清空输入框内容alert(登录失败);document.getElementById(user_name).value ;document.getElementById(password).value ;};//js相关的一些其他WebAPIfunction demo() {var div getElementById(div);//读取页面内容var msg div.innerHTML;//向控制台打印日志信息console.log(msg);//修改页面内容div.innerHTML spanhello js/span;}
/javascript参考资料
JavaScript 教程 – 菜鸟教程
6.4 AJAX 简单了解
为了降低学习成本这里我们并不使用 js 中原生的 AJAX而是使用 jQuery 中的 AJAX jQuery 是一种基于JavaScript的开源库。它简化了HTML文档遍历、事件处理、动画效果等操作。通过使用jQuery开发者可以更轻松地操作DOM元素、处理事件、发送AJAX请求以及创建动态效果从而使网页开发变得更加便捷和灵活。 jQuery AJAX 是指使用 jQuery 库中提供的 AJAX 相关方法来进行异步数据交互。通过使用 jQuery 提供的 AJAX 方法开发者可以轻松地执行诸如发送 GET 或 POST 请求、处理服务器响应、以及执行其他与异步数据交互相关的操作简化了原生 JavaScript 中使用 XMLHttpRequest 对象进行 AJAX 操作的复杂性。
bodyinput typetext iduser_nameinput typepassword idpassword!--为button按钮添加点击事件调用登录函数--button idsubmit onclicklogin()提交/button
/body
// 引用jQuery库
script srcjquery-1.10.2.min.js/script
javascriptfunction login() {//获取输入框中的内容var username document.getElementById(user_name).value;var password document.getElementById(password).value;// 通过ajax向服务器发送登录请求$.ajax({// 请求类型 -- get/posttype: post,// 请求资源路径url: http://106.52.90.67/login,// 请求的数据data: JSON.stringify(log_info),// 请求成功处理函数success: function(res) {alert(登录成功);},// 请求失败处理函数error: function(xhr) {document.getElementById(user_name).value ;document.getElementById(password).value ;alert(JSON.stringify(xhr));}})};
/javascript参考资料
jQuery 安装 – 菜鸟教程
jQuery Ajax 参考手册 – 菜鸟教程 四、框架设计
1. 项目模块划分
1.1 总体模块划分
本项目一共会划分为三个大的模块
用户数据管理模块基于 MySQL 数据库进行用户数据的管理包括用户名、密码、天梯分数、比赛场次、获胜场次等。前端界面模块基于 HTTP/CSS/JS/AJAX 实现用户注册、登录、游戏大厅和游戏房间前端界面的动态控制以及与服务器的通信。业务处理模块通过 WebSocketpp 相关 API 搭建 WebSocket 服务器与客户端浏览器进行通信接受客户端请求并进行业务处理。
1.2 业务处理子模块划分
由于项目需要实现用户注册、用户登录、用户匹配对战以及游戏内实时聊天等不同的功能所以需要对业务处理模块进行子模块划分让不同的子模块负责不同的业务处理。
业务处理模块具体的子模块划分如下
网络通信模块基于 websocketpp 库实现 HttpWebSocket 服务器的搭建提供客户端与服务器的网络通信功能。会话管理模块对客户端的连接进行 cookiesession 管理实现 HTTP 短连接下客户端身份识别的功能。在线用户管理模块对进行游戏大厅与游戏房间的用户进行在线管理提供用户在线判断与用户 WebSocket 长连接获取等功能。游戏房间管理模块为匹配成功的用户创建游戏房间提供实时的五子棋对战与聊天业务功能。匹配对战管理根据天梯分数为不同段位的玩家创建不同的匹配队列为匹配成功的用户创建游戏房间并加入游戏房间。
2. 项目流程图
2.1 用户角度流程图
从用户/玩家的角度出发本项目的流程是 注册 - 登录 - 对战匹配 - 游戏对战实时聊天 - 游戏结束返回游戏大厅。 2.2 服务器角度流程图
从服务器角度出发本项目的流程如下
服务器收到客户端获取注册页面请求服务器响应注册页面 register.html。服务器收到客户端用户注册请求服务器根据用户提交上来的注册信息向数据库中新增用户并返回注册成功或失败的响应。服务器收到客户端获取登录页面请求服务器响应登录页面 login.html。服务器收到客户端用户登录请求服务器使用用户提交上来的登录信息与数据库中的信息进行比对并返回登录成功或失败的响应。(注用户登录成功后服务器会为用户创建会话信息并将用户会话 id 添加到 http 头部中进行返回)服务器收到客户端获取游戏大厅页面请求服务器响应游戏大厅页面 game_hall.html。服务器收到客户端获取用户详细信息请求服务器会取出请求头部中的 cookie 信息获取用户 session 信息cookie/session 不存在则返回失败响应 (会话验证)存在则通过用户 session 信息获取用户 id再通过用户 id 从数据库中获取用户详细信息并返回。服务器收到客户端建立游戏大厅 WebSocket 长连接请求 会话验证成功后返回长连接建立成功或失败的响应。(游戏大厅长连接建立后用户会被加入到游戏大厅在线用户管理中)服务器收到客户端开始/停止对战匹配的请求会话验证成功后会根据用户天梯分数将用户加入对应的匹配队列或从对应的匹配队列中移除并返回响应。(游戏匹配成功后服务器会为用户创建游戏房间并主动给客户端发送 match_success 响应)游戏匹配成功后服务器收到客户端建立游戏房间长连接请求会话验证成功后返回长连接建立成功或失败的响应。(游戏房间长连接建立后用户会被加入到游戏房间在线用户管理中)之后开始游戏对战与实时聊天服务器会收到或主动向另一个客户端推送下棋/聊天信息。最后当游戏结束后用户会返回游戏大厅并重新建立游戏大厅长连接。 五、模块开发
1. 实用工具类模块
在进行具体的业务模块开发之前我们可以提前封装实现⼀些项⽬中会用到的边缘功能代码这样以后在项目中有相应需求时就可以直接使用了。
1.1 日志宏封装
日志宏功能主要负责程序日志的打印方便我们在程序出错时能够快速定位错误以及在程序运行过程中打印一些关键的提示信息。
logger.hpp:
#ifndef __LOGGER_HPP__
#define __LOGGER_HPP__
#include cstdio
#include time.h/*日志等级*/
enum {NORMAL, DEBUG, ERROR,FATAL
};/*将日志等级转化为字符串*/
const char* level_to_stirng(int level) {switch (level){case NORMAL:return NORMAL;case DEBUG:return DEBUG;case ERROR:return ERROR;case FATAL:return FATAL;default:return nullptr;}
}#define LOG(level, format, ...) do {\const char* levelstr level_to_stirng(level); /*日志等级*/\time_t ts time(NULL); /*时间戳*/\ struct tm *lt localtime(ts); /*格式化时间*/\ char buffer[32] { 0 };\strftime(buffer, sizeof(buffer) - 1, %y-%m-%d %H:%M:%S, lt); /*格式化时间到字符串*/\fprintf(stdout, [%s][%s][%s:%d] format \n, levelstr, buffer, __FILE__, __LINE__, ##__VA_ARGS__); /*##解除必须传递可变参数的限制*/\
} while(0)
#endif1.2 MySQL C API 封装
MySQL C API 工具类主要是封装部分C语言连接数据库的接口包括 MySQL 句柄的创建和销毁以及 sql 语句的执行。
需要注意的是我们并没有封装获取 sql 查询结果的相关接口因为是否要获取查询结果、要获取哪部分查询结果以及以何种形式获取查询结果这些都是与业务需求强相关的。
mysql_util:
/*MySQL C API工具类*/
class mysql_util {
public:/*创建MySQL句柄*/static MYSQL *mysql_create(const std::string host, const std::string user, const std::string passwd, const std::string db gobang, uint16_t port 4106) {/*初始化MYSQL句柄*/MYSQL *mysql mysql_init(nullptr);if(mysql nullptr) {LOG(FATAL, mysql init failed);return nullptr;}/*连接MySQL数据库*/mysql mysql_real_connect(mysql, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0);if(mysql nullptr) {LOG(FATAL, mysql connect failed: %s, mysql_error(mysql));mysql_close(mysql);return nullptr;}/*设置客户端字符集*/if(mysql_set_character_set(mysql, utf8) ! 0) {LOG(ERROR, client character set failed: %s, mysql_error(mysql));}return mysql;}/*执行sql语句*/static bool mysql_execute(MYSQL *mysql, const std::string sql) {if(mysql_query(mysql, sql.c_str()) ! 0) {LOG(ERROR, sql query failed: %s, mysql_error(mysql));return false;}return true;}/*销毁MySQL句柄*/static void mysql_destroy(MYSQL *mysql) {if(mysql ! nullptr) {mysql_close(mysql);}}
};1.3 JsonCpp 封装
jsoncpp 工具类主要是完成数据的序列化与反序列化工作。
json_util:
/*jsoncpp工具类*/
class json_util {
public:/*序列化接口*/static bool serialize(Json::Value root, std::string str) {Json::StreamWriterBuilder swb;std::unique_ptrJson::StreamWriter sw(swb.newStreamWriter());std::stringstream ss;if(sw-write(root, ss) ! 0) {LOG(ERROR, json serialize failed);return false;}str ss.str();return true;}/*反序列化接口*/static bool deserialize(const std::string str, Json::Value root) {Json::CharReaderBuilder crb;std::unique_ptrJson::CharReader cr(crb.newCharReader());std::string err;if(cr-parse(str.c_str(), str.c_str() str.size(), root, err) false) {LOG(ERROR, json deserialize failed: %s, err);return false;}return true;}
};1.4 String Split 封装
string split 主要是按照特定分隔符对字符串进行分割并将分割后的结果进行返回。在本项目中它的使用场景是分割请求头部中的 cookie 信息获取 session id。
string_util:
/*字符串处理工具类*/
class string_util {
public:/*将源字符串按照特定分隔符分割为若干个子字符串*/static int split(const std::string src, const std::string sep, std::vectorstd::string res) {// ..abc..de..efint index 0, pos 0;while(index src.size()) {pos src.find(sep, index);if(pos std::string::npos) {res.push_back(src.substr(index));break;}if(index pos) {index sep.size();continue;}else {res.push_back(src.substr(index, pos - index));index pos sep.size();}}return res.size();}
};1.5 File Read 封装
file read 的作用是读取指定文件中的内容。
file_util:
/*读取文件数据工具类*/
class file_util {
public:static bool read(const char* filename, std::string data) {/*以二进制形式打开文件*/std::ifstream ifs(filename, std::ios::binary);if(ifs.is_open() false) {LOG(ERROR, open %s file failed, filename);return false;}/*获取文件大小*/size_t size;ifs.seekg(0, std::ios::end);size ifs.tellg();ifs.seekg(0, std::ios::beg);/*读取文件内容*/data.resize(size);ifs.read(data[0], size);if(ifs.good() false) {LOG(ERROR, read %s file content failed, filename);ifs.close();return false;}/*关闭文件*/ifs.close();return true;}
};2. 用户数据管理模块
用户数据管理模块主要负责对数据库中数据进行统⼀的增删查改管理其他模块对数据的操作都必须通过用户数据管理模块来完成。
2.1 用户信息表
在本项目中用户数据主要包括用户名、用户密码、用户天梯分数、用户对战场次以及用户获胜场次我们可以在数据库中创建一个 user 表来保存用户数据。其中user 表中需要有一个自增主键 id 来唯一标识一个用户。
create database if not exists gobang;
use gobang;
create table if not exists user (id bigint unsigned primary key auto_increment key,username varchar(32) unique key not null,password varchar(64) not null,score int default 1000,total_count int default 0,win_count int default 0
);2.2 用户数据管理类
对于一般的数据库来说数据库中有可能存在很多张表而每张表中管理的数据以及要进行的数据操作都各不相同因此我们可以为每⼀张表中的数据操作都设计⼀个类通过类实例化的对象来访问这张数据库表中的数据。这样当我们要访问哪张表的时候只需要使用对应类实例化的对象即可。
对于本项目而言目前数据库中只有一张 user 表所以我们需要为其设计一个类它的主要功能如下
registers完成新用户注册返回是否注册成功。login完成用户登录验证如果登录成功返回 true 并且填充用户详细信息。select_by_name通过用户名查找用户详细信息。select_by_id通过用户 id 查找用户详细信息。win当用户对战胜利后修改用户数据库数据 – 天梯分数、对战场次、获胜场次。lose当用户对战失败后修改用户数据库数据 – 天梯分数、对战场次。
db.hpp:
#ifndef __DB_HPP__
#define __DB_HPP__
#include util.hpp
#include mutex
#include cassert/*用户数据管理模块 -- 用于管理数据库数据为数据库中的每张表都设计一个类然后通过类对象来操作数据库表中的数据*/
/*用户信息表*/
class user_table {
public:user_table(const std::string host, const std::string user, const std::string passwd, \const std::string db gobang, uint16_t port 4106) {_mysql mysql_util::mysql_create(host, user, passwd, db, port);assert(_mysql ! nullptr);LOG(DEBUG, 用户数据管理模块初识化完毕);}~user_table() {if(_mysql ! nullptr) { mysql_util::mysql_destroy(_mysql);_mysql nullptr;}LOG(DEBUG, 用户数据管理模块已被销毁);}/*新用户注册*/bool registers(Json::Value user) {if(user[username].isNull() || user[password].isNull()) {LOG(NORMAL, please input username and password);return false; }// 由于用户名有唯一键约束所以不需要担心用户已被注册的情况char sql[1024];
#define INSERT_USER insert into user values(null, %s, password(%s), 1000, 0, 0)sprintf(sql, INSERT_USER, user[username].asCString(), user[password].asCString());// LOG(DEBUG, %s, sql);if(mysql_util::mysql_execute(_mysql, sql) false) {LOG(NORMAL, user register failed);return false;}LOG(NORMAL, %s register success, user[username].asCString());return true;}/*用户登录验证*/bool login(Json::Value user) {// 与数据库中的用户名密码进行比对// 注意数据库的password是经过mysql password函数转换后的所以sql查询时也需要对user[password].asString()进行转化
#define SELECT_USER select id, score, total_count, win_count from user where username %s and password password(%s) char sql[1024];sprintf(sql, SELECT_USER, user[username].asCString(), user[password].asCString());MYSQL_RES *res nullptr;{// mysql查询与查询结果的本地保存两步操作需要加锁避免多线程使用同一句柄进行操作的情况下发送结果集的数据覆盖问题// 将锁交给RAII unique_lock进行管理std::unique_lockstd::mutex lock(_mutex);if(mysql_util::mysql_execute(_mysql, sql) false) return false;;// 获取查询到的结果--一行记录res mysql_store_result(_mysql);// 注意当mysql查询结果为空时mysql_store_result也不会返回空所以不能在这里判断用户名密码是否正确if(res nullptr) {LOG(NORMAL, mysql store failed: , mysql_error(_mysql));return false;}}int row_count mysql_num_rows(res);int col_count mysql_num_fields(res);// row_count 为0说明查询不到与当前用户名密码匹配的数据即用户名或密码错误if(row_count 0) {LOG(NORMAL, the username or password error, please input again);return false;}// 用户名存在唯一键约束if(row_count 1) {LOG(ERROR, there are same user %s in the database, user[username].asCString());return false;} LOG(NORMAL, %s login success, user[username].asCString());// 填充该用户的其他详细信息MYSQL_ROW row mysql_fetch_row(res);user[id] std::stoi(row[0]);user[score] std::stoi(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 SELECT_BY_USERNAME select id, score, total_count, win_count from user where username %schar sql[1024];sprintf(sql, SELECT_BY_USERNAME, name.c_str());MYSQL_RES *res nullptr;{// 加锁std::unique_lockstd::mutex lock(_mutex);if(mysql_util::mysql_execute(_mysql, sql) false) return false;// 获取查询到的结果--一行记录res mysql_store_result(_mysql);// 注意当mysql查询结果为空时mysql_store_result也不会返回空所以不能在这里判断用户是否存在if(res nullptr) {LOG(DEBUG, mysql store failed: , mysql_error(_mysql));return false;}}int row_count mysql_num_rows(res);int col_count mysql_num_fields(res);// row_count为0说明查询不到与当前用户名匹配的数据即用户不存在if(row_count 0) {LOG(DEBUG, the user with name %s does not exist, name.c_str());return false;}// 用户名存在唯一键约束if(row_count 1) {LOG(ERROR, there are same user name %s in the database, name.c_str());return false;} MYSQL_ROW row mysql_fetch_row(res);// password是转换后的获取无意义user[id] std::stoi(row[0]);user[username] name.c_str();user[score] std::stoi(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 SELECT_BY_ID select username, score, total_count, win_count from user where id %dchar sql[1024];sprintf(sql, SELECT_BY_ID, id);MYSQL_RES *res nullptr;{// 加锁std::unique_lockstd::mutex lock(_mutex);if(mysql_util::mysql_execute(_mysql, sql) false) return false;// 获取查询到的结果--一行记录res mysql_store_result(_mysql);// 注意当mysql查询结果为空时mysql_store_result也不会返回空所以不能在这里判断用户是否存在if(res nullptr) {LOG(DEBUG, mysql store failed: , mysql_error(_mysql));return false;}}int row_count mysql_num_rows(res);int col_count mysql_num_fields(res);// row_count为0说明查询不到与当前用户名ID匹配的数据即用户不存在if(row_count 0) {LOG(DEBUG, the user with ID %d does not exist, id);return false;}// 用户名存在唯一键约束if(row_count 1) {LOG(ERROR, there are same user with ID %d in the database, id);return false;}MYSQL_ROW row mysql_fetch_row(res);// password是转换后的获取无意义user[id] (Json::UInt64)id;user[username] row[0];user[score] std::stoi(row[1]);user[total_count] std::stoi(row[2]);user[win_count] std::stoi(row[3]);mysql_free_result(res);return true; }/*用户对战胜利修改分数以及比赛和胜利场次胜利一场增加30分*/bool win(uint64_t id) {
#define UPDATE_WIN update user set scorescore30, total_counttotal_count1, win_countwin_count1 where id %dchar sql[1024];sprintf(sql, UPDATE_WIN, id);if(mysql_util::mysql_execute(_mysql, sql) false) {LOG(ERROR, update the user info of win failed);return false;}return true;}/*用户对战失败修改分数以及比赛场次*失败一场减少30分*/bool lose(uint64_t id) {
#define UPDATE_LOSE update user set scorescore-30, total_counttotal_count1 where id %dchar sql[1024];sprintf(sql, UPDATE_LOSE, id);if(mysql_util::mysql_execute(_mysql, sql) false) {LOG(ERROR, update the user info of lose failed);return false;}return true;}
private:MYSQL *_mysql; // mysql操作句柄std::mutex _mutex; // 解决多线程使用同一类对象(句柄)访问数据库时可能发生的线程安全问题
};
#endif3. 在线用户管理模块
在线用户管理模块主要管理两类用户 – 进入游戏大厅的用户与进入游戏房间的用户因为用户只有进入了游戏大厅或者游戏房间其对应的客户端才会与服务器建立 WebSocket 长连接。
此时我们需要将用户 id 与用户所对应的 WebSocket 长连接关联起来这样我们就能够通过用户 id 找到用户所对应的连接进而实现服务器主动向客户端推送消息的功能 在游戏大厅中当一个用户开始匹配后如果匹配成功服务器需要主动向客户端推送匹配成功的消息。 在游戏房间中当一个玩家有下棋或者聊天动作时服务器也需要将这些动作主动推送给另一个玩家。
需要注意的是用户在游戏大厅的长连接与游戏房间的长连接是不同的所以我们需要分别建立游戏大厅用户 id 与 WebSocket 长连接的关联关系以及游戏房间用户 id 与 WebSocket 长连接的关联关系。
在线用户管理类的主要功能如下
enter_game_hall指定用户进入游戏大厅此时需要建立用户 id 与游戏大厅 WebSocket 长连接的关联关系。enter_game_hall指定用户进入游戏房间此时需要建立用户 id 与游戏房间 WebSocket 长连接的关联关系。exit_game_hall指定用户离开游戏大厅此时需要断开用户 id 与游戏大厅 WebSocket 长连接的关联关系。exit_game_room指定用户离开游戏房间此时需要断开用户 id 与游戏房间 WebSocket 长连接的关联关系。is_in_game_hall判断指定用户是否在游戏大厅中。is_in_game_room判断指定用户是否在游戏房间中。get_conn_from_hall获取指定用户的游戏大厅长连接。get_conn_from_room获取指定用户的游戏房间长连接。
online.hpp:
#ifndef __ONLINE_HPP__
#define __ONLINE_HPP__
#include util.hpp
#include mutex
#include unordered_map
#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpptypedef websocketpp::serverwebsocketpp::config::asio wsserver_t;/*在线用户管理模块 -- 用于管理在游戏大厅以及游戏房间中的用户建立用户id与websocket长连接的对应关系*/
class online_manager {
public:online_manager() { LOG(DEBUG, 在线用户管理模块初始化完毕); }~online_manager() { LOG(DEBUG, 在线用户管理模块已被销毁); }/*用户进入游戏大厅(此时用户websocket长连接已建立好)*/void enter_game_hall(uint64_t uid, wsserver_t::connection_ptr conn) {std::unique_lockstd::mutex lock(_mutex);_hall_user[uid] conn;}/*用户进入游戏房间*/void enter_game_room(uint64_t uid, wsserver_t::connection_ptr conn) {std::unique_lockstd::mutex lock(_mutex);_room_user[uid] conn;}/*用户离开游戏大厅(websocket长连接断开时)*/void exit_game_hall(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);_hall_user.erase(uid);}/*用户对战结束离开游戏房间回到游戏大厅*/void exit_game_room(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);_room_user.erase(uid);}/*判断当前用户是否在游戏大厅*/bool is_in_game_hall(uint64_t uid) {std::unique_lockstd::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_lockstd::mutex lock(_mutex);auto it _room_user.find(uid);if(it _room_user.end()) return false;return true;}/*通过用户id获取游戏大厅用户的通信连接*/wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);auto it _hall_user.find(uid);if(it _hall_user.end()) return nullptr;return _hall_user[uid];}/*通过用户id获取游戏房间用户的通信连接*/wsserver_t::connection_ptr get_conn_from_room(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);auto it _room_user.find(uid);if(it _room_user.end()) return nullptr;return _room_user[uid];}
private:std::mutex _mutex; // 解决多线程模式下的线程安全问题std::unordered_mapuint64_t, wsserver_t::connection_ptr _hall_user; // 建立游戏大厅用户id与通信连接之间的关联关系std::unordered_mapuint64_t, wsserver_t::connection_ptr _room_user; // 建立游戏房间用户id与通信连接之间的关联关系
};
#endif4. 游戏房间管理模块
游戏房间管理模块就是设计一个房间类能够实现房间的实例化房间类主要是对匹配成功的玩家建立一个小范围的关联关系当一个房间中的玩家发生下棋或者聊天动作时服务器能够将其广播给房间中的其他玩家。
游戏房间类的具体功能如下
add_white_user为房间添加白棋玩家。add_black_user为白棋添加黑棋玩家。handler总的动作处理函数函数内部会根据不同的动作类型 (下棋/聊天) 调用不同的子函数进行处理得到响应。broadcast将处理动作得到的响应广播给房间中的其他玩家。
同时由于同一时间段内进行匹配或者正在对战的玩家有很多所以游戏房间可能会有多个那么我们就需要设计一个游戏房间管理类来对多个房间进行管理。
游戏房间管理类的具体功能如下
create_room为两个玩家创建一个游戏房间。get_room_by_rid通过房间 id 获取房间信息。get_room_by_uid通过玩家 id 获取玩家所在房间的房间信息。remove_room通过房间 id 销毁房间。remove_room_user移除房间中的指定玩家若房间中没有玩家了则直接销毁房间。
最后需要注意的是在游戏房间管理模块中由于我们需要根据不同的消息类型来调用不同的函数进而得到不同的响应所以我们需要提前规定好 WebSocket (游戏房间中 WebSocket 长连接已建立) 网络通信中不同类型的消息的格式是怎样的。这部分代码会在服务器模块的通信接口设计处给出但为了便于理解这里我们也放一份。
玩家下棋的消息
// 玩家下棋消息
{optype: put_chess, // put_chess表示当前请求是下棋操作room_id: 222, // room_id 表⽰当前动作属于哪个房间uid: 1, // 当前的下棋操作是哪个用户发起的row: 3, // 当前下棋位置的⾏号col: 2 // 当前下棋位置的列号
}// 下棋成功后后台回复的消息
{optype: put_chess,result: true,reason: 下棋成功或游戏胜利或游戏失败,room_id: 222,uid: 1,row: 3,col: 2,winner: 0 // 游戏获胜者0表示未分胜负!0表示已分胜负
}// 下棋失败后后台回复的消息
{optype: put_chess,result: false,reason: 下棋失败的原因,room_id: 222, uid: 1,row: 3,col: 2,winner: 0
}玩家聊天的消息
// 玩家聊天消息
{optype: chat, // chat表示当前请求是下棋操作room_id: 222, // room_id 表⽰当前动作属于哪个房间uid: 1, // 当前的下棋操作是哪个用户发起的message: 你好 // 聊天消息的具体内容
}// 聊天消息发送成功后台回复的消息
{optype: chat,result: true,room_id: 222,uid: 1,message: 你好
}// 聊天消息发送失败后台回复的消息
{optype: chat,result: false,reason: 错误原因比如消息中包含敏感词,room_id: 222,uid: 1,message: 你好
}未知类型的消息
{optype: 消息的类型,result: false,reason: 未知类型的消息
}room.hpp:
#ifndef __ROOM_HPP__
#define __ROOM_HPP__
#include util.hpp
#include db.hpp
#include online.hpp
#include vector#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2typedef enum {GAME_START,GAME_OVER
} room_status;/*游戏房间管理模块 -- 用于管理在游戏房间中产生的各种数据以及动作同时也包括对多个游戏房间本身的管理*/
/*游戏房间类*/
class room {
private:/*check_win子函数其中row/col表示下棋位置row_off/col_off表示是否偏移*/bool five_piece(int row, int col, int row_off, int col_off, int color) {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;}/*判断是否有用户胜利并返回winner_id (0表示没有用户胜利非0表示有)*/uint64_t check_win(int chess_row, int chess_col, int cur_color) {uint64_t winner_id cur_color CHESS_WHITE ? _white_user_id : _black_user_id;// 横行方向当前位置开始行不变列/--if(five_piece(chess_row, chess_col, 0, 1, cur_color)) return winner_id;// 纵列方向当前位置开始行/--列不变if(five_piece(chess_row, chess_col, 1, 0, cur_color)) return winner_id;// 正斜方向当前位置开始行列-- 以及 行--列if(five_piece(chess_row, chess_col, 1, -1, cur_color)) return winner_id;// 反斜方向当前位置开始行列 以及 行--列--if(five_piece(chess_row, chess_col, 1, 1, cur_color)) return winner_id;// 没有人获胜返回0return 0;}/*用户胜利或失败后更新用户数据库信息*/void update_db_info(uint64_t winner_id, uint64_t loser_id) {_tb_user-win(winner_id);_tb_user-lose(loser_id);}
public:room(uint64_t room_id, user_table *tb_user, online_manager *online_user): _room_id(room_id), _statu(GAME_START), _tb_user(tb_user), _online_user(online_user), _board(BOARD_ROW, std::vectorint(BOARD_COL, 0)){LOG(DEBUG, %d号房间创建成功, _room_id);}~room() { LOG(DEBUG, %d号房间已被销毁, _room_id); }/*添加白棋用户*/void add_white_user(uint64_t id) {_white_user_id id;_player_count;}/*添加黑棋用户*/void add_black_user(uint64_t id) {_black_user_id id;_player_count;}/*处理玩家下棋动作并返回响应*/Json::Value handler_chess(Json::Value req) {Json::Value resp req;// 判断白棋与黑棋用户是否在线若一方不在线另一方直接获胜if(_online_user-is_in_game_room(_white_user_id) false) {resp[result] true;resp[reason] 对方已掉线游戏获胜; // 在黑棋的视角白棋是对方 resp[winner] (Json::UInt64)_black_user_id; // 白棋掉线黑棋用户}if(_online_user-is_in_game_room(_black_user_id) false) {resp[result] true;resp[reason] 对方已掉线游戏胜利; resp[winner] (Json::UInt64)_white_user_id; }// 获取下棋位置判断位置是否合理并下棋uint64_t cur_uid req[uid].asUInt64();int chess_row req[row].asInt();int chess_col req[col].asInt();if(_board[chess_row][chess_col] ! 0) {resp[result] false;resp[reason] 该位置已被占用;return resp; }int cur_color (cur_uid _white_user_id ? CHESS_WHITE : CHESS_BLACK);_board[chess_row][chess_col] cur_color;// 判断是否有玩家获胜(存在五星连珠的情况) 其中0表示没有玩家胜利非0表示胜利的玩家iduint64_t winner_id check_win(chess_row, chess_col, cur_color);resp[result] true;resp[reason] 下棋成功; resp[winner] (Json::UInt64)winner_id;if(winner_id ! 0) { resp[reason] 五星连珠游戏胜利; }return resp;}/*处理玩家聊天动作并返回响应*/Json::Value handler_chat(Json::Value req) {Json::Value resp req;// 检查消息中是否包含敏感词std::string msg req[message].asString();size_t pos msg.find(垃圾);if(pos ! std::string::npos) {resp[result] false;resp[reason] 消息中包含敏感词;return resp;}resp[reslut] true;return resp;}/*处理玩家退出动作并返回响应*/void handler_exit(uint64_t uid) {// 如果玩家在下棋中则对方直接获胜if(_statu GAME_START) {Json::Value resp;resp[optype] put_chess;resp[result] true;resp[reason] 对方已退出游戏胜利;resp[room_id] (Json::UInt64)_room_id;resp[uid] (Json::UInt64)uid;resp[row] -1;resp[col] -1;resp[winner] (Json::UInt64)(uid _white_user_id ? _black_user_id : _white_user_id);// 更新用户数据库信息与游戏房间的状态uint64_t loser_id uid;uint64_t winner_id loser_id _white_user_id ? _black_user_id : _white_user_id;update_db_info(winner_id, loser_id);_statu GAME_OVER;// 将消息广播给房间其他玩家broadcast(resp);}// 游戏结束正常退出直接更新玩家数量--_player_count;}/*总的动作处理函数负责判断动作类型并调用对应的处理函数得到处理响应后将其广播给房间中其他用户*//*注意玩家退出动作属于玩家断开连接后调用的操作不属于handler的一种*/void handler(Json::Value req) {Json::Value resp;// 判断房间号是否匹配if(_room_id ! req[room_id].asUInt64()) {resp[optype] req[optype].asString();resp[result] false;resp[reason] 房间号不匹配;broadcast(resp);return;}// 根据请求类型调用不同的处理函数std::string type req[optype].asString();if(type put_chess) {resp handler_chess(req);// 判断是否有玩家获胜如果有则需要更新用户数据库信息与游戏房间的状态if(resp[winner].asUInt64() ! 0) {uint64_t winner_id resp[winner].asUInt64();uint64_t loser_id (winner_id _white_user_id ? _black_user_id : _white_user_id);update_db_info(winner_id, loser_id);_statu GAME_OVER;}} else if(type chat) {resp handler_chat(req);} else {resp[optype] req[optype].asString();resp[result] false;resp[reason] 未知类型的消息;}// 将消息广播给房间中的其他玩家broadcast(resp);}/*将动作响应广播给房间中的其他玩家*/void broadcast(Json::Value resp) {// 将Json响应进行序列化std::string body;json_util::serialize(resp, body);// 获取房间中的所有玩家的通信连接wsserver_t::connection_ptr conn_white _online_user-get_conn_from_room(_white_user_id);wsserver_t::connection_ptr conn_black _online_user-get_conn_from_room(_black_user_id);// 如果玩家连接没有断开则将消息广播给他if(conn_white.get() ! nullptr) {conn_white-send(body);}if(conn_black.get() ! nullptr) {conn_black-send(body);}}
public:// 将部分成员变量设为public供外部类访问uint64_t _room_id; // 房间IDroom_status _statu; // 房间状态int _player_count; // 玩家数量uint64_t _white_user_id; // 白棋玩家IDuint64_t _black_user_id; // 黑棋玩家ID
private:user_table *_tb_user; // 管理玩家数据的句柄online_manager *_online_user; // 管理玩家在线状态的句柄 std::vectorstd::vectorint _board; // 二维棋盘
};/*管理房间数据的智能指针*/
using room_ptr std::shared_ptrroom; /*游戏房间管理类*/
class room_manager {
public:room_manager(user_table *tb_user, online_manager *online_user): _next_rid(1), _tb_user(tb_user), _online_user(online_user) {LOG(DEBUG, 游戏房间管理模块初始化成功);}~room_manager() { LOG(NORMAL, 游戏房间管理模块已被销毁); }/*为两个玩家创建房间并返回房间信息*/room_ptr create_room(uint64_t uid1, uint64_t uid2) {// 判断两个玩家是否都处于游戏大厅中if(_online_user-is_in_game_hall(uid1) false || _online_user-is_in_game_hall(uid2) false) {LOG(DEBUG, 玩家不在游戏大厅中匹配失败);return room_ptr();}// 创建游戏房间将用户信息添加到房间中std::unique_lockstd::mutex lock(_mutex);room_ptr rp(new room(_next_rid, _tb_user, _online_user));rp-add_white_user(uid1);rp-add_black_user(uid2);// 将游戏房间管理起来(建立房间id与房间信息以及玩家id与房间id的关联关系)_rooms[_next_rid] rp;_users[uid1] _next_rid;_users[uid2] _next_rid;// 更新下一个房间的房间id_next_rid;// 返回房间信息return rp;}/*通过房间id获取房间信息*/room_ptr get_room_by_rid(uint64_t rid) {std::unique_lockstd::mutex lock(_mutex);auto it _rooms.find(rid);if(it _rooms.end()) return room_ptr();return _rooms[rid];}/*通过用户id获取房间信息*/room_ptr get_room_by_uid(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);// 获取房间idauto it1 _users.find(uid);if(it1 _users.end()) return room_ptr();uint64_t rid _users[uid];// 获取房间信息(这里不能直接调用get_room_by_rid会造成死锁)auto it2 _rooms.find(rid);if(it2 _rooms.end()) return room_ptr();return _rooms[rid];}/*通过房间id销毁房间*/void remove_room(uint64_t rid) {// 通过房间id获取房间信息room_ptr rp get_room_by_rid(rid);if(rp.get() nullptr) return;// 通过房间信息获取房间中的玩家uint64_t white_user_id rp-_white_user_id;uint64_t black_user_id rp-_black_user_id;// 移除房间管理中的玩家信息std::unique_lockstd::mutex lock(_mutex);_users.erase(white_user_id);_users.erase(black_user_id);// 移除房间管理信息 -- 移除房间对应的shared_ptr(room_ptr)_rooms.erase(rid);}/*删除房间中的指定用户若房间中没有用户则销毁房间(用户断开websocket连接时调用)*/void remove_room_user(uint64_t uid) {// 通过玩家id获取房间信息room_ptr rp get_room_by_uid(uid);if(rp.get() nullptr) return;// 玩家退出rp-handler_exit(uid);// 如果房间中没有玩家了则移除房间if(rp-_player_count 0) remove_room(rp-_room_id);}
private:uint64_t _next_rid; //房间ID分配计数器std::mutex _mutex; user_table *_tb_user; // 管理玩家数据的句柄online_manager *_online_user; // 管理玩家在线状态的句柄std::unordered_mapuint64_t, room_ptr _rooms; // 建立房间id与房间信息的关联关系std::unordered_mapuint64_t, uint64_t _users; // 建立用户id与房间id的关联关系
};
#endif5. 用户 session 信息管理模块
什么是 cookiesession
在 web 开发中由于 HTTP 是一种无状态短连接的协议这就导致一个用户可能当前登录了但过一会在进行其他操作时原来的连接已经断开了而我们又不知道新连接对应的用户是谁。这就导致要么我们频繁让用户执行登录操作完成身份认证要么不给用户提供服务又或者在不确定当前用户身份和状态的情况下为用户提供服务。显然这些方式都是不合理的。为了解决这个问题有大佬就提出了 cookie 的方案 – 客户端在第一次登录成功后服务器会为响应添加一个 “Set-Cookie” 头部字段“Set-Cookie” 中包含了诸如 usernamepassword 这类信息客户端收到响应后会将 “Set-Cookie” 中的信息保存起来并且之后发送新的请求时会自动将 cookie 信息发送给服务器进行身份与状态验证从而避免了用户频繁登录的问题。但是这样简单的 cookie 机制会带来安全问题因为客户端可能会自己伪造 “Set-Cookie” 信息或者 HTTP 请求被中间人劫持导致 cookie 信息被篡改所以大佬又提出了 session 机制。session 机制是指客户端在第一次登录成功后服务器会为客户端实例化一个 session (会话) 对象该对象中保存了诸如用户 id、用户名、用户密码、用户状态 (登录/未登录等) 这类信息最重要的是服务器会为每一个 session 对象即每一个用户分配一个唯一的 session id (ssid)。
此后服务器与客户端就通过 cookie 和 session 相结合的方式完成用户身份与状态的验证
用户首次登录时服务器会为其实例化一个 session 对象然后将 ssid 添加到 “Set-Cookie” 头部字段中响应给客户端。客户端收到响应后会保存 cookie 信息并且以后每次请求都自动带上 cookie 信息发送给服务器。服务器收到新的客户端请求后会从请求头部中获取 cookie 信息如果 cookie 信息中没有 ssid 或者该 ssid 与服务器中所有的 session id 都不匹配时服务器会让客户端重新登录并为其实例化 session 对象。如果服务器中存在与该 ssid 匹配的 session 对象则为客户端提供服务。 基于上面的原理在本项目中我们也需要设计一个 session 类以及一个 session 管理类用来完成客户端身份与状态的验证以及 session 对象的管理。需要注意的是session 对象不能一直存在即当用户长时间无操作后我们需要删除服务器中该用户对应的 session 对象因此我们需要使用 WebSocketpp 的定时器功能对每个创建的 session 对象进行定时销毁否则也算是一种资源泄露。
session 类的具体功能如下
add_user为 session 对象关联具体的用户。get_user获取 session 对象关联的用户。is_login获取用户状态 (是否登录)。get_ssid获取 session id。set_timer设置 session 定时删除任务。get_timer获取 session 关联的定时器。
session 管理类的具体功能如下
create_session为指定用户创建 session 信息并返回 session 信息。get_session_by_ssid通过 sessionID 获取 session 信息。remove_session通过 sessionID 删除 session 信息。set_session_expire_time设置 session 过期时间。
session.hpp:
#ifndef __SESSION_HPP__
#define __SESSION_HPP__
#include online.hpp
#include logger.hpp
#include functionaltypedef enum {UNLOGIN, LOGIN
} ss_statu;/*用户session信息管理模块 -- 用于http短连接通信情况下用户状态的管理(登录/未登录)*/
/*session 类*/
class session {
public:session(uint64_t ssid) : _ssid(ssid), _statu(LOGIN) { LOG(DEBUG, session %d:%p 被创建, _ssid, this); }~session() { LOG(DEBUG, session %d:%p 被删除, _ssid, this); }/*添加用户*/void add_user(uint64_t uid) { _uid uid; }/*获取用户id*/uint64_t get_user() { return _uid; }/*获取用户状态(检查用户是否已登录)*/bool is_login() { return _statu LOGIN; }/*获取session id*/uint64_t get_ssid() { return _ssid; }/*设置session定时删除任务*/void set_timer(const wsserver_t::timer_ptr tp) { _tp tp; }/*获取session关联的定时器*/wsserver_t::timer_ptr get_timer() { return _tp; }
private:uint64_t _ssid; // session iduint64_t _uid; // session对应的用户idss_statu _statu; // 用户状态(登录/未登录)wsserver_t::timer_ptr _tp; // session关联的定时器
};#define SESSION_TIMEOUT 30000 //30s
#define SESSION_FOREVER -1/*使用智能指针来管理session信息*/
using session_ptr std::shared_ptrsession;/*session 管理类*/
class session_manager {
public:session_manager(wsserver_t *server): _server(server), _next_ssid(1) {LOG(DEBUG, 用户session管理模块初始化成功);}~session_manager() { LOG(DEBUG, 用户session管理模块已被销毁); }/*为指定用户创建session信息并返回*/session_ptr create_session(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);// 创建session信息session_ptr ssp(new session(_next_ssid));ssp-add_user(uid);// 建立sessionID与session信息的关联关系_sessions[_next_ssid] ssp;// 更新下一个session的id计数_next_ssid;return ssp;}/*通过sessionID获取session信息*/session_ptr get_session_by_ssid(uint64_t ssid) {std::unique_lockstd::mutex lock(_mutex);auto it _sessions.find(ssid);if(it _sessions.end()) return session_ptr();return _sessions[ssid];}/*删除session信息*/void remove_session(uint64_t ssid) {std::unique_lockstd::mutex lock(_mutex);_sessions.erase(ssid);}/*重新添加因cancel函数被删除的_sessions成员*/void append_session(session_ptr ssp) {std::unique_lockstd::mutex lock(_mutex);_sessions.insert(make_pair(ssp-get_ssid(), ssp)); // _sessions[ssp-get_ssid()] ssp;}/*设置session过期时间(毫秒)*//*基于websocketpp定时器(timer_ptr)来完成对session生命周期的管理*/void set_session_expire_time(uint64_t ssid, int ms) {//当客户端与服务器建立http短连接通信(登录/注册)时session应该是临时的需要设置定时删除任务//当客户端与服务器建立websocket长连接通信(游戏大厅/游戏房间)时session应该是永久的直到websocket长连接断开session_ptr ssp get_session_by_ssid(ssid);if(ssp.get() nullptr) return;// 获取session状态 -- session对象创建时默认没有关联time_ptr此时session是永久存在的(timer_ptrnullptr)wsserver_t::timer_ptr tp ssp-get_timer();// 1. 在session永久的情况下设置永久if(tp.get() nullptr ms SESSION_FOREVER) return;// 2. 在session永久的情况下设置定时删除任务else if(tp.get() nullptr ms ! SESSION_FOREVER) {wsserver_t::timer_ptr tp_task _server-set_timer(ms, std::bind(session_manager::remove_session, this, ssid));ssp-set_timer(tp_task); // 重新设置session关联的定时器}// 3. 在session定时删除的情况下设置永久(删除定时任务)else if(tp.get() ! nullptr ms SESSION_FOREVER) {// 注意websocketpp使用cancel函数删除定时任务会导致定时任务直接被执行所以我们需要重新向_sessions中添加ssid与session_ptr// 同时由于这个定时任务不是立即被执行的(服务器处理时才处理这个任务)所以我们不能在cancel函数后面直接重新添加session_ptr(这样可能出现先添加、再删除的情况)// 而是需要专门设置一个定时器来添加ssid与session_ptrtp-cancel();// 通过定时器来添加被删除的_sessions成员_server-set_timer(0, std::bind(session_manager::append_session, this, ssp)); ssp-set_timer(wsserver_t::timer_ptr()); // 将session关联的定时器设置为空(session永久有效)}// 4. 在session定时删除的情况下重置删除时间else {// 先删除定时任务tp-cancel();_server-set_timer(0, std::bind(session_manager::append_session, this, ssp)); ssp-set_timer(wsserver_t::timer_ptr()); // 将session关联的定时器设置为空(session永久有效)// 再重新添加定时任务wsserver_t::timer_ptr tp_task _server-set_timer(ms, std::bind(session_manager::remove_session, this, ssid));ssp-set_timer(tp_task); // 重新设置session关联的定时器}}
private:uint64_t _next_ssid; // sessionID计数器 std::mutex _mutex; std::unordered_mapuint64_t, session_ptr _sessions; // 建立ssid与session信息之间的关联关系wsserver_t *_server; // 服务器指针对象用于设置定时任务
};
#endif6. 匹配对战管理模块
匹配对战管理模块主要负责游戏大厅内玩家开始匹配与取消匹配的功能本模块将玩家按照天梯分数分为三个段位 (玩家的初始天梯分数为1000分)
青铜天梯分数小于2000分。黄金天梯分数大于等于2000分但小于3000分。王者天梯分数大于等于3000分。
本模块的设计思想是为不同段位的玩家分别设计一个匹配阻塞队列
当有玩家开始匹配时服务器会将该玩家加入对应的匹配队列中并唤醒该匹配队列的线程。当有玩家取消匹配时会将该玩家从对应的匹配队列中移除.当某个匹配队列中的玩家人数不足两个时服务器会将该匹配队列的线程阻塞等待有新玩家加入匹配队列时被唤醒。当某个匹配队列中的玩家人数达到两个时服务器会将队头的两个玩家出队列并给对应的玩家推送匹配成功的信息同时为匹配成功的玩家创建游戏房间。
最后和游戏房间管理模块一样这里我们也给出 WebSocket 通信的消息格式。
游戏匹配成功的消息
{optype: match_success, //表⽰成匹配成功result: true
}matcher.hpp:
#ifndef __MATCHER_HPP__
#define __MATCHER_HPP__
#include db.hpp
#include room.hpp
#include util.hpp
#include online.hpp
#include list
#include thread
#include mutex
#include condition_variable/*用户对战匹配管理模块 -- 将用户按分数分为青铜、黄金、王者三档并分别为它们设计一个匹配队列队列元素2则匹配成功否则阻塞*/
/*匹配队列类*/
template class T
class match_queue {
public:match_queue() {}~match_queue() {}/*目标元素入队列并唤醒线程*/void push(const T data) {std::unique_lockstd::mutex lock(_mutex);_list.push_back(data);LOG(DEBUG, %d用户加入匹配队列, data);// 匹配队列每新增一个元素就唤醒对应的匹配线程判断是否满足匹配要求(队列人数2)_cond.notify_all();}/*队头元素出队列并返回队头元素*/bool pop(T data) {std::unique_lockstd::mutex lock(_mutex);if(_list.empty()) return false;data _list.front();_list.pop_front();LOG(DEBUG, %d用户从匹配队列中移除, data);return true;}/*移除队列中的目标元素*/void remove(const T data) {std::unique_lockstd::mutex lock(_mutex);_list.remove(data);LOG(DEBUG, %d用户从匹配队列中移除, data);}/*阻塞线程*/void wait() {std::unique_lockstd::mutex lock(_mutex);_cond.wait(lock);}/*获取队列元素个数*/int size() { std::unique_lockstd::mutex lock(_mutex);return _list.size(); }/*判断队列是否为空*/bool empty() {std::unique_lockstd::mutex lock(_mutex); return _list.empty();}
private:std::listT _list; // 使用双向链表而不是queue充当匹配队列便于用户取消匹配时将该用户从匹配队列中移除std::mutex _mutex; // 实现线程安全std::condition_variable _cond; // 条件变量当向队列中push元素时唤醒用于阻塞消费者
};/*匹配管理类*/
class matcher {
private:void handler_match(match_queueuint64_t mq) {while(true) {// 检查匹配条件是否满足(人数2)不满足则继续阻塞while(mq.size() 2) mq.wait();// 条件满足从队列中取出两个玩家uint64_t uid1, uid2;if(mq.pop(uid1) false) continue;if(mq.pop(uid2) false) {// 如果第二个玩家出队列失败则需要将第一个玩家重新添加到队列中this-add(uid1);continue;}// 检查两个玩家是否都处于大厅在线状态若一方掉线则需要将另一方重新添加到队列wsserver_t::connection_ptr conn1 _om-get_conn_from_hall(uid1);wsserver_t::connection_ptr conn2 _om-get_conn_from_hall(uid2);if(conn1.get() nullptr) {this-add(uid2);continue;}if(conn2.get() nullptr) {this-add(uid1);continue;}// 为两个玩家创建房间失败则重新添加到队列room_ptr rp _rm-create_room(uid1, uid2);if(rp.get() nullptr) {this-add(uid1);this-add(uid2);continue;}// 给玩家返回匹配成功的响应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_low_entry() { handler_match(_q_low); }void th_mid_entry() { handler_match(_q_mid); }void th_high_entry() { handler_match(_q_high); }
public:matcher(user_table *ut, online_manager *om, room_manager *rm): _ut(ut), _om(om), _rm(rm), _th_low(std::thread(matcher::th_low_entry, this)),_th_mid(std::thread(matcher::th_mid_entry, this)),_th_high(std::thread(matcher::th_high_entry, this)) {LOG(DEBUG, 游戏对战匹配管理模块初始化完毕);}~matcher() {LOG(DEBUG, 游戏对战匹配管理模块已被销毁);}/*添加用户到匹配队列*/bool add(uint64_t uid) {// 根据用户id获取用户数据库信息Json::Value user;if(_ut-select_by_id(uid, user) false) {LOG(DEBUG, 查找玩家%d信息失败, uid);return false;}// 根据用户分数将用户添加到对应的匹配队列中去int score user[score].asInt();if(score 2000) _q_low.push(uid);else if(score 2000 score 3000) _q_mid.push(uid);else _q_high.push(uid);return true;}/*将用户从匹配队列中移除*/bool remove(uint64_t uid) {// 根据用户id获取用户数据库信息Json::Value user;if(_ut-select_by_id(uid, user) false) {LOG(DEBUG, 查找用户%d信息失败, uid);return false;}// 根据用户分数将用户从对应的匹配队列中移除int score user[score].asInt();if(score 2000) _q_low.remove(uid);else if(score 2000 score 3000) _q_mid.remove(uid);else _q_high.remove(uid);return true; }
private:// 三个匹配队列(青铜/黄金/王者 - low/mid/high)match_queueuint64_t _q_low;match_queueuint64_t _q_mid;match_queueuint64_t _q_high;// 三个管理匹配队列的线程std::thread _th_low;std::thread _th_mid;std::thread _th_high;room_manager *_rm; // 游戏房间管理句柄online_manager *_om; // 在线用户管理句柄user_table *_ut; // 用户数据管理句柄
};
#endif7. 整合封装服务器模块
服务器模块是对当前所实现的所有模块进行整合并进行服务器搭建的⼀个模块。目的是封装实现出⼀个 gobang_server 的服务器模块类向外提供搭建五子棋对战服务器的接口。程序员通过实例化服务器模块类对象可以简便的完成服务器的搭建。
7.1 网络通信接口设计
在实现具体的服务器类之前我们需要对 HTTP 网络通信的通信接口格式进行设计确保服务器能够根据客户端请求的格式判断出这是一个什么类型请求并在完成业务处理后给客户端以特定格式的响应。
本项目采用 RESTful 风格通信接口
资源定位每个资源都有一个唯一的 URI 标识符比如 /login.html 表示获取登录页面/hall 表示进入游戏大厅请求。使用 HTTP 方法使用 HTTP 的 GET 和 POST 方法来对资源进行获取与提交操作。无状态性客户端状态信息由客户端保存 (cookiesession)服务器不保存客户端的每个请求都是独立的。统一接口使用统一的接口约束包括使用标准的 HTTP 方法和状态码使用标准的媒体类型 JSON 来传输数据。
本项目中客户端的 HTTP 请求分为静态资源请求与动态功能请求静态资源请求指获取游戏注册页面、登录页面等动态功能请求指用户登录/注册请求、协议切换请求等。
7.1.1 静态资源请求
静态资源页面在后台服务器上就是一个个 HTML/CSS/JS 文件而静态资源请求其实就是让服务器把对应的文件发送给客户端。
获取注册界面
// 客户端请求
GET /register.html HTTP/1.1
报头其他字段// 服务器响应
// 响应报头
HTTP/1.1 200 OK
Content-Length: XXX
Content-Type: text/html
报头其他字段
// 响应正文
register.html文件中的数据获取登录界面、游戏大厅页面与游戏房间页面类似
// 客户端请求
GET /login.html HTTP/1.1 or GET /game_hall.html HTTP/1.1 or GET /game_room.html HTTP/1.1
报头其他字段// 服务器响应
// 响应报头
HTTP/1.1 200 OK
Content-Length: XXX
Content-Type: text/html
报头其他字段
// 响应正文
login.html/game_hall/game_room文件中的数据7.1.2 动态功能请求
用户注册请求
// 客户端请求
// 请求报头
POST /reg HTTP/1.1
Content-Type: application/json
Content-Length: XXX
// 请求正文 -- 序列化的用户名和用户密码
{username:zhangsan, password:123456}// 服务器成功的响应
// 响应报头
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15
// 响应正文
{result:true, reason: 用户注册成功}// 服务器失败的响应
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: XXX
{result:false, reason: 错误信息比如该用户名已被占用}用户登录请求
// 客户端请求
POST /login HTTP/1.1
Content-Type: application/json
Content-Length: XXX
{username:zhangsan, password:123456}// 服务器成功的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: XXX
{result:true, reason: 用户登录成功}// 服务器失败的响应
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: XXX
{result:false, reason: 错误信息比如用户名或密码错误}获取玩家详细信息请求
// 客户端请求
GET /info HTTP/1.1
Content-Type: application/json
Content-Length: 0// 服务器成功的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: XXX
{id:1, username:zhangsan, score:1000, total_count:4, win_count:2}// 服务器失败的响应
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: XXX
{result:false, reason: 错误信息比如用户信息不存在}游戏大厅 WebSocket 长连接协议切换请求
// 客户端请求
/* ws://localhost:9000/match */
GET /hall HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......// 服务器成功的响应
HTTP/1.1 101 Switching
...游戏房间 WebSocket 长连接协议切换请求
// 客户端请求
/* ws://localhost:9000/match */
GET /room HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......// 服务器成功的响应
HTTP/1.1 101 Switching
...7.1.3 WebSocket 通信格式
上面我们提到的不管是静态资源请求还是动态功能请求它们本质上都是 HTTP 请求所以我们使用 RESTful 风格的通信接口但是当玩家进入游戏大厅或者游戏房间后客户端就会向服务器发送协议切换请求 (协议切换请求本身是 HTTP 请求)将 HTTP 短连接通信协议升级为 WebSocket 长连接通信协议。
由于 WebSocket 协议是一种全双工的持久连接协议它允许在客户端和服务器之间进行双向实时通信所以我们每次通信时直接使用 WebSocketpp::server 中的 send 接口向对方发送消息即可而不再需要重新建立连接。
但是我们仍然需要事先规定好发送消息中不同字段代表的含义这样才能正确区分收到的消息类型从而根据消息不同的类型执行不同的处理函数并返回不同的消息。
游戏大厅 WebSocket 握手成功后的回复
// 游戏大厅进入成功
{optype: hall_ready, result: true
}// 游戏大厅进入失败
{optype: hall_ready, result: false,reason: 失败原因
}玩家开始匹配消息
// 开始匹配消息
{optype: match_start
}// 后台正确处理后回复的消息
{optype: match_startresult: true,
}玩家停止匹配消息
// 停止匹配消息
{optype: match_stop
}// 后台正确处理后回复的消息
{optype: match_stopresult: true
}游戏匹配成功后后台回复的消息
{optype: match_success, result: true
}游戏房间 WebSocket 握手成功后的回复
// 游戏房间创建成功
{optype: room_ready,result: true,room_id: 222, //房间IDuid: 1, //⾃⾝IDwhite_id: 1, //⽩棋IDblack_id: 2, //⿊棋ID
}// 游戏房间创建失败
{optype: room_ready,result: false,reason: 失败原因
}玩家下棋的消息
// 玩家下棋消息
{optype: put_chess, // put_chess表示当前请求是下棋操作room_id: 222, // room_id 表⽰当前动作属于哪个房间uid: 1, // 当前的下棋操作是哪个用户发起的row: 3, // 当前下棋位置的⾏号col: 2 // 当前下棋位置的列号
}// 下棋成功后后台回复的消息
{optype: put_chess,result: true,reason: 下棋成功或游戏胜利或游戏失败,room_id: 222,uid: 1,row: 3,col: 2,winner: 0 // 游戏获胜者0表示未分胜负!0表示已分胜负
}// 下棋失败后后台回复的消息
{optype: put_chess,result: false,reason: 下棋失败的原因,room_id: 222, uid: 1,row: 3,col: 2,winner: 0
}玩家聊天的消息
// 玩家聊天消息
{optype: chat, // chat表示当前请求是下棋操作room_id: 222, // room_id 表⽰当前动作属于哪个房间uid: 1, // 当前的下棋操作是哪个用户发起的message: 你好 // 聊天消息的具体内容
}// 聊天消息发送成功后台回复的消息
{optype: chat,result: true,room_id: 222,uid: 1,message: 你好
}// 聊天消息发送失败后台回复的消息
{optype: chat,result: false,reason: 错误原因比如消息中包含敏感词,room_id: 222,uid: 1,message: 你好
}未知类型的消息
{optype: 消息的类型,result: false,reason: 未知类型的消息
}7.2 服务器模块实现
关于如何使用 WebSocketpp 来搭建一个服务器我们在上面前置知识了解那里已经说过了大体流程如下
实例化一个 websocketpp::server 对象。设置日志等级。(本项目中我们使用自己封装的日志函数所以这里设置日志等级为 none)初始化 asio 调度器。设置处理 http 请求、websocket 握手成功、websocket 连接关闭以及收到 websocket 消息的回调函数。设置监听端口。开始获取 tcp 连接。启动服务器。
class gobang_server {
public:/*成员初始化与服务器回调函数设置*/gobang_server(const std::string host, const std::string user, const std::string passwd, \const std::string db gobang, uint16_t port 4106): _wwwroot(WWWROOT), _ut(host, user, passwd, db, port), _sm(_wssrv), _rm(_ut, _om), _mm(_ut, _om, _rm) {// 设置日志等级_wssrv.set_access_channels(websocketpp::log::alevel::none);// 初始化asio调度器_wssrv.init_asio();// 设置回调函数_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(uint16_t port) {// 设置监听端口_wssrv.listen(port);_wssrv.set_reuse_addr(true);// 开始获取新连接_wssrv.start_accept();// 启动服务器_wssrv.run(); }
private:std::string _wwwroot; // 静态资源根目录user_table _ut; // 用户数据管理模块句柄session_manager _sm; // 用户session信息管理模块句柄online_manager _om; // 用户在线信息管理模块句柄room_manager _rm; // 游戏房间管理模块句柄matcher _mm; // 用户对战匹配管理模块句柄wsserver_t _wssrv; // websocketpp::server 句柄
}; 我们的重难点在于如何实现 http 请求、websocket 握手成功、websocket 连接关闭以及 websocket 消息这四个回调函数。具体实现如下
/*
服务器模块
通过对之前所有模块进行整合以及进行服务器搭建最终封装实现出⼀个gobang_server的服务器模块类向外提供搭建五⼦棋对战服务器的接⼝。
达到通过实例化的对象就可以简便的完成服务器搭建的目的
*/#ifndef __SERVER_HPP__
#define __SERVER_HPP__
#include util.hpp
#include db.hpp
#include online.hpp
#include room.hpp
#include matcher.hpp
#include session.hpp#define WWWROOT ./wwwroottypedef websocketpp::serverwebsocketpp::config::asio wsserver_t;class gobang_server {
private:/*http静态资源请求处理函数(注册界面、登录界面、游戏大厅界面)*/void file_handler(wsserver_t::connection_ptr conn) {// 获取http请求对象与请求uriwebsocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();// 根据uri组合出文件路径如果文件路径是目录(/结尾)则追加login.html否则返回相应界面std::string pathname _wwwroot uri;if(pathname.back() /) {pathname login.html;}// 读取文件内容如果文件不存在则返回404std::string body;if(file_util::read(pathname.c_str(), body) false) {body htmlheadmeta charsetUTF-8//headbodyh1 404 Not Found /h1/body/html;// 设置响应状态码conn-set_status(websocketpp::http::status_code::not_found);}else conn-set_status(websocketpp::http::status_code::ok);// 添加响应头部conn-append_header(Content-Length, std::to_string(body.size()));// 设置响应正文conn-set_body(body); }/*处理http响应的子功能函数*/void http_resp(wsserver_t::connection_ptr conn, bool result, websocketpp::http::status_code::value code, const std::string reason) {// 设置响应正文及其序列化Json::Value resp;std::string resp_body;resp[result] result;resp[reason] reason;json_util::serialize(resp, resp_body);// 设置响应状态码添加响应正文以及正文类型conn-set_status(code);conn-append_header(Content-Type, application/json);conn-set_body(resp_body);}/*http动态功能请求处理函数 -- 用户注册*/void reg(wsserver_t::connection_ptr conn) {// 获取json格式的请求正文std::string req_body conn-get_request_body();// 将正文反序列化得到username和passwordJson::Value user_info;if(json_util::deserialize(req_body, user_info) false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 请求正文格式错误);}// 数据库新增用户if(user_info[username].isNull() || user_info[password].isNull()) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 请输入用户名/密码);}if(_ut.registers(user_info) false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 该用户名已被占用);}return http_resp(conn, true, websocketpp::http::status_code::ok, 用户注册成功);}/*http动态功能请求处理函数 -- 用户登录*/void login(wsserver_t::connection_ptr conn) {// 获取请求正文并反序列化std::string req_body conn-get_request_body();Json::Value user_info;if(json_util::deserialize(req_body, user_info) false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 请求正文格式错误);}if(user_info[username].isNull() || user_info[password].isNull()) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 请输入用户名/密码);}// 用户登录 -- 登录失败返回404if(_ut.login(user_info) false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 用户名/密码错误);}// 登录成功则为用户创建session信息以及session生命周期session_ptr ssp _sm.create_session(user_info[id].asUInt64());if(ssp.get() nullptr) {return http_resp(conn, false, websocketpp::http::status_code::internal_server_error, 用户会话创建失败);}_sm.set_session_expire_time(ssp-get_ssid(), SESSION_TIMEOUT);// 设置过响应头部 将cookie返回给客户端std::string cookie_ssid SSID std::to_string(ssp-get_ssid());conn-append_header(Set-Cookie, cookie_ssid);return http_resp(conn, true, websocketpp::http::status_code::ok, 用户登录成功);}/*从http请求头部Cookie中获取指定key对应的value*/bool get_cookie_val(const std::string cookie_str, const std::string key, std::string val) {// cookie_str格式SSIDXXX; path/XXX// 先以逗号为分割将cookie_str中的各个cookie信息分割开std::vectorstd::string cookies;string_util::split(cookie_str, ;, cookies);// 再以等号为分割将单个cookie中的key与val分割开比对查找目标key对应的valfor(const auto cookie : cookies) {std::vectorstd::string kv;string_util::split(cookie, , kv);if(kv.size() ! 2) continue;if(kv[0] key) {val kv[1];return true;}}return false;}/*http动态功能请求处理函数 -- 获取用户信息*/void info(wsserver_t::connection_ptr conn) {// 通过http请求头部中的cookie字段获取用户ssidstd::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 找不到Cookie信息请重新登录);}std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 找不到Session信息请重新登录);}// 根据ssid_str获取用户Session信息session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, Session已过期请重新登录);}// 通过用户session获取用户id再根据用户id获取用户详细信息uint64_t uid ssp-get_user(); Json::Value user;if(_ut.select_by_id(uid, user) false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, 用户信息不存在);}// 返回用户详细信息std::string body;json_util::serialize(user, body);std::string resp_cookie SSID ssid_str;conn-set_status(websocketpp::http::status_code::ok);conn-append_header(Content-Type, application/json);conn-append_header(Set-Cookie, resp_cookie);conn-set_body(body);// 更新用户session过期时间_sm.set_session_expire_time(ssp-get_ssid(), SESSION_TIMEOUT); }
private:/*************************************************************************************************//*http请求回调函数*//*************************************************************************************************/void http_callback(websocketpp::connection_hdl hdl) {wsserver_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) reg(conn);else if(method POST uri /login) login(conn);else if(method GET uri /info) info(conn);// 静态资源请求else file_handler(conn);}/*游戏大厅websocket长连接建立后的响应子函数*/void game_hall_resp(wsserver_t::connection_ptr conn, bool result, const std::string reason ) {Json::Value resp;resp[optype] hall_ready;resp[result] result;// 只有错误才返回错误信息reasonif(result false) resp[reason] reason;std::string body;json_util::serialize(resp, body);conn-send(body);}/*wsopen_callback子函数 -- 游戏大厅websocket长连接建立后的处理函数*/void wsopen_game_hall(wsserver_t::connection_ptr conn) {// 检查用户是否登录 -- 检查cookiesession信息// 通过http请求头部中的cookie字段获取用户ssidstd::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) {return game_hall_resp(conn, false, 找不到Cookie信息请重新登录);}std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) {return game_hall_resp(conn, false, 找不到Session信息请重新登录);}// 根据ssid_str获取用户Session信息session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) {return game_hall_resp(conn, false, Session已过期请重新登录);}// 通过用户session获取用户iduint64_t uid ssp-get_user();// 检查用户是否重复登录 -- 用户游戏大厅长连接/游戏房间长连接是否已经存在if(_om.is_in_game_hall(uid) true) {return game_hall_resp(conn, false, 玩家重复登录);} // 将玩家及其连接加入到在线游戏大厅中_om.enter_game_hall(uid, conn);// 返回响应game_hall_resp(conn, true);// 将用户Session过期时间设置为永不过期_sm.set_session_expire_time(ssp-get_ssid(), SESSION_FOREVER);}/*游戏房间websocket长连接建立后的响应子函数*/void game_room_resp(wsserver_t::connection_ptr conn, bool result, const std::string reason, uint64_t room_id 0, uint64_t self_id 0, uint64_t white_id 0, uint64_t black_id 0) {Json::Value resp;resp[optype] room_ready;resp[result] result;// 如果成功返回room_id,self_id,white_id,black_id等信息如果错误则返回错误信息if(result true) {resp[room_id] (Json::UInt64)room_id;resp[uid] (Json::UInt64)self_id;resp[white_id] (Json::UInt64)white_id;resp[black_id] (Json::UInt64)black_id;}else resp[reason] reason; std::string body;json_util::serialize(resp, body);conn-send(body);} /*wsopen_callback子函数 -- 游戏房间websocket长连接建立后的处理函数*/void wsopen_game_room(wsserver_t::connection_ptr conn) {// 获取cookiesession信息std::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) {return game_room_resp(conn, false, 找不到Cookie信息请重新登录);}std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) {return game_room_resp(conn, false, 找不到Session信息请重新登录);}// 根据ssid_str获取用户Session信息session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) {return game_room_resp(conn, false, Session已过期请重新登录);} // 判断用户是否已经处于游戏大厅/房间中了(在创建游戏房间长连接之前游戏大厅的长连接已经断开了) -- 在线用户管理if(_om.is_in_game_hall(ssp-get_user()) || _om.is_in_game_room(ssp-get_user())) {return game_room_resp(conn, false, 玩家重复登录);} // 判断游戏房间是否被创建 -- 游戏房间管理room_ptr rp _rm.get_room_by_uid(ssp-get_user());if(rp.get() nullptr) {return game_room_resp(conn, false, 找不到房间信息);}// 将玩家加入到在线游戏房间中_om.enter_game_room(ssp-get_user(), conn);// 返回响应信息game_room_resp(conn, true, , rp-_room_id, ssp-get_user(), rp-_white_user_id, rp-_black_user_id);// 将玩家session设置为永不过期_sm.set_session_expire_time(ssp-get_ssid(), SESSION_FOREVER);}/*************************************************************************************************//*websocket长连接建立之后的处理函数*//*************************************************************************************************/void wsopen_callback(websocketpp::connection_hdl hdl) {// 获取通信连接、http请求对象和请求uriwsserver_t::connection_ptr conn _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();// 进入游戏大厅与进入游戏房间需要分别建立websocket长连接if(uri /hall) wsopen_game_hall(conn);else if(uri /room) wsopen_game_room(conn);}/*wsclose_callback子函数 -- 游戏大厅websocket长连接断开后的处理函数*/void wsclose_game_hall(wsserver_t::connection_ptr conn) {// 获取cookiesession如果不存在则说明websocket长连接未建立(websocket长连接建立后Session永久存在)直接返回std::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) return;std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) return;session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) return;// 将玩家从游戏大厅移除_om.exit_game_hall(ssp-get_user());// 将玩家session设置为定时删除_sm.set_session_expire_time(ssp-get_ssid(), SESSION_TIMEOUT); } /*wsclose_callback子函数 -- 游戏房间websocket长连接断开后的处理函数*/void wsclose_game_room(wsserver_t::connection_ptr conn) {// 获取cookiesession如果不存在直接返回std::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) return;std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) return;session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) return;// 将玩家从在线用户管理的游戏房间中移除_om.exit_game_room(ssp-get_user());// 将玩家从游戏房间管理的房间中移除_rm.remove_room_user(ssp-get_user());// 设置玩家session为定时删除_sm.set_session_expire_time(ssp-get_ssid(), SESSION_TIMEOUT); }/*************************************************************************************************//*websocket长连接断开之间的处理函数*//*************************************************************************************************/void wsclose_callback(websocketpp::connection_hdl hdl) {// 获取通信连接、http请求对象和请求uriwsserver_t::connection_ptr conn _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();// 离开游戏大厅与离开游戏房间需要分别断开websocket长连接if(uri /hall) wsclose_game_hall(conn);else if(uri /room) wsclose_game_room(conn); }/*wsmsg_callback子函数 -- 游戏大厅通信处理函数*/void wsmsg_game_hall(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {// 获取cookiesession如果不存在则返回错误信息std::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) {return game_hall_resp(conn, false, 找不到Cookie信息请重新登录);}std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) {return game_hall_resp(conn, false, 找不到Session信息请重新登录);}session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) {return game_hall_resp(conn, false, Session已过期请重新登录);}// 获取请求信息 std::string req_msg_body msg-get_payload(); Json::Value req_msg;if(json_util::deserialize(req_msg_body, req_msg) false) {return game_hall_resp(conn, false, 请求信息解析失败); }// 处理请求信息 -- 开始对战匹配与停止对战匹配Json::Value resp req_msg;std::string resp_body;// 开始对战匹配请求则将用户加入到匹配队列中取消对战匹配请求则将用户从匹配队列中移除if(req_msg[optype].isNull() false req_msg[optype].asString() match_start) {_mm.add(ssp-get_user());resp[result] true;json_util::serialize(resp, resp_body);conn-send(resp_body);} else if(req_msg[optype].isNull() false req_msg[optype].asString() match_stop) {_mm.remove(ssp-get_user());resp[result] true;json_util::serialize(resp, resp_body);conn-send(resp_body);} else {resp[optype] req_msg[optype].asString();resp[result] false;resp[reason] 未知类型的消息;json_util::serialize(resp, resp_body);conn-send(resp_body);} } /*wsmsg_callback子函数 -- 游戏房间通信处理函数*/void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {// 获取cookiesession如果不存在则返回错误信息std::string cookie_str conn-get_request_header(Cookie);if(cookie_str.empty()) {return game_room_resp(conn, false, 找不到Cookie信息请重新登录);}std::string ssid_str;if(get_cookie_val(cookie_str, SSID, ssid_str) false) {return game_room_resp(conn, false, 找不到Session信息请重新登录);}session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() nullptr) {return game_room_resp(conn, false, Session已过期请重新登录);}// 获取房间信息room_ptr rp _rm.get_room_by_uid(ssp-get_user());if(rp.get() nullptr) {return game_room_resp(conn, false, 找不到房间信息);}// 获取请求信息 std::string req_msg_body msg-get_payload(); Json::Value req_msg;if(json_util::deserialize(req_msg_body, req_msg) false) {return game_room_resp(conn, false, 请求信息解析失败); }// 处理请求信息 -- 下棋动作与聊天动作rp-handler(req_msg);} /*************************************************************************************************//*websocket长连接建立后通信的处理函数*//*************************************************************************************************/void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {// 获取通信连接、http请求对象和请求uriwsserver_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) wsmsg_game_hall(conn, msg);else if(uri /room) wsmsg_game_room(conn, msg); }
public:/*成员初始化与服务器回调函数设置*/gobang_server(const std::string host, const std::string user, const std::string passwd, \const std::string db gobang, uint16_t port 4106): _wwwroot(WWWROOT), _ut(host, user, passwd, db, port), _sm(_wssrv), _rm(_ut, _om), _mm(_ut, _om, _rm) {// 设置日志等级_wssrv.set_access_channels(websocketpp::log::alevel::none);// 初始化asio调度器_wssrv.init_asio();// 设置回调函数_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(uint16_t port) {// 设置监听端口_wssrv.listen(port);_wssrv.set_reuse_addr(true);// 开始获取新连接_wssrv.start_accept();// 启动服务器_wssrv.run(); }
private:std::string _wwwroot; // 静态资源根目录user_table _ut; // 用户数据管理模块句柄session_manager _sm; // 用户session信息管理模块句柄online_manager _om; // 用户在线信息管理模块句柄room_manager _rm; // 游戏房间管理模块句柄matcher _mm; // 用户对战匹配管理模块句柄wsserver_t _wssrv; // websocketpp::server 句柄
};
#endif8. 前端界面模块
8.1 用户注册界面
register.html:
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title注册/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/login.css
/head
bodydiv classnav网络五子棋对战游戏 /divdiv classlogin-container!-- 登录界面的对话框 --div classlogin-dialog!-- 提示信息 --h3注册/h3!-- 这个表示一行 --div classrowspan用户名/spaninput typetext iduser_name nameusername/div!-- 这是另一行 --div classrowspan密码/spaninput typepassword idpassword namepassword/div!-- 提交按钮 --div classrow!--给提交按钮添加点击事件 -- 调用注册函数reg--button idsubmit onclickreg()提交/button/div/div/div script srcjs/jquery.min.js/scriptscript// 封装实现注册函数function reg() {// 获取输入框中的username和password并将它们组织成json格式字符串var reg_info {username: document.getElementById(user_name).value,password: document.getElementById(password).value};// 通过ajax向服务器发送注册请求$.ajax({url: /reg,type: post,data: JSON.stringify(reg_info),// 请求失败清空输入框中的内容并提示错误信息请求成功则返回用户登录页面success: function(res) {if(res.result false) {document.getElementById(user_name).value ;document.getElementById(password).value ;alert(res.reason);} else {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
/html8.2 用户登录界面
login.html:
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title登录/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/login.css
/head
bodydiv classnav网络五子棋对战游戏/divdiv classlogin-container!-- 登录界面的对话框 --div classlogin-dialog!-- 提示信息 --h3登录/h3!-- 这个表示一行 --div classrowspan用户名/spaninput typetext iduser_name/div!-- 这是另一行 --div classrowspan密码/spaninput typepassword idpassword/div!-- 提交按钮 --div classrow!--为按钮添加点击事件调用登录函数--button idsubmit onclicklogin()提交/button/div/div/divscript src./js/jquery.min.js/scriptscriptfunction login() {// 获取输入框中的username和passwordvar log_info {username: document.getElementById(user_name).value,password: document.getElementById(password).value};// 通过ajax向服务器发送登录请求$.ajax({url: /login,type: post,data: JSON.stringify(log_info),// 请求成功返回游戏大厅页面请求失败则清空输入框中的内容并提示错误信息success: function(res) {alert(登录成功);window.location.assign(/game_hall.html);},error: function(xhr) {document.getElementById(user_name).value ;document.getElementById(password).value ;alert(JSON.stringify(xhr));}})}/script
/body
/html8.3 游戏大厅界面
game_hall.html:
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏大厅/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/game_hall.css
/head
bodydiv classnav网络五子棋对战游戏/div!-- 整个页面的容器元素 --div classcontainer!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 --div!-- 展示用户信息 --div idscreen/div!-- 匹配按钮 --div idmatch-button开始匹配/div/div/divscript src./js/jquery.min.js/scriptscriptws_hdl null;//设置离开当前页面后立即断开websocket链接window.onbeforeunload function () {ws_hdl.close();}// 获取玩家信息展示在游戏大厅与websocket长连接切换function get_user_info() {// 通过ajax向服务器发送获取用户信息请求$.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;// 获取玩家信息成功之后将http短连接协议切换为websocket长连接切换ws_url ws:// location.host /hall;ws_hdl new WebSocket(ws_url);// 为websocket各种触发事件设置回调函数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);}})}// 匹配按钮一共有两种状态 -- 未开始匹配(unmatched)和匹配中(matching)var button_statu unmatched;// 为匹配按钮添加点击事件var button_ele document.getElementById(match-button);button_ele.onclick function() {// 在没有匹配状态下点击按钮则发送开始匹配请求if(button_statu unmatched) {var req { optype: match_start };ws_hdl.send(JSON.stringify(req));}// 在匹配状态下点击按钮则范式停止匹配请求else if(button_statu matching) {var req { optype: match_stop };ws_hdl.send(JSON.stringify(req));}}function ws_onopen() {console.log(游戏大厅长连接建立成功);}function ws_onclose() {console.log(游戏大厅长连接断开);}function ws_onerror() {console.log(游戏大厅长连接建立出错);}// 服务器响应处理函数function ws_onmessage(evt) {// 判断请求是否被成功处理如果处理失败则提示错误信息并跳转登录页面var resp JSON.parse(evt.data);if(resp.result false) {alert(evt.data)location.replace(/login.html);return;}// 根据不同的响应类型进行不同的操作(成功建立大厅长连接、开始匹配、停止匹配、匹配成功以及未知响应类型)if(resp.optype hall_ready) {} else if(resp.optype match_start) {console.log(玩家已成功加入匹配队列);button_statu matching;button_ele.innerHTML 匹配中... (点击停止匹配);} else if(resp.optype match_stop) {console.log(玩家已从匹配队列中移除);button_statu unmatched;button_ele.innerHTML 开始匹配;} else if(resp.optype match_success) {alert(匹配成功);location.replace(/game_room.html);}else {alert(evt.data);location.replace(/login.html);}}// 调用获取玩家信息函数get_user_info();/script
/body
/html8.4 游戏房间界面
game_room.html:
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏房间/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/game_room.css
/head
bodydiv classnav网络五子棋对战游戏/divdiv classcontainerdiv idchess_area!-- 棋盘区域, 需要基于 canvas 进行实现 --canvas idchess width450px height450px/canvas!-- 显示区域 --div idscreen 等待玩家连接中... /div/divdiv idchat_area width400px height300pxdiv idchat_showp idself_msg你好/p/brp idpeer_msg你好/p/br/divdiv idmsg_showinput typetext idchat_inputbutton idchat_button发送/button/div/div/divscriptlet chessBoard [];let BOARD_ROW_AND_COL 15;let chess document.getElementById(chess);//获取chess控件区域2d画布let context chess.getContext(2d);// 将http协议切换为游戏房间的websocket长连接协议var ws_url ws:// location.host /room;var ws_hdl new WebSocket(ws_url);// 设置离开当前页面立即断开websocket连接window.onbeforeunload function () {ws_hdl.close();}// 保存房间信息与是否轮到己方走棋var room_info;var is_me;function initGame() {initBoard();// 背景图片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() {context.strokeStyle #BFBFBF;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();//createLinearGradient() 方法创建放射状/圆形渐变对象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) {// 如果当前轮到对方走棋则直接返回if(is_me false) {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;}// 发送走棋请求send_chess(row, col);}// 发送走棋请求(websocket长连接通信直接使用ws_hdl.send而不是通过ajax)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));}// 聊天动作// 给消息发送按钮添加点击事件var chat_button_div document.getElementById(chat_button);chat_button_div.onclick function() {// 获取聊天输入框中的消息var chat_msg {optype: chat,room_id: room_info.room_id,uid: room_info.uid,message: document.getElementById(chat_input).value};// 将消息发送给服务器ws_hdl.send(JSON.stringify(chat_msg)); } // websocket各种事件的执行函数ws_hdl.onopen function() {console.log(游戏房间长连接建立成功);}ws_hdl.onclose function() {console.log(游戏房间长连接断开);}ws_hdl.onerror function() {console.log(游戏房间长连接建立出错);}// 更新screen显示的内容function set_screen(me) {var screen_div document.getElementById(screen);if(me) screen_div.innerHTML 轮到己方走棋...;else screen_div.innerHTML 轮到对方走棋...;}ws_hdl.onmessage function(evt) {console.log(message: evt.data);var resp JSON.parse(evt.data);// 收到room_ready响应消息if(resp.optype room_ready) {// 保存房间信息与执棋用户room_info resp; // 规定白棋先走is_me (room_info.uid room_info.white_id ? true : false);if(resp.result false) {alert(resp.reason);location.replace(/login.html);} else {// 更新screen显示的内容set_screen(is_me);// 初始化游戏initGame();}}// 收到put_chess响应消息else if(resp.optype put_chess) {// 判断走棋是否成功if(resp.result false) {alert(resp.reason);return;}// 下棋坐标为-1表示对方掉线if(resp.row ! -1 resp.col ! -1) {// 绘制棋子isWhite (resp.uid room_info.white_id ? true : false);oneStep(resp.col, resp.row, isWhite);// 更新棋盘chessBoard[resp.row][resp.col] 1; }// 更新执棋玩家is_me !is_me;// 更新screen显示的内容set_screen(is_me);// 判断是否有胜利者winner resp.winner;if(winner 0) return;// 更新screen信息var screen_div document.getElementById(screen);if(winner room_info.uid) screen_div.innerHTML resp.reason;else screen_div.innerHTML 游戏失败再接再厉;// 在chess_area区域下方添加返回大厅按钮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);}// 收到chat响应消息else if(resp.optype chat) {if(resp.result false) {alert(resp.reason);document.getElementById(chat_input).value ;return;}// 创建一个子控件将消息内嵌到其中var msg_div document.createElement(p);msg_div.innerHTML resp.message;// 添加属性if(resp.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 ;}}/script
/body
/html六、项目演示
编译 main.cc 得到可执行程序 gobang 并运行
main.cc
#include server.hpp#define HOST 127.0.0.1
#define USER thj
#define PASSWD Abcd1234int main()
{gobang_server server(HOST, USER, PASSWD);server.start(8081);return 0;
}打开浏览器访问 106.52.90.67:8081/register.html 进行新用户注册注册成功后浏览器弹出 “用户注册成功” 提示框点击确定会自动跳转到登录页面。
此时打开 mysql 客户端可以看到 xiaowang 的用户信息记录被成功创建。
输入用户名密码点击登录浏览器弹出 “登录成功” 提示框点击自动跳转游戏大厅页面并且该用户的详细信息成功从数据库获取并展示在游戏大厅页面同时该用户与服务器的通信协议由 HTTP 变为 WebSocket控制台打印 “游戏大厅长连接建立成功” 日志该用户的 session 信息也被创建并且由于建立了 WebSocket 长连接所以 session 被设置为永久有效。 然后点击开始匹配该用户会根据其天梯分数被添加到对应的匹配队列中点击停止匹配该用户会从对应的匹配队列中移除。控制台提示相关信息。
此时我们再用另外一个浏览器注册一个用户登录并开始匹配由于新用户天梯分数默认都是 1000所以两个玩家匹配成功浏览器弹出 “匹配成功” 提示框点击确定自动跳转到游戏房间界面此时原来游戏大厅的长连接会断开游戏房间的长连接会被创建。(使用不同的浏览器防止 cookie 信息冲突) 此时一方的聊天信息以及走棋信息都能被另一方知道。在游戏结束途中如果一方退出另一方直接获胜游戏结束后用户可以点击 “返回大厅” 按钮回到游戏大厅。
回到游戏大厅后大厅界面显示的玩家的比赛信息以及数据库中玩家的比赛信息都会被更新。 七、项目扩展
我们上面实现的网络五子棋其实只是一个最基础的版本或者说是一个重度删减版其实还可以对它进行许多的扩展比如添加如下的一些功能 实现局时与步时功能我们可以设置一个玩家一局游戏能够思考的总时间以及一步棋能够思考的最长时间如果步时到了玩家仍未下棋那么系统可以随机落下一枚棋子。 实现棋谱保存与录像回放功能我们可以在数据库中创建一个对战表用来存储玩家的对战数据即己方与对方下棋的步骤。 这样玩家在对局结束后可以生成对局录像回放 (将数据库中该局对战双方的下棋步骤获取出来然后间隔一定时间依次显示到前端页面中)同时如果玩家游戏中途刷新界面或掉线重连后我们也可以通过数据库中的对战数据让其可以继续对战。 实现观战功能我们可以在游戏大厅中显示当前正在对战的所有游戏房间然后家可以选中某个房间以观众的形式加入到房间中实时的看到选手的对局情况。 实现人机对战的功能当玩家长时间匹配不到对手时我们可以为该玩家分配一个 AI 对手与其进行对战同时在玩家游戏过程中我们也可以提供类似 “托管” 的功能由人机代替玩家来进行对战。 八、项目总结
本项目是一个业务型的项目也是本人的第一个项目在编程方面的难度其实并不是太大主要是学习一个具体业务的整体工作逻辑是怎样的 (从请求到业务处理再到响应)以及前后端是如何配合进行工作的 (HTML/CSS/JS/AJAX)。
在项目编写过程中相较于 C、系统编程、网络编程这些已经学过的东西其实前端以及 WebSocketpp 这方面的知识花费的时间精力会要更多一些因为这些技术都是第一次接触需要一边查阅文档一边使用很多地方出了 bug 也需要花很多时间才能修复。
下面是项目中一些需要特别注意的地方也可以说是我自己踩过的坑
C语言可变参数与宏函数本项目日志宏封装模块中使用了一些C语言的知识包括可变参数、宏函数、预处理符号 ## 以及格式化输出函数 fprintf 等要注意正确使用它们。C11 相关本项目中用到了一些 C11 相关的知识包括函数绑定、智能指针、互斥锁、条件变量等其中要特别注意 bind 如何使用包括如何使用 bind 固定参数、调整参数顺序等。动静态库相关由于本项目中使用了一些第三方库包括 JsonCpp、WebSocketpp、MySQL C API 等所以在 Makefile 中进行编译链接时需要使用 -l、-L、-I 选项来指定动态库名称、动态库路径以及库头文件路径。WebSocketpp 相关由于本项目是使用 WebSocketpp 来进行服务器搭建所以要对其相关的接口及其使用有一定的了解特别是其中的 cancel 函数需要充分了解它的特性才能够正确的使用它。 源码地址
https://gitee.com/tian-hongjin/project-design/tree/master/gobang