当前位置: 首页 > article >正文

【项目设计】网页版五子棋

文章目录

  • 一、项目介绍
    • 1.项目简介
    • 2.开发环境
    • 3.核心技术
    • 4.开发阶段
  • 二、Centos-7.6环境搭建
    • 1.安装wget工具
    • 2.更换软件源(yum源)
    • 3.安装scl工具
    • 4.安装epel软件源
    • 5.安装lrzsz传输工具
    • 6.安装高版本gcc/g++编译器
    • 7.安装gdb调试器
    • 8.安装git
    • 9.安装cmake
    • 10.安装boost库
    • 11.安装Jsoncpp库
    • 12.安装Mysql数据库服务及开发包
      • 12.1获取mysql官方yum源
      • 12.2安装mysql官方yum源
      • 12.3安装Mysql数据库服务
      • 12.4出错解决
      • 12.5安装Mysql开发包
      • 12.6进行Mysql配置修改
      • 12.7启动Mysql服务
      • 12.8获取Mysql临时密码
      • 12.9设置mysql数据库密码
      • 12.10登录查看Mysql字符集是否正常
      • 12.11查看数据库相关文件
    • 13.安装Websocketpp库
  • 三、前置知识了解
    • 1.Websocketpp
      • 1.1Websocket介绍
      • 1.3原理解析
      • 1.4报文格式
      • 1.5Websocketpp介绍
      • 1.6Websocketpp使用
      • 1.7Simple http/websocket服务器
    • 2.JsonCpp使用
      • 1.1Json数据格式
      • 1.2.JsonCpp介绍
    • 3.MYSQL API
    • 4.前端知识介绍
      • 4.1HTML介绍
      • 4.2css介绍
      • 4.3 javascript介绍
      • 4.4 ajax介绍
      • 4.5 WebSocket介绍
  • 四、项目结构设计
    • 1.项目模块划分说明
    • 2.业务处理模块的子模块划分
    • 3.项目流程图
      • 3.1玩家用户角度流程图:
      • 3.2服务器流程结构图:
  • 五、实用工具类模块代码实现
    • 1.日志宏封装
    • 2.Mysql-API封装
    • 3.Jsoncpp-API封装
    • 4.String-Split封装
    • 5.File-read封装
  • 六、数据管理模块实现
    • 1.数据库设计
    • 2.创建user_table类
  • 七、在线用户管理模块实现
  • 八、游戏房间管理模块
    • 1.房间类实现
    • 2.房间管理类实现
  • 九、session管理模块设计
    • 1.什么是session
    • 2.session工作原理
    • 3.session类设计实现
    • 4.session管理设计实现
  • 十、五子棋对战玩家匹配管理设计实现
    • 1.匹配队列实现
    • 2.玩家匹配管理模块设计实现
  • 十一、整合封装服务器模块设计实现
    • 1.通信接口设计(Restful风格)
      • 1.1静态资源请求
      • 1.2注册用户
      • 1.3用户登录
      • 1.4获取客户端信息
      • 1.5websocket长连接协议切换请求(进入游戏大厅)
      • 1.6开始对战匹配
      • 1.7停止匹配
      • 1.8websocket长连接协议切换请求(入游戏房间)
      • 1.9走棋
      • 1.10聊天
    • 2.服务器模块实现
      • 2.1静态资源请求的处理file_handler
      • 2.2用户注册功能请求的后端处理
      • 2.3用户注册功能请求的前端处理
      • 2.4用户登录功能请求的后端处理
      • 2.5用户登录功能请求的前端处理
      • 2.6用户信息获取功能请求的处理
      • 2.7建立游戏大厅的长连接
      • 2.8断开游戏大厅的长连接
      • 2.9游戏大厅信息处理
      • 2.10建立游戏房间的长连接
      • 2.11断开游戏房间的长连接
      • 2.12游戏房间信息处理
      • 2.13完整代码
  • 十二、客户端开发
    • 1.登录页面: login.html
    • 2.注册页面: register.html
    • 3.游戏大厅页面:game_hall.html
    • 4.游戏房间页面:game_room.html
    • 5.common.css
    • 6.game_hall.css
    • 7.game_room.css
    • 8.login.css
  • 十三、项目总结
  • 十四、项目扩展
    • 1.实现局时/步时
    • 2.保存棋谱&录像回放
    • 3.观战功能
    • 4.虚拟对手&人机对战
    • 5.common.css
    • 6.game_hall.css
    • 7.game_room.css
    • 8.login.css
  • 十三、项目总结
  • 十四、项目扩展
    • 1.实现局时/步时
    • 2.保存棋谱&录像回放
    • 3.观战功能
    • 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 数据库的功能。
  • C++11:使用 C++11 中的某些新特性完成代码的编写,例如 bind/shared_ptr/thread/mutex。
  • BlockQueue:为不同段位的玩家设计不同的阻塞式匹配队列来完成游戏的匹配功能。
  • HTML/CSS/JS/AJAX:通过 HTML/CSS/JS 来构建与渲染游戏前端页面,以及通过 AJAX来向服务器发送 HTTP 客户端请求。

WebSocketpp 和 WebSocket 都是用于实现 WebSocket 协议的库或技术。

1.WebSocketpp:WebSocketpp 是一个 C++ 编写的 WebSocket 协议库,它提供了在 C++ 应用程序中实现 WebSocket 客户端和服务器的功能。WebSocketpp 提供了一个简单而强大的 API,使开发者能够轻松地集成 WebSocket 功能到他们的 C++ 应用程序中。它支持多种操作系统和编译器,并且具有良好的性能和稳定性。

2.WebSocket:WebSocket 是一种在客户端和服务器之间进行全双工通信的协议,它允许浏览器和 Web 服务器之间进行实时数据交换。与传统的 HTTP 请求-响应模型不同,WebSocket 在客户端和服务器之间建立持久连接,可以在任何一方发送消息而无需等待对方请求。这使得 WebSocket 适用于实时聊天应用、在线游戏、实时数据更新等场景。

WebSocketpp 是一个用于在 C++ 应用程序中实现 WebSocket 功能的库,而 WebSocket 是一种协议,用于在客户端和服务器之间建立实时通信的连接。你可以使用 WebSocketpp 来开发 WebSocket 客户端和服务器,以便在 C++ 应用程序中实现实时通信功能。

WebSocket 通信的数据格式与 HTTP 是不同的。

1.HTTP:HTTP 是一种基于文本的协议,通常使用 ASCII 编码来传输数据。HTTP 请求和响应由多个部分组成,包括请求/响应行、请求/响应头部和可选的消息体。消息头部包含了关于请求/响应的元数据信息,如请求方法、URI、状态码、内容类型等。消息体包含了实际的数据内容,可以是文本、HTML、JSON、XML 等格式。

2.WebSocket:WebSocket 是一种基于帧的协议,它允许在客户端和服务器之间进行双向实时通信。WebSocket 协议通过建立持久连接来实现实时通信,通信过程中的数据被分割为一个个帧(Frame)进行传输。WebSocket 帧包含了控制帧和数据帧,其中控制帧用于协商连接和传输控制信息,而数据帧用于传输应用数据。

虽然 WebSocket 和 HTTP 都可以在 Web 上进行通信,但它们的通信方式和数据格式是不同的。HTTP 是一种请求-响应模式的协议,每次请求都需要客户端发起,而服务器响应;而 WebSocket 是一种双向通信协议,客户端和服务器可以在任何时候互相发送数据。

4.开发阶段

本项目一共分为四个开发阶段:

  1. 环境搭建:在 Centos7.6 环境下安装本项目会使用到的各种工具以及第三方库。

  2. 前置知识了解:对项目中需要用到的一些知识进行了解,学会它们的基本使用,比如 bind/WebSocketpp/HTML/JS/AJAX 等。

  3. 框架设计:进行项目模块划分,确定每一个模块需要实现的功能。

  4. 模块开发 && 功能测试:对各个子模块进行开发与功能测试,最后再将这些子模块进行整合与整体功能测试。

二、Centos-7.6环境搭建

1.安装wget工具

我们可以使用如下指令查看是否已经按照wget工具,有就不需要再进行安装了

rpm -qa | grep wget

这条命令是在 Linux 系统上执行的。让我解释一下每个部分的含义:

  • rpm: 这是 Red Hat Package Manager 的缩写,用于管理在 Red Hat 系统上安装的软件包。
  • -qa: 这是 rpm 命令的一个选项,意思是查询(Query)所有(All)已安装的软件包(Query All)。它告诉 rpm 列出所有已经安装的软件包。
  • |: 这个符号是管道符号,它将一个命令的输出作为另一个命令的输入。
  • grep: 这是一个用于在文本中搜索特定模式的命令。
  • wget: 这是一个在 Linux 系统上常用的命令行工具,用于从网络上下载文件。

因此,整个命令 rpm -qa | grep wget 的含义是查询所有已安装的软件包,并在结果中筛选出包含“wget”这个关键字的行。这通常用于检查系统中是否已经安装了 wget 这个工具。

wget 是一个在 Unix 和类 Unix 系统上广泛使用的命令行工具,用于从网络上下载文件。它支持 HTTP、HTTPS 和 FTP 协议,可以下载文件并保存到本地计算机上。wget 具有许多功能,包括断点续传、递归下载、后台下载等。由于其简单易用的特性,wget 被广泛用于自动化下载文件、批量下载网页内容等场景中。

要使用 wget 命令下载文件,你需要在终端中输入以下命令:

wget [URL]

如果你需要将文件保存到特定的目录,可以在命令中指定该目录:

wget -P [目录路径] [URL]

安装指令:

sudo yum install -y wget

2.更换软件源(yum源)

查看本地的yum源

ls /etc/yum.repos.d/

我们要对该目录下的CentOS-Base.repo进行备份

sudo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak

然后就可以进行更换

sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
// -o:指定软件源安装的路径
// http:* 官方yum源的地址

清理以前yum源的缓存

sudo yum clean all

重新建立新的缓存

sudo yum makecache

3.安装scl工具

sudo yum install -y centos-release-scl-rh centos-release-scl

在 CentOS 中,“centos-release-scl-rh” 软件包提供了一个软件集合,其中包含了 Software Collections Library(SCL)和 Red Hat 软件集合库(rhscl)。SCL 允许用户在 CentOS 中安装并运行多个软件版本,而不会影响系统中默认安装的软件版本。rhscl 则提供了一些额外的软件包和工具。

查看软件源

ls /etc/yum.repos.d/

此时该目录下就会有如下两个文件

CentOS-SCLo-scl.repo
CentOS-SCLo-scl-rh.repo

4.安装epel软件源

sudo yum install -y epel-release

EPEL 仓库中的软件包包括各种常见的开源软件、工具和库,涵盖了各种用途,比如开发工具、网络服务、数据库、系统管理工具等。通过将 EPEL 仓库添加到系统中,用户可以方便地安装这些额外的软件包,从而扩展系统的功能和应用场景。它提供了一系列额外的软件包,这些软件包通常不包含在 RHEL 或 CentOS 官方仓库中,但是对于企业和生产环境来说却非常有用。

5.安装lrzsz传输工具

sudo yum install -y lrzsz

查看版本信息

rz --version

lrzsz实现服务器和本地进行文件传输:

rz:本地 -> 服务器

sz:服务器 -> 本地

6.安装高版本gcc/g++编译器

第三方工具devtoolset与本地的gcc不冲突

搜索devtoolset有哪些版本

yum search devtoolset

我们可以直接安装全部7.*版本的

sudo yum install -y devtoolset-7-all

这里我们安装如下两个就够了

sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++

我们可以对安装的文件进行查看

ls /opt/
ls /opt/rh
ls /opt/rh/devtoolset-7
ls /opt/rh/devtoolset-7/enable

我们使用如下指令之后发现版本并没有变化

[hdp@VM-12-6-centos ~]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)

此时我们就需要使用如下指令来加载配置。

source /opt/rh/devtoolset-7/enable

想要使用高版本的就要加载配置,但是此时换一个终端就无效,要每次打开都有效,就需要将配置文件放到终端初始化的配置文件中

cd ~  // 进入家目录
ls -a 
// .bashrc是终端打开时会默认加载的配置文件

我们可以直接使用echo重定向到bashrc中

echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc

我们也可以使用vim打开,然后将其复制到文件中

vim .bashrc

然后使用如下指令进行加载

source ~/.bashrc

7.安装gdb调试器

sudo yum install -y gdb

最好gdb与编译器版本相匹配

我们可以安装devtoolset-7-gdb

sudo yum install -y devtoolset-7-gdb

8.安装git

sudo yum install -y git

9.安装cmake

sudo yum install -y cmake

10.安装boost库

websocketpp依赖boost

我们同样可以如下指令搜索boost的版本

yum source boost

这里我们全部进行安装即可

sudo yum install -y boost-devel

我们可以使用如下指令查看boost相关文件

ls /usr/include/boost

11.安装Jsoncpp库

sudo yum install -y jsoncpp-devel

我们可以使用如下指令查看相关文件

ls /usr/include/jsoncpp/json  // 头文件
ls /usr/lib64/libjson*  // 库文件

12.安装Mysql数据库服务及开发包

安装mysql之前,我们要先移除已有的数据库服务

查看相关的数据库文件

rpm -qa | grep mysql

移除相关服务

sudo yum remove -y mysql*
// 如果一次性全部移除不行,那么就一个一个的进行移除
rpm -qa | grep mariadb  //有也要进行删除

删除相关配置文件

sudo find /var/ -name 'mysql*'
sodu rm -rf /var/log/mysqld.log
sudo rm -rf /var/lib/yum/repos/x86_64/7/mysql*
sudo rm -rf /var/cache/yum/x86_64/7/mysql*
sudo rm -rf /var/tmp/yum-zwc-AgRIT9/x86_64/7/mysql*
sudo rm -rf mysql157-community-release-e17-10.noarch.rpm

删除
ls /etc/my.cnf
ls /etc/my.cnf.d

12.1获取mysql官方yum源

wget http://repo.mysql.com/mysql57-community-release-el7-10.noarch.rpm

12.2安装mysql官方yum源

sudo rpm -ivh mysql57-community-release-el7-10.noarch.rpm
// i:install
// v:显示详细信息
// h:安装时显示进度

RPM 文件实际上是一种二进制软件包,其中包含了预编译的软件以及与之相关的元数据。这些元数据描述了软件包的名称、版本、依赖关系等信息。RPM 文件可以通过 RPM 软件包管理器来安装、升级、卸载和查询。

在安装 RPM 文件时,系统会将其中的文件解压并放置到系统的相应目录中,并根据元数据信息进行配置。通常情况下,RPM 文件具有 .rpm 扩展名。

12.3安装Mysql数据库服务

sudo yum install -y mysql-community-server
// 查看相关文件
ls /etc/yum.pos.d  -> mysql* 2个相关文件

12.4出错解决

如果因为GPG KEY的过期导致安装失败

GPG Keys are configured as: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql

则执行以下指令,然后重新安装

sudo rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022

12.5安装Mysql开发包

sudo yum install -y mysql-community-devel

12.6进行Mysql配置修改

配置 ‘/etc/my.cnf’ 字符集

sudo vim /etc/my.cnf
// 插入一下内容
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
character-set-server=utf8

完成之后重新启动数据库

sudo systemctl restart mysql

12.7启动Mysql服务

sudo systemctl start mysqld

注意,如果启动的时候遇到了以下情况,输入系统的root管理员密码即可

[hdp@VM-8-12-centos ~]$ systemctl start mysqld
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to manage system services or units.
Authenticating as: 
Password: 

12.8获取Mysql临时密码

临时密码保证在一下文件中

ls /var/log/mysqld.log

获取临时密码

sudo grep 'temporary password' /var/log/mysqld.log

12.9设置mysql数据库密码

登录数据库

mysql -uroot -p

登录数据库之后进行如下操作:

set global validate_password_policy=0;
set global validate_password_length=1;
ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码';
FLUSH PRIVILEGES;

12.10登录查看Mysql字符集是否正常

show variables like '%chara%';

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)

12.11查看数据库相关文件

ls /usr/include/
// 头文件
ls /usr/include/mysql/
// 动静态库
ls /usr/lib64/mysql

13.安装Websocketpp库

从github中把websocketpp的源码克隆下来

git clone https://github.com/zaphoyd/websocketpp.git

由于 github 服务器在国外,所以可能会出现 clone 失败的情况,此时可以从 gitee 仓库克隆 WebSocketpp 库:

git clone https://gitee.com/freeasm/websocketpp.git

安装websocketpp,clone 成功后执行如下指令来安装 WebSocketpp 库 (执行 git clone 语句的目录下):

cd websocketpp/
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr ..   //安装在usr中,头文件在usr/include 库文件在usr/lib64中
sudo make install

验证websocketpp是否安装成功

[hdp@VM-12-6-centos build]$ cd ../examples/echo_server
[hdp@VM-12-6-centos echo_server]$ ls
CMakeLists.txt echo_handler.hpp echo_server.cpp SConscript
[hdp@VM-12-6-centos echo_server]$ g++ -std=c++11 echo_server.cpp -o echo_server -lpthread -lboost_system
[hdp@VM-12-6-centos echo_server]$

g++ 编译 echo_server.cpp,如果编译成功则说明安装成功。

这里没有生成库,而是.hpp,使用时不需要连接库,而是直接使用即可。

三、前置知识了解

1.Websocketpp

1.1Websocket介绍

WebSocket 是从 HTML5 开始支持的⼀种网页端和服务端保持长连接的消息推送机制。

传统的 web 程序都是属于 “⼀问⼀答” 的形式,即客户端给服务器发送了⼀个 HTTP 请求,服务器给客户端返回⼀个 HTTP 响应。这种情况下服务器是属于被动的⼀方,如果客户端不主动发起请求服务器就无法主动给客户端响应

像网页实时聊天或者我们做的五子棋游戏这样的程序都是非常依赖 “消息推送” 的, 即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送⼀般需要通过 “轮询” 的方式实现, 而轮询的成本比较高并且也不能及时的获取到消息的响应。

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

对于实时消息推送,使用轮询的确会增加服务器负担并且不能实时地获取消息响应。为了解决这个问题,可以使用一些现代的技术和协议来实现更高效的消息推送,其中包括:

1.WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它允许服务器主动向客户端推送消息,而不需要客户端发起请求。使用 WebSocket 可以实现实时的双向通信,使得服务器可以立即将消息推送给客户端,而不需要轮询。

2.Server-Sent Events (SSE):服务器发送事件是一种在客户端和服务器之间单向推送消息的技术,通过持久的 HTTP 连接,服务器可以发送事件给客户端。与 WebSocket 不同,SSE 是单向通信,只能由服务器向客户端推送消息,但它简单易用,并且兼容性良好。

3.WebRTC:WebRTC 是一种用于实现浏览器之间点对点通信的技术,它可以用于实时音视频通信,但也可以用于数据通信。虽然 WebRTC 更适用于音视频通信,但它也可以用于实现实时数据通信,例如实时消息推送。

这些技术都可以在现代的 Web 应用程序中实现实时消息推送,提供更好的用户体验和更高效的通信。选择哪种技术取决于你的应用场景和需求。

轮询是一种通过定期发送请求来获取更新的技术,虽然它不如 WebSocket 或 SSE 高效,但仍然是一种常见的实现方式。以下是一个简单的轮询实现步骤:

1.客户端发送请求:客户端定期发送 HTTP 请求给服务器,以获取最新的数据或消息更新。

2.服务器处理请求:服务器接收到客户端的请求后,检查是否有新的数据或消息需要推送。

3.返回响应:如果有新的数据或消息,服务器将其包含在 HTTP 响应中返回给客户端;如果没有,服务器可能返回一个空响应或者之前的数据。

4.客户端处理响应:客户端收到服务器的响应后,解析其中的数据或消息,并根据需要更新界面或执行其他操作。

5.重复以上步骤:客户端在一定的时间间隔内重复发送请求,以获取最新的数据或消息更新。

1.3原理解析

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

QQ截图20231122193043

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

QQ截图20231122193210

建立TCP连接之后,浏览器发送一下的请求:

GET /ws HTTP/1.1
Connection:Upgrade
Upgrade:Websocket
Sec-WebSocket-Version:xxx
Sec-WebSocket-key:xxx   // base64码:sec-websocket-key使用某个公开的算法生成  如果服务器支持,则之后走websocket握手流程

服务器回复如下响应:

HTTP/1.1 101 XXX  // 101指协议切换
Connection:Upgrade
Upgrade:WebSocket
Sec-WebSocket-Accpet:xxx

浏览器使用公开的算法将base64码生成对应的字符串,如果和服务器传回来的字符串一致,则连接建立成功。

同一个连接,之前通信采用HTTP协议的格式,之后使用WebSocket协议的格式

1.4报文格式

QQ截图20231122193256

报文字段比较多,我们重点关注这几个字段:

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: 报文携带的载荷数据

注: B站的这个视频对Websocket协议的讲述非常清晰, 大家下去可以去看看。

Websocket

1.5Websocketpp介绍

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

WebSocketpp的主要特性包括:

1.事件驱动的接口

2.支持HTTP/HTTPS、WS/WSS、IPv6

3.灵活的依赖管理 — Boost库/C++11标准库

4.可移植性:Posix/Windows、32/64bit、Intel/ARM

5.线程安全

WebSocketpp同时支持HTTP和Websocket两种网络协议, 比较适用于我们本次的项目, 所以我们选用该库作为项目的依赖库用来搭建HTTP和WebSocket服务器。

下面是该项目的⼀些常用网站, 大家可以去学习

1.github: https://github.com/zaphoyd/websocketpp

2.用户手册:https://docs.websocketpp.org/

3.官网:http://www.zaphoyd.com/websocketpp

1.6Websocketpp使用

websocketpp常用接口介绍:

namespace websocketpp
{
    typedef lib::weak_ptr<void> connection_hdl;

    template <typename config>
    class endpoint : public config::socket_type
    {
        typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;
        typedef typename connection_type::ptr connection_ptr;
        typedef typename connection_type::message_ptr message_ptr;

        typedef lib::function<void(connection_hdl)> open_handler;
        typedef lib::function<void(connection_hdl)> close_handler;
        typedef lib::function<void(connection_hdl)> http_handler;
        typedef lib::function<void(connection_hdl, message_ptr)> message_handler;
        
        /* websocketpp::log::alevel::none 禁止打印所有⽇志*/
        void set_access_channels(log::level channels);   /*设置⽇志打印等级*/
        void clear_access_channels(log::level channels); /*清除指定等级的⽇志*/
        
        /*设置指定事件的回调函数*/
        void set_open_handler(open_handler h);       /*websocket握手成功回调处理函数*/
        void set_close_handler(close_handler h);     /*websocket连接关闭回调处理函数*/
        void set_message_handler(message_handler h); /*websocket消息回调处理函数*/
        void set_http_handler(http_handler h);       /*http请求回调处理函数*/
        
        /*发送数据接口*/
        void send(connection_hdl hdl, std::string &payload, frame::opcode::value op);
        void send(connection_hdl hdl, void *payload, size_t len, frame::opcode::value op);
        
        /*关闭连接接口*/
        void close(connection_hdl hdl, close::status::value code, std::string &reason);
        /*获取connection_hdl 对应连接的connection_ptr*/
        connection_ptr get_con_from_hdl(connection_hdl hdl);
        /*websocketpp基于asio框架实现,init_asio用于初始化asio框架中的io_service调度器*/
        void init_asio();
        /*设置是否启用地址重用*/
        void set_reuse_addr(bool value);
        /*设置endpoint的绑定监听端口*/
        void listen(uint16_t port);
        /*对io_service对象的run接口封装,用于启动服务器*/
        std::size_t run();
        /*websocketpp提供的定时器,以毫秒为单位*/
        timer_ptr set_timer(long duration, timer_handler callback);
    };

    template <typename config>
    class server : public endpoint<connection<config>, config>
    {
        /*初始化并启动服务端监听连接的accept事件处理*/
        void start_accept();
    };

    template <typename config>
    class connection
        : public config::transport_type::transport_con_type,
          public config::connection_base
    {
        /*发送数据接口*/
        error_code send(std::string &payload, frame::opcode::value, op = frame::opcode::text);
        /*获取http请求头部*/
        std::string const &get_request_header(std::string const &key);
        /*获取请求正文*/
        std::string const &get_request_body();
        /*设置响应状态码*/
        void set_status(http::status_code::value code);
        /*设置http响应正文*/
        void set_body(std::string const &value);
        /*添加http响应头部字段*/
        void append_header(std::string const &key, std::string const &val);
        /*获取http请求对象*/
        request_type const &get_request();
        /*获取connection_ptr 对应的 connection_hdl */
        connection_hdl get_handle();
    };

    namespace http
    {
        namespace parser
        {
            class parser
            {
                std::string const &get_header(std::string const &key);
            };

            class request : public parser
            {
                /*获取请求方法*/
                std::string const &get_method();
                /*获取请求uri接口*/
                std::string const &get_uri();
            };
        }
    }

    namespace message_buffer
    {
        /*获取websocket请求中的payload数据类型*/
        frame::opcode::value get_opcode();
        /*获取websocket中payload数据*/
        std::string const &get_payload();
    };

    namespace 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_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,
            };
        }
    }
}

日志相关接口函数:set_access_channels

回调函数相关接口:针对不同事件设置不同的回调函数

websocketpp搭建了服务器之后,给不同的事件设置了不同的处理函数指针,这些指针,可以指向指定的函数,当服务器收到指定的数据,触发了指定的事件就会通过函数指针去调用这些函数,这时候,我们程序员就可以编写一些业务处理函数,将其设置为对应事件的业务处理函数

set_open_handler:设置websocket协议握手成功的回调函数

set_close_handler:设置websocket连接断开的回调函数

set_message_handler:设置websocket消息处理函数

set_http_handler:设置http请求的处理函数

通信连接相关接口:

send:给客户端发送消息

close:关闭连接

get_con_from_hdl:通过connection_hdl获取对应的connection_ptr

其他服务器搭建的接口:

init_asio() 初始化asio框架

set_reuse_addr 是否启动地址重用

start_accept 开始获取新建连接

listen 设置绑定监听接口

run 启动服务器

set_timer 设置定时任务

1.7Simple http/websocket服务器

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

使用websocket搭建服务器的流程:

1.实例化server对象

2.设置日志输出等级

3.初始化asio框架中的调度器

4.设置业务处理回调函数(具体业务处理的函数由我们自己实现)

5.设置服务器监听端口

6.开始获取新建连接

7.启动服务器

C++11中bind的使用:

作用:用于实现对函数进行参数绑定的功能

void print(char* str,int num)
{
    std::cout<<str<<num<<std::endl;
}
print("nihao",10);//调用函数的时候需要我们传入参数
auto func = std::bind(print,"nihao",std::placeholders:_1);//对print函数进行参数绑定并生成了一个新的可调用对象
func(10);//函数调用等价于print("nihao",10);

简单服务器实现:

#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>

typedef websocketpp::server<websocketpp::config::asio> wsserver_t;

static const uint16_t defaultport = 8080;

// 给客户端返回一个hello world页面
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;

    std::string body = "<html><body><h1>Hello World</h1></body></html>";
    conn->set_body(body);
    conn->append_header("Content-Type", "text/html");
    conn->set_status(websocketpp::http::status_code::ok);
}
void open_callback(wsserver_t *srv, websocketpp::connection_hdl hdl)
{
    std::cout << "websocket 握手成功" << std::endl;
}
void close_callback(wsserver_t *srv, websocketpp::connection_hdl hdl)
{
    std::cout << "websocket 连接断开成功" << std::endl;
}
void message_callback(wsserver_t *srv, websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg)
{
    wsserver_t::connection_ptr conn = srv->get_con_from_hdl(hdl);
    std::cout << "wsmsg: " << msg->get_payload() << std::endl;
    std::string rep = "client say: " + msg->get_payload();
    conn->send(rep, websocketpp::frame::opcode::text);
}

int main()
{
    // 1.实例化serevr对象
    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(open_callback, &wssrv, std::placeholders::_1));
    wssrv.set_close_handler(std::bind(close_callback, &wssrv, std::placeholders::_1));
    wssrv.set_message_handler(std::bind(message_callback, &wssrv, std::placeholders::_1, std::placeholders::_2));

    // 5.设置监听端口
    wssrv.listen(defaultport);
    // 6.开始获取连接
    wssrv.start_accept();
    // 7.启动服务器
    wssrv.run();
    return 0;
}

http_callback 函数中,通过设置响应体、Content-Type 和状态码,WebSocketpp 库会自动构建 HTTP 响应,并将其发送回客户端。

具体来说,以下几个函数用于设置 HTTP 响应的相关字段:

  • conn->set_body(body):设置响应体的内容。
  • conn->append_header("Content-Type", "text/html"):添加一个名为 “Content-Type” 的响应头字段,值为 “text/html”,指定返回的内容类型为 HTML。
  • conn->set_status(websocketpp::http::status_code::ok):设置响应的状态码为 200(表示成功)。

一旦这些字段被设置,WebSocketpp 库会根据这些信息自动构建响应,并将其发送回客户端。

在 WebSocketpp 库中,websocketpp::connection_hdl 是一个连接处理器(Connection Handler)的类型。它代表着一个特定的 WebSocket 连接,在处理多个连接时可以使用该类型来标识和管理不同的连接。通过 connection_hdl,可以对连接进行操作,比如发送消息、关闭连接等操作。

2.JsonCpp使用

1.1Json数据格式

Json 是⼀种数据交换格式,它采用完全独立于编程语言的文本格式来存储和表示数据。

例如: 我们想表示⼀个同学的学生信息

C 代码表示

struct student
{
    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]
    }
]

Json 的数据类型包括对象,数组,字符串,数字等。

对象:使用花括号 {} 括起来的表示⼀个对象

数组:使用中括号 [] 括起来的表示⼀个数组

字符串:使用常规双引号 “” 括起来的表示⼀个字符串

数字:包括整形和浮点型,直接使用

1.2.JsonCpp介绍

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();
    val["name"].asString();
    const char *asCString() const; // 转char* char *name =val["name"].asCString();
    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功能代码用例编写

序列化步骤:

1.将需要进行序列化的数据,存储在Json::Value对象中

2.实例化一个StreamWriterBuilder工厂类对象

3.通过StreamWriterBuilder工厂类对象生产一个StreamWrite对象

4.使用StreamWrite对象,对Json::Value中存储的数据进行序列化

反序列化步骤:

1.实例化一个CharReadBuilder工厂类对象

2.使用CharReaderBuilder工厂生产一个CharReader对象

3.定义一个Json::Value对象

4.使用CharRead对象进行json格式字符串str的反序列化

5.逐个元素去访问Json::Value中的数据

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

// 使用jsoncpp库进行多个数据对象的序列化
std::string serialize()
{
    // 1.将需要进行序列化的数据,存储在Json::Value对象中
    Json::Value root;
    root["姓名"] = "小明";
    root["年龄"] = 18;
    root["成绩"].append(92);
    root["成绩"].append(90);
    root["成绩"].append(83);

    // 2.实例化一个StreamWriterBuilder工厂类对象
    Json::StreamWriterBuilder swb;
    // 3.通过StreamWriterBuilder工厂类对象生产一个StreamWrite对象
    Json::StreamWriter *sw = swb.newStreamWriter();
    // 4.使用StreamWrite对象,对Json::Value中存储的数据进行序列化

    std::stringstream ss;
    int ret = sw->write(root, &ss);
    if (ret != 0)
    {
        std::cout << "json serialize failed!" << std::endl;
        return "";
    }
    std::cout << ss.str() << std::endl;
    delete sw;
    return ss.str();
}

void deserialize(const std::string &str)
{
    // 1.实例化一个CharReadBuilder工厂类对象
    Json::CharReaderBuilder crb;
    // 2.使用CharReaderBuilder工厂生产一个CharRead对象
    Json::CharReader *cr = crb.newCharReader();
    // 3.定义一个Json::Value对象
    Json::Value root;
    // 4.使用CharRead对象进行json格式字符串str的反序列化
    std::string err;
    bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);
    if (ret == false)
    {
        std::cout << "json deserialize failed" << std::endl;
        return;
    }
    // 5.逐个元素去访问Json::Value中的数据
    std::cout << "姓名: " << root["姓名"].asString() << std::endl;
    std::cout << "年龄: " << root["年龄"].asInt() << std::endl;
    int sz = root["成绩"].size();
    for (int i = 0; i < sz; i++)
    {
        std::cout << "成绩: " << root["成绩"][i].asFloat() << std::endl;
    }
    delete cr;
}
int main()
{
    std::string str = serialize();
    deserialize(str);
    return 0;
}

3.MYSQL API

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

相关接口介绍如下:

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

// 连接mysql服务器
// 参数说明:
// mysql--初始化完成的句柄
// host---连接的mysql服务器的地址
// username---连接的服务器的用户名
// password-连接的服务器的密码
// dbname ----默认选择的数据库名称
// port---连接的服务器的端口: 默认是3306端口
// unix_socket---通信管道文件或者socket⽂件,通常置NULL
// client_flag---客户端标志位,通常置0
// 返回值:成功返回句柄指针,失败返回NULL
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *username,
                          const char *password, const char *dbname, unsigned int port,
                          const char *unix_socket, unsigned long client_flag);
// 设置当前客户端的字符集
// 参数说明:
// mysql--初始化完成的句柄
// csname--字符集名称,通常:"utf8"
// 返回值:成功返回0, 失败返回非0
int mysql_set_character_set(MYSQL *mysql, const char *csname);

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

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

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

// 将结果保存到本地之后,之后所有的操作都是针对result进行操作

// 获取结果集中的行数
// 参数说明:
// result--保存到本地的结果集地址
// 返回值:结果集中数据的条数
uint64_t mysql_num_rows(MYSQL_RES *result);

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

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

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

// 关闭数据库客户端连接,销毁句柄
// 参数说明:
// mysql--初始化完成的句柄
void mysql_close(MYSQL *mysql);

// 获取mysql接口执行错误原因
// 参数说明:
// mysql--初始化完成的句柄
const char *mysql_error(MYSQL *mysql);

MYSQL步骤如下:

1.初始化mysql句柄 mysql_init()

2.连接服务器 mysql_real_connect()

3.设置客户端字符集 mysql_set_charactor_set()

4.选择要操作的数据库 mysql_select_db()

5.执行sql语句 mysql_query()

6.如果sql语句是查询语句,则需要保存结果到本地 mysql_store_result()

7.获取结果集中的结果条数 mysql_num_rows()

8.遍历保存到本地的结果集 mysql_num_fields() mysql_fetch_row()

9.释放结果集 mysql_free_result()

10.关闭连接,释放句柄 mysql_close()

简单案例如下:

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

#define HOST "127.0.0.1"
#define USER "root"
#define PASSWORD "123456"
#define PORT 3306
#define DBNAME "gobang"

int main()
{
    // 1.初始化mysql句柄
    MYSQL *mysql = mysql_init(nullptr);
    if (mysql == nullptr)
    {
        std::cerr << "mysql init failed" << std::endl;
        return -1;
    }
    // 2.连接服务器
    if (mysql_real_connect(mysql, HOST, USER, PASSWORD, DBNAME, PORT, nullptr, 0) == nullptr)
    {
        std::cerr << "mysql connect failed" << std::endl;
        return -1;
    }
    // 3.设置客户端字符集
    if (mysql_set_character_set(mysql, "utf8") != 0)
    {
        std::cerr << "mysql set character failed" << std::endl;
        return -1;
    }
    // 4.选择要操作的数据库
    if (mysql_select_db(mysql, DBNAME) != 0)
    {
        std::cerr << "mysql select db failed" << std::endl;
        return -1;
    }
    // 5.执行sql语句
    // std::string sql = "insert stu values (1,'小明',18,82.5,98.5,86);";
    // std::string sql = "update stu set name='小红' where id=1;";
    // std::string sql = "delete from stu where id=1;";
    std::string sql = "select * from stu;";
    if (mysql_query(mysql, sql.c_str()) != 0)
    {
        std::cerr << "mysql query failed" << std::endl;
        std::cout << "sql: " << sql << std::endl;
        mysql_close(mysql);
        return -1;
    }
    // 6.如果sql语句是查询语句,则需要保存结果到本地
    MYSQL_RES *res = mysql_store_result(mysql);
    if (res == nullptr)
    {
        mysql_close(mysql);
        return -1;
    }
    // 7.获取结果集中的结果条数
    uint64_t num_row = mysql_num_rows(res);
    int num_col = mysql_num_fields(res);
    // 8.遍历保存到本地的结果集
    for (int i = 0; i < num_row; i++)
    {
        MYSQL_ROW rows = mysql_fetch_row(res);
        for (int j = 0; j < num_col; j++)
        {
            printf("%s\t", rows[j]);
        }
        std::cout << std::endl;
    }
    // 9.释放结果集
    mysql_free_result(res);
    // 10.关闭连接,释放句柄
    mysql_close(mysql);
    return 0;
}

4.前端知识介绍

HTML:标签语言,渲染前端网页的语言

CSS:层叠样式表,对HTML标签进行样式渲染,让页面控件更好看

JS:脚本语言,在前端web这里进行控制页面的渲染

AJAX:异步的http客户端,向服务器发送http请求

4.1HTML介绍

HTML代码是由标签构成的。

形如:

<body>hello</body>

标签名 (body) 放到 < > 中

大部分标签成对出现. 为开始标签, 为结束标签.

少数标签只有开始标签, 称为 “单标签”.

开始标签和结束标签之间, 写的是标签的内容. (hello)

开始标签中可能会带有 “属性”. id 属性相当于给这个标签设置了一个唯一的标识符(身份证号码).

<body id="myId">hello</body>

HTML文件基本结构

<html>
    <head>
<title>第一个页面</title>
    </head>
    <body>
		hello world
    </body>
</html>

html 标签是整个 html 文件的根标签(最顶层标签)

head 标签中写页面的属性.

body 标签中写的是页面上显示的内容

title 标签中写的是页面的标题

标签层次结构

父子关系

兄弟关系

head 和 body 是 html 的子标签(html 就是 head 和 body 的父标签)

title 是 head 的子标签. head 是 title 的父标签.

head 和 body 之间是兄弟关系.

标签之间的结构关系, 构成了一个 DOM

QQ截图20231211224835

HTML 常见标签

标题标签: h1-h6

有六个, 从 h1 - h6. 数字越大, 则字体越小.

<h1>hello</h1>
<h2>hello</h2>
<h3>hello</h3>
<h4>hello</h4>
<h5>hello</h5>
<h6>hello</h6>
QQ截图20231211225002

段落标签: p

把一段比较长的文本粘贴到 html 中, 会发现并没有分成段落. 在html中使用

标签括起一个段落进行换行。当然也可以在段落内使用 <br/>标签进行换行操作。

文本格式化标签

加粗: strong 标签 和 b 标签

倾斜: em 标签 和 i 标签

删除线: del 标签 和 s 标签

下划线: ins 标签 和 u 标签

<p><b>比如b标签就是加粗</b></p>
<p><i>比如i标签就是斜体</i></p>
<p><s>比如s标签就是删除线</s></p>
<p><u>比如u就是下划线</u></p>
QQ截图20231211225228

图片标签: img

img 标签必须带有 src 属性. 表示图片的路径.

alt: 替换文本. 当文本不能正确显示的时候, 会显示一个替换的文字.

title: 提示文本. 鼠标放到图片上, 就会有提示.

width/height: 控制宽度高度. 高度和宽度一般改一个就行, 另外一个会等比例缩放. 否则就会图片失衡.

border: 边框, 参数是宽度的像素. 但是一般使用 CSS 来设定

超链接标签: a

href: 必须具备, 表示点击后会跳转到哪个页面.

target: 打开方式. 默认是 _self. 如果是 _blank 则用新的标签页打开

<a href="http://www.baidu.com" target="_blank">点击这里打开新标签访问百度</a>

注意:这里百度文本就是一个连接点击项,而这个点击项不一定非是文本,可以是图片也可以是其他的一些信息。

表格标签

table 标签: 表示整个表格

tr: 表示表格的一行

td: 表示一个单元格

th: 表示表头单元格. 会居中加粗

thead: 表格的头部区域(注意和 th 区分, 范围是比 th 要大的)

tbody: 表格得到主体区域

列表标签

主要使用来布局的. 整齐好看.

无序列表[重要] ul li , .

有序列表[用的不多] ol li

自定义列表[重要] dl (总标签) dt (小标题) dd (围绕标题来说明) 上面有个小标题, 下面有几个围绕着标题来展开的.

<ul> 
 <li>ul/li是无序列表</li> 
 <li>ul/li是无序列表</li> 
</ul> 
<ol> 
 <li>ol/li是有序列表</li> 
 <li>ol/li是有序列表</li> 
</ol> 
<dl> 
 <dt>dl/dt是小标题</dt> 
 <dd>dl/dd是围绕标题的描述</dd> 
 <dd>dl/dd是围绕标题的描述</dd> 
</dl>
QQ截图20231211225446

表单标签

表单是让用户输入信息的重要途径.

分成两个部分:

表单域: 包含表单元素的区域. 重点是 form 标签.

表单控件: 输入框 , 提交按钮等. 重点是 input 标签.

input标签的认识

<input type="text" placeholder="input标签默认是文本框"> <br/> 
<input type="password" placeholder="type属性为password是密码框"> <br/> 
<input type="radio" name="sex">type属性为radio是单选框,name属性相同则默认为同一组-男 <br/> 
<input type="radio" name="sex" checked="checked">type属性为radio是单选框-女<br/> 
<input type="checkbox"> checkbox是复选框-吃饭 <br/> 
<input type="checkbox"> checkbox是复选框-睡觉 <br/> 
<input type="checkbox"> checkbox是复选框-打游戏<br/> 
<input type="checkbox" id="testid"> 
<label for="testid">label标签for属性与对应的输入框id对应起来,这时候点击文字也能选中</label><br/> 
<input type="button" value="button是普通按钮" onclick="alert('alert是提示框调用函数')"><br/> 
<input type="submit" value="submit是提交按钮">点击这里就会向服务器提交表单域中的表单数据<br/> 
<input type="file" value="file是文件选择按钮框"><br/> 
<input type="reset" value="reset是清空按钮,会清空表单域的所有数据"><br>
QQ截图20231211225545

下拉菜单标签

下拉菜单

option 中定义 selected=“selected” 表示默认选中

<select> 
 <option selected="selected">--请选择年份--</option> 
 <option>1990</option> 
 <option>1991</option> 
 <option>1992</option> 
</select>
QQ截图20231211225646

文本域标签

<textarea name="文本域标签" id="" cols="30" rows="10" placeholder="textarea是文本域标签"> 
</textarea>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

无语义标签: div & span

div 标签, division 的缩写, 含义是 分割

span 标签, 含义是跨度

就是两个盒子. 用于网页布局

div 是独占一行的, 是一个大盒子.

span 不独占一行, 是一个小盒子.

<div>div是个大盒子独占一行</div> 
<span>span是个小盒子并不独占一行</span> 
<span>span是个小盒子并不独占一行</span>
QQ截图20231211225836

html5语义化标签

div 没有语义. 对于搜索引擎来说没有意义. 为了让搜索引擎能够更好的识别和分析页面(SEO 优化), HTML 引入了更多的 “语义化” 标签. 但是这些标签其实本质上都和 div 一样(对于前端开发来说). 然而对于搜索引擎来说, 见到 header和 article 这种标签就会重点进行解析, 而 footer 这种标签就可以适当忽略.

header: 头部

nav: 导航

article: 内容

section: 某个区域

aside: 侧边栏

footer: 尾部

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学习页面</title>
</head>

<body>
    <h1>Hello World</h1>
    <h2>Hello World</h2>
    <p>段落,<br />
        在HTML中,一般的回车并不起作用,会被解释为一个空格</p>
    但是br不一样,br标签的作用就是换行<br />
    <img src='C:\Users\Administrator\Desktop\R-C.jpg' width="300px">
    <br />
    <a href='https://www.baidu.com'>
        这是一个链接,可以跳转到百度
    </a>
    <br>
    <ul>
        <li>ul/li是无序列表</li>
        <li>ul/li是无需列表</li>
    </ul>
    <ol>
        <li>ol/li是有序列表</li>
        <li>ol/li是有序列表</li>
    </ol>
    <dl>
        <dt>dl/dt是小标题</dt>
        <dd>dl/dd是围绕标题的描述</dd>
        <dd>dl/dd是围绕标题的描述</dd>
    </dl>
    <form action='' http://101.33.252.57/login' method='post'>
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" name='submit' value="提交">
    </form>
    <button>普通的按钮</button>
</body>

</html>
QQ截图20231212103453

4.2css介绍

层叠样式表 (Cascading Style Sheets).

CSS 能够对网页中元素位置的排版进行像素级精确控制, 实现美化页面的效果. 能够做到页面的样式和结构分离.

基本语法规范

选择器 + {一条/N条声明}

选择器决定针对谁修改 (找谁)

声明决定修改啥. (干啥)

声明的属性是键值对. 使用 ; 区分键值对, 使用 : 区分键和值

<style> 
 p { 
 /* 设置字体颜色 */ 
 color: red; 
 /* 设置字体大小 */ 
 font-size: 30px; 
 } 
</style> 
<p>hello</p>

CSS 要写到 style 标签中(后面还会介绍其他写法)

style 标签可以放到页面任意位置. 一般放到 head 标签内.

CSS 使用 /* */ 作为注释. (VSCode 使用 ctrl + / 快速切换) .

选择器

选择器的功能

选中页面中指定的标签元素.要先选中元素, 才能设置元素的属性.

选择器的种类

1. 基础选择器: 单个选择器构成的

标签选择器

类选择器

id 选择器

通配符选择器

2. 复合选择器: 把多种基础选择器综合运用起来.

后代选择器

子选择器

并集选择器

伪类选择器

/*通配选择器-对所有的标签产生效果*/ 
* { 
    margin: 0px; 
    padding: 3px; 
}

/*p这种与html标签同名的称之为标签选择器,同类标签都具有这个属性*/ 
p { 
    color: red; 
    font-size: 30px; 
} 

<p>这是一个标题</p>
/*类选择器,类名以.开头, 多个类属性可以在一个标签内使用*/ 
.green { 
    color: green; 
} 
.big { 
    font-size: 40px; 
} 
<p class="green big">这是一个类选择器修饰的属性</p>

/*id选择器,名称以#开头,修饰指定id的标签容器,只能被一个标签使用,因为id唯一*/ 
#font { 
	font-size: 20px; 
	font-family:sans-serif; 
	color: aqua; 
} 
<p id="font">这个是id选择器弄的样式</p>

代码样例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学习页面</title>
    <style>
        * {
            margin: 0px;
            padding: 5px;
        }

        p {
            color: red;
            font-size: 20px;
        }

        .cgreen {
            color: green;
        }

        #h2_title {
            color: blue;
            font-size: 40px;
        }
    </style>
</head>

<body>
    <h1 class='cgreen'>Hello World</h1>
    <h2 id='h2_title'>Hello World</h2>
    <p>段落,<br />
        在HTML中,一般的回车并不起作用,会被解释为一个空格</p>
    但是br不一样,br标签的作用就是换行<br />
    <img src='C:\Users\Administrator\Desktop\R-C.jpg' width="300px">
    <br />
    <a href='https://www.baidu.com'>
        这是一个链接,可以跳转到百度
    </a>
    <br>
    <ul>
        <li>ul/li是无序列表</li>
        <li>ul/li是无需列表</li>
    </ul>
    <ol>
        <li>ol/li是有序列表</li>
        <li>ol/li是有序列表</li>
    </ol>
    <dl>
        <dt>dl/dt是小标题</dt>
        <dd>dl/dd是围绕标题的描述</dd>
        <dd>dl/dd是围绕标题的描述</dd>
    </dl>
    <form action='' http://101.33.252.57/login' method='post'>
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" name='submit' value="提交">
    </form>
    <button>普通的按钮</button>
</body>

</html>
QQ截图20231212110936

4.3 javascript介绍

将上面的代码中,给按钮增加点击事件,点击之后,提示h2_title中的内容,然后将内容改为"你好,世界"。

然后获取输入框中的username,提示输入框中的信息,然后将输入框中的数据清空

1.使用js给按钮添加点击事件

2.使用js获取以及设置一个页面控件(普通的控件以及输入框)的内容

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学习页面</title>
    <style>
        * {
            margin: 0px;
            padding: 3px;
        }

        p {
            color: red;
            font-size: 20px;
        }

        .cgreen {
            color: green;
        }

        #h2_title {
            color: blue;
            font-size: 40px;
        }
    </style>
</head>

<body>
    <h1 class='cgreen'>Hello World</h1>
    <h2 id='h2_title'>Hello World</h2>
    <p>段落,<br />
        在HTML中,一般的回车并不起作用,会被解释为一个空格</p>
    但是br不一样,br标签的作用就是换行<br />
    <img src='C:\Users\Administrator\Desktop\R-C.jpg' width="300px">
    <br />
    <a href='https://www.baidu.com'>
        这是一个链接,可以跳转到百度
    </a>
    <br>
    <ul>
        <li>ul/li是无序列表</li>
        <li>ul/li是无需列表</li>
    </ul>
    <ol>
        <li>ol/li是有序列表</li>
        <li>ol/li是有序列表</li>
    </ol>
    <dl>
        <dt>dl/dt是小标题</dt>
        <dd>dl/dd是围绕标题的描述</dd>
        <dd>dl/dd是围绕标题的描述</dd>
    </dl>
    <form action='' http://101.33.252.57/login' method='post'>
        <input type="text" id='username' name="username">
        <input type="password" name="password">
        <input type="submit" name='submit' value="提交">
    </form>
    <button onclick="test()">普通的按钮</button>
</body>
<script>
    function test() {
        // var h2 = document.getElementById("h2_title");
        // alert(h2.innerHTML);
        // h2.innerHTML = "你好,世界";
        var input = document.getElementById("username");
        alert(input.value);
        input.value = "";
    }
</script>

</html>

4.4 ajax介绍

我们要使用Ajax,需要先使用它的库,这里我们不使用原生的ajax,直接使用jQuery的ajax,jQuery是一个js框架。就相当于C/C++的库/头文件一样,我们可以直接在浏览器中搜索jquery ajax使用菜鸟教程,ws3scool,或者官方文档中的即可。

在浏览器搜索jQuery ajax即可 https://www.w3schools.cn/jquery/jquery_get_started.html

QQ截图20231212224526

将上面的代码添加到我们的代码中即可,但是那个链接可能有的时候无法使用,我们重新搜索复制到我们的代码中即可

使用案例:获取页面中输入框中的内容,作为一个登录请求发送给服务器,将服务器的响应进行打印

ajax默认页面来源于哪里,请求就会发送给谁,我们这里页面和服务器不在同一个地方,所以会报错,但是我们这里只需要观察看服务器收到http请求即可。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学习页面</title>
    <style>
        * {
            margin: 0px;
            padding: 3px;
        }

        p {
            color: red;
            font-size: 20px;
        }

        .cgreen {
            color: green;
        }

        #h2_title {
            color: blue;
            font-size: 40px;
        }
    </style>
</head>

<body>
    <h1 class='cgreen'>Hello World</h1>
    <h2 id='h2_title'>Hello World</h2>
    <p>段落,<br />
        在HTML中,一般的回车并不起作用,会被解释为一个空格</p>
    但是br不一样,br标签的作用就是换行<br />
    <img src='C:\Users\Administrator\Desktop\R-C.jpg' width="300px">
    <br />
    <a href='https://www.baidu.com'>
        这是一个链接,可以跳转到百度
    </a>
    <br>
    <ul>
        <li>ul/li是无序列表</li>
        <li>ul/li是无需列表</li>
    </ul>
    <ol>
        <li>ol/li是有序列表</li>
        <li>ol/li是有序列表</li>
    </ol>
    <dl>
        <dt>dl/dt是小标题</dt>
        <dd>dl/dd是围绕标题的描述</dd>
        <dd>dl/dd是围绕标题的描述</dd>
    </dl>
    <form action='' http://101.33.252.57/login' method='post'>
        <input type='text' id='username' name='username'>
        <input type='password' id='password' name='password'>
        <input type='submit' name='submit' value="提交">
    </form>
    <button onclick="test()">普通的按钮</button>
</body>
<script src="https://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js">
</script>

<script>
    function test() {
        var login_info = {
            username: document.getElementById("username").value,
            password: document.getElementById("password").value
        }
        $.ajax({
            type: "post",
            url: "http://101.33.252.57:8080/login",
            data: JSON.stringify(login_info),
            success: function (res, statu, xhr) {
                // res:响应的
                alert(res);
            },
            error: function (xhr) {
                // xhr:http request对象,包含了响应信息
                alert(JSON.stringify(xhr));
            }
        });
    }
</script>

</html>

4.5 WebSocket介绍

WebSocket:创建一个websocket请求,请求服务器建立长连接,进行持久通信。

案例:建立websocket长连接之后,将输入框中的数据发送给服务器,h2_title中的内容改为服务器响应的内容

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>学习页面</title>
    <style>
        * {
            margin: 0px;
            padding: 3px;
        }

        p {
            color: red;
            font-size: 20px;
        }

        .cgreen {
            color: green;
        }

        #h2_title {
            color: blue;
            font-size: 40px;
        }
    </style>
</head>

<body>
    <h1 class='cgreen'>Hello World</h1>
    <h2 id='h2_title'>Hello World</h2>
    <p>段落,<br />
        在HTML中,一般的回车并不起作用,会被解释为一个空格</p>
    但是br不一样,br标签的作用就是换行<br />
    <img src='C:\Users\Administrator\Desktop\R-C.jpg' width="300px">
    <br />
    <a href='https://www.baidu.com'>
        这是一个链接,可以跳转到百度
    </a>
    <br>
    <ul>
        <li>ul/li是无序列表</li>
        <li>ul/li是无需列表</li>
    </ul>
    <ol>
        <li>ol/li是有序列表</li>
        <li>ol/li是有序列表</li>
    </ol>
    <dl>
        <dt>dl/dt是小标题</dt>
        <dd>dl/dd是围绕标题的描述</dd>
        <dd>dl/dd是围绕标题的描述</dd>
    </dl>
    <form action='' http://101.33.252.57/login' method='post'>
        <input type='text' id='username' name='username'>
        <input type='password' id='password' name='password'>
        <input type='submit' name='submit' value="提交">
    </form>
    <button onclick="test()">普通的按钮</button>
</body>

<script>
    var ws = new WebSocket("ws://101.33.252.57:8080/ws");
    ws.onopen = function () {
        alert("ws 握手成功");
    }
    ws.onclose = function () {
        alert("ws 连接断开");
    }
    ws.onerror = function () {
        alert("ws 通信错误");
    }
    ws.onmessage = function (evt) {
        // alert(evt.data);
        var h2 = document.getElementById("h2_title");
        h2.innerHTML = evt.data;
    }
    function test() {
        ws.send(document.getElementById("username").value);
        document.getElementById("username").value = "";
        // var login_info = {
        //     username: document.getElementById("username").value,
        //     password: document.getElementById("password").value
        // }
        // $.ajax({
        //     type: "post",
        //     url: "http://101.33.252.57:8080/login",
        //     data: JSON.stringify(login_info),
        //     success: function (res, statu, xhr) {
        //         // res:响应的
        //         alert(res);
        //     },
        //     error: function (xhr) {
        //         // xhr:http request对象,包含了响应信息
        //         alert(JSON.stringify(xhr));
        //     }
        // });
    }
</script>

</html>

四、项目结构设计

1.项目模块划分说明

项目的实现,咱们将其划分为三个大模块来进行:

• 数据管理模块:基于Mysql数据库进行用户数据的管理

• 前端界面模块:基于JS实现前端页面(注册,登录,游戏大厅,游戏房间)的动态控制以及与服务器的通信。

• 业务处理模块:搭建WebSocket服务器与客户端进行通信,接收请求并进行业务处理。

在这里回顾⼀下我们要实现的项目功能,我们要实现的是⼀个在线五子棋对战服务器,提供用户通过浏览进行用户注册,登录,以及实时匹配,对战,聊天等功能。而如果要实现这些功能,那么就需要对业务处理模块再次进行细分为多个模块来实现各个功能。

2.业务处理模块的子模块划分

• 网络通信模块:基于websocketpp库实现HTTP&&WebSocket服务器的搭建,提供网络通信功能。

• 会话管理模块:对客户端的连接进行cookie&session管理,实现HTTP短连接时客户端身份识别功能。

• 在线管理模块:对进入游戏大厅与游戏房间中用户进行管理,提供用户是否在线以及获取用户连接的功能。

• 房间管理模块:为匹配成功的用户创建对战房间,提供实时的五子棋对战与聊天业务功能。

• 用户匹配模块:根据天梯分数不同进行不同层次的玩家匹配,为匹配成功的玩家创建房间并加入房间。

QQ截图20231213092017

3.项目流程图

3.1玩家用户角度流程图:

玩家角度流程图.drawio

3.2服务器流程结构图:

服务器角度流程思想及模块结构图.drawio

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,通常用于实现实时的、双向的通信。当页面进行跳转时,如果是同一域名下的不同页面之间的跳转,WebSocket 连接通常不会中断,因为 WebSocket 连接的生命周期与页面的生命周期无关,它们都是在浏览器的后台进行管理的。

但是,如果跳转到了不同域名的页面,或者是在跳转后进行了页面刷新,浏览器会丢弃旧的页面上的 WebSocket 连接,因此需要重新建立连接。这是因为浏览器安全策略的限制,跨域请求会导致连接断开。

所以,一般情况下,如果页面跳转在同一域名下,并且不刷新页面,WebSocket 连接可以保持有效;如果页面跳转到了不同域名,或者进行了页面刷新,需要重新建立 WebSocket 连接。

五、实用工具类模块代码实现

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

实现工具类模块:

1.日志宏程序日志打印

2.mysql_util:数据库的连接 && 初始化,句柄的销毁,语句的执行

3.json_util:封装实现json的序列化和反序列化

4.string_util:主要是封装字符串分割的功能

5.file_util:主要是封装了文件数据的读取功能(对于html文件进行读取)

1.日志宏封装

#ifndef _M_LOG_H
#define _M_LOG_H
#include <stdio.h>
#include <time.h>

#define INF 0
#define BEBUG 1
#define ERROR 2
#define DEFAULT_LOG_LEVEL INF

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

#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(BEBUG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERROR, format, ##__VA_ARGS__)

#endif

2.Mysql-API封装

主要框架:

class mysql_util
{
public:
    MYSQL* mysql_create(const std::string &host, const std::string &username,
                        const std::string &password, const std::string dbname,
                        const uint16_t &port = 3306);
    bool mysql_exec(MYSQL* mysql,const std::string& sql);
    void mysql_destroy(MYSQL* mysql);
};

具体实现:

class mysql_util
{
public:
    // mysql服务器创建
    static MYSQL *mysql_create(const std::string &host, const std::string &username,
                               const std::string &password, const std::string dbname,
                               const uint16_t &port = 3306)
    {
        // 1.初始化句柄
        MYSQL *mysql = mysql_init(nullptr);
        if (mysql == nullptr)
        {
            ELOG("mysql init failed");
            return nullptr;
        }

        // 2. 连接服务器
        MYSQL *ret = mysql_real_connect(mysql, host.c_str(), username.c_str(),
                                	password.c_str(), dbname.c_str(), port, nullptr, 0);
        if (ret == nullptr)
        {
            ELOG("mysql_real_connect failed: %s", mysql_errno(mysql));
            mysql_close(mysql);
            return nullptr;
        }

        // 3.设置客户端字符集
        if (mysql_set_character_set(mysql, "utf8") != 0)
        {
            ELOG("mysql set character failed: %s", mysql_errno(mysql));
            mysql_close(mysql);
            return nullptr;
        }

        return mysql;
    }

    // sql语句执行
    static bool mysql_exec(MYSQL *mysql, const std::string &sql)
    {
        int ret = mysql_query(mysql, sql.c_str());
        if (ret != 0)
        {
            ELOG("%s", sql.c_str());
            ELOG("mysql query failed : %s", mysql_error(mysql));
            return false;
        }
        return true;
    }

    // 关闭连接,销毁句柄
    static bool mysql_destroy(MYSQL *mysql)
    {
        if (mysql)
        {
            mysql_close(mysql);
        }
        return true;
    }
};

3.Jsoncpp-API封装

json_util:

1.json序列化:将多个数据对象,进行序列化得到一个json格式的字符串

2.json反序列化:将json格式的字符串,反序列化得到多个数据对象

主要框架:

class json_util
{
public:
    static bool serialize(const Json::Value& root,std::string& str);
    static bool deserialize(const std::string& str,Json::Value& root);
};

具体实现:

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 deserialize(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 deserizlize failed : %s\n", err.c_str());
            return false;
        }
        return true;
    }
};

4.String-Split封装

主要框架:

class string_util
{
public:
    static int split(const std::string& src,const std::string& 						                           sep,std::vector<std::string>& res);
};

具体实现:

class string_util
{
public:
    // 123,456,,,789
    static int split(const std::string &src, const std::string &sep, std::vector<std::string> &res)
    {
        size_t n = src.size(), pos = 0, index = 0;
        while (index < n)
        {
            pos = src.find(sep, index);
            // 没有找到,字符串中没有间隔字符了,则跳出循环
            if (pos == std::string::npos)
            {
                res.push_back(src.substr(index));
                break;
            }
            // 连续的分割符,则直接跳过
            if (pos == index)
            {
                index = pos + sep.size();
                continue;
            }
            res.push_back(src.substr(index, pos - index));
            index = pos + sep.size();
        }
        return res.size();
    }
};

5.File-read封装

主要框架:

class file_util
{
public:
    static bool read(const std::string& filename,std::string& body);
};

具体实现:

class file_util
{
public:
    static bool read(const std::string &filename, std::string &body)
    {
        // 打开文件
        std::ifstream ifs(filename, std::ios::binary);
        if (!ifs.is_open())
        {
            ELOG("%s file open failed", filename);
            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())
        {
            ELOG("read %s file content failed", filename.c_str());
            ifs.close();
            return false;
        }

        // 关闭文件
        ifs.close();
        return true;
    }
};

六、数据管理模块实现

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

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 
);

2.创建user_table类

数据库中有可能存在很多张表,每张表中管理的数据又有不同,要进行的数据操作也各不相同,因此我们可以为每⼀张表中的数据操作都设计⼀个类,通过类实例化的对象来访问这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。

创建user_table类, 该类的作用是负责通过 MySQL 接口管理用户数据。主要提供了四个方法:

• select_by_name: 根据用户名查找用户信息, 用于实现登录功能

• insert: 新增用户,用户实现注册功能

• login: 登录验证,并获取完整的用户信息

• win: 用于给获胜玩家修改分数

• lose: 用户给失败玩家修改分数

这里我们实现一个我们自己的mysql客户端来访问服务器进行数据的操作

针对我们管理的每一张表都设计一个类,通过类实例化的对象管理指定的数据库表

主要框架:

class user_table
{
private:
    MYSQL* _mysql; // mysql操作句柄
    std::mutex _mutex;// 互斥锁保护数据库的访问操作
public:
    user(){}
    ~user(){}
    bool insert(Json::Value& user); // 注册时新增用户
    bool login(Json::Value& user); // 登录验证,并返回详细的用户信息
    bool select_by_name(const std::string& name,Json::Value& user); // 用过用户名获取用户信息
    bool select_by_id(const uint64_t &id,Json::Value& user); // 通过用户id获取用户信息
    bool win(const uint64_t &id); // 胜利时天梯分数增加,战斗场次增加,胜利场次增加
    bool lose(const uint64_t &id); // 失败是天梯分数减少,战斗场次增加,其他不变
};

具体实现:

#ifndef __M_DB_H__
#define __M_DB_H__
#include "util.hpp"
#include <mutex>
#include <cassert>
using namespace Util;

class user_table
{
public:
    user_table(const std::string &host, const std::string &username,
               const std::string &password, const std::string dbname,
               const uint16_t &port = 3306)
    {
        _mysql = mysql_util::mysql_create(host, username, password, dbname, port);
    }
    ~user_table()
    {
        if (_mysql)
        {
            mysql_util::mysql_destory(_mysql);
        }
    }

public:
    // 注册时新增用户
    bool insert(Json::Value &user)
    {
#define INSERT_USER "insert user values(null,'%s',password('%s'),1000,0,0);"
        if (user["username"].isNull() || user["password"].isNull())
        {
            DLOG("please input username or password");
            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)
        {
            ELOG("insert user information failed");
            return false;
        }

        return true;
    }

    // 登录验证,并返回详细的用户信息
    bool login(Json::Value &user)
    {
        if (user["username"].isNull() || user["password"].isNull())
        {
            DLOG("please input username or password");
            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 = nullptr;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if (ret == false)
            {
                DLOG("user login failed");
                return false;
            }
            
            // 按理说要么有数据,要么没有数据,就算有数据也只能有一条数据
            res = mysql_store_result(_mysql);
            if (res == nullptr)
            {
                DLOG("have no login user information");
                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_username(std::string &username, 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, username);

        MYSQL_RES *res = nullptr;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if (ret == false)
            {
                DLOG("get user by name failed");
                return false;
            }
            
            // 按理说要么有数据,要么没有数据,就算有数据也只能有一条数据
            res = mysql_store_result(_mysql);
            if (res == nullptr)
            {
                DLOG("have no login user information");
                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_id(const uint64_t &id, Json::Value &user)
    {
#define USER_BY_ID "select username,score,total_count,win_count from user where id=%d;"
        char sql[4096] = {0};
        sprintf(sql, USER_BY_ID, id);

        MYSQL_RES *res = nullptr;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if (ret == false)
            {
                DLOG("get user by name failed");
                return false;
            }
            
            // 按理说要么有数据,要么没有数据,就算有数据也只能有一条数据
            res = mysql_store_result(_mysql);
            if (res == nullptr)
            {
                DLOG("have no login user information");
                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)id;
        user["username"] = 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(const 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 information failed");
            return false;
        }
        return true;
    }

    // 失败时天梯分数减少30,战斗场次增加1,其他不变
    bool lose(const 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 information failed");
            return false;
        }
        return true;
    }

private:
    MYSQL *_mysql;
    std::mutex _mutex;
};

#endif

七、在线用户管理模块实现

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

1.能够让程序中根据用户信息,进而找到能够与用户客户端进行通信的Socket连接,进而实现与客户端的通信。

2.判断⼀个用户是否在线,或者判断用户是否已经掉线

在线用户管理:

管理的是两类用户:进入游戏大厅 && 进入游戏房间

原因:进入游戏大厅的用户和进入游戏房间的用户才会建立websocket长连接

管理:将用户的id和对应的客户端websocket长连接进行关联起来

作用:

当一个用户发了消息(实时聊天/下棋信息),我们可以找到房间中的其他用户,在在线用户管理模块中,找到这个用户对应的websocket连接,然后将消息发送给指定的用户

1.通过用户id找到用户连接,进而实现指定用户的客户端推送消息,websocket连接关闭的时候,会自动在在线用户管理模块中删除自己的信息

2.可以通过判断一个用户是否还在用户管理模块中确定用户是否在线。

主要框架:

class onlie_manager
{
private:
    std::mutex _mutex;
    // 用于建立游戏大厅用户的id与通信连接的关系
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user;
    // 用于建立游戏房间用户的id与通信连接的关系
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user;

public:
    // websocket连接建立的时候才会加入游戏大厅 && 游戏房间在线用户管理
    void enter_game_hall(uint64_t uid, wsserver_t::connection_ptr &conn);
    void enter_game_room(uint64_t uid, wsserver_t::connection_ptr &conn);
    // websocket连接断开的时候,才会移除游戏大厅 && 游戏房间在线用户管理
    void exit_game_hall(uint64_t uid);
    void exit_game_room(uint64_t uid);
     // 判断当前指定用户是否在游戏大厅/游戏房间
    bool is_in_game_hall(uint64_t uid)();
    bool is_in_game_room(uint64_t uid)();
    // 通过用户id在游戏大厅/游戏房间用户管理中获取对应的通信连接
    wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid);
    wsserver_t::connection_ptr get_conn_from_room(uint64_t uid);
};

具体实现:

#ifndef __M_ONLINE_H__
#define __M_ONLINE_H__
#include "util.hpp"
#include <mutex>
#include <unordered_map>

class onlie_manager
{
private:
    std::mutex _mutex;
    // 用于建立游戏大厅用户的id与通信连接的关系
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user;
    // 用于建立游戏房间用户的id与通信连接的关系
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user;

public:
    // websocket连接建立的时候才会加入游戏大厅 && 游戏房间在线用户管理
    void enter_game_hall(uint64_t uid, wsserver_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, wsserver_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);
        _hall_user.erase(uid);
    }

    // 判断当前指定用户是否在游戏大厅/游戏房间
    bool is_in_game_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _hall_user.find(uid);
        return it != _hall_user.end();
    }

    bool is_in_game_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _room_user.find(uid);
        return it != _room_user.end();
    }

    // 通过用户id在游戏大厅/游戏房间用户管理中获取对应的通信连接
    wsserver_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 wsserver_t::connection_ptr();
        }
        return it->second;
    }

    wsserver_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 wsserver_t::connection_ptr();
        }
        return it->second;
    }
};

#endif

八、游戏房间管理模块

游戏房间管理模块:

1.对匹配成功的玩家创建房间,建立起一个小范围的玩家之间的关联关系,房间里的玩家产生的动作会广播给房间里的其他用户

2.因为房间有可能很多,因此需要将这些房间管理起来以便于对于房间生命周期的控制

实现两个部分:

1.房间的设计

2.房间管理的设计

游戏房间的设计:

管理的数据:

1.房间的id

2.房间的状态(决定了一个玩家退出房间时所作的动作)

3.房间中玩家的数量(决定了房间什么时候销毁)

4.白棋玩家id

5.黑棋玩家id

6.用户信息表的句柄(当玩家胜利/失败时更新用户数据)

7.棋盘信息(二维数组)

8.在线用户的管理句柄

房间中产生的动作:

1.下棋

2.聊天

不管是哪个动作,只要是合理的,都要广播给房间里的其他用户

游戏房间:

1.管理房间中的数据

2.处理房间中产生的动作

Restful风格的网络接口设计:

Restful风格:依托与HTTP协议来实现

GET:获取 POST:新增 PUT:更新 DELETE:删除

正文采用xml/json格式进行正文的格式组织

1.房间类实现

首先,需要设计⼀个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建立⼀个小范围的关联关系,⼀个房间中任意⼀个用户发生的任何动作,都会被广播给房间中的其他用户。而房间中的动作主要包含两类:

1.棋局对战

2.实时聊天

框架实现:

#define BOARD_ROW 15
#define BOARD_COL 15
#define WHITE_CHESS 1
#define BLACK_CHESS 2

typedef enum
{
    GAME_START,
    GAME_OVER
} room_statu;

class room
{
private:
    uint64_t _room_id;   // 房间id
    room_statu _statu;   // 房间状态
    int _player_count;   // 房间中的人数
    uint64_t _white_id;  // 白棋玩家的id
    uint64_t _black_id;  // 黑棋玩家id
    user_table *_tb_user;// 数据管理句柄
    onlie_manager *_online_user; // 在线用户管理句柄
    std::vector<std::vector<int>> _board; //棋盘

public:
    room(uint64_t room_id, user_table *tb_user, onlie_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 get_room_id() { return _room_id; }
    room_statu statu() { return _statu; }
    int player_count() { return _player_count; }
    
    void add_white_user(uint64_t uid)
    {
        _white_id = uid;
        _player_count++;
    }
    
    void add_black_user(uint64_t uid)
    {
        _black_id = uid;
        _player_count++;
    }
    
    uint64_t get_white_id() { return _white_id; }
    uint64_t get_black_id() { return _black_id; }

    // 处理下棋动作
    Json::Value handle_chess(Json::Value &req);
    // 处理聊天动作
    Json::Value handle_chat(Json::Value &req);
    // 处理玩家退出房间动作
    void handle_exit(uint64_t uid);
    // 总的请求处理函数,在函数内部,区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播
    void handle_request(Json::Value &req);
    // 将指定的信息广播给房间中所有玩家
    void broadcast(Json::Value &rsp);
};

具体实现:

#ifndef __M_ROOM_H__
#define __M_ROOM_H__

#include "util.hpp"
#include "log.hpp"
#include "db.hpp"
#include "online.hpp"

#define BOARD_ROW 15
#define BOARD_COL 15
#define WHITE_CHESS 1
#define BLACK_CHESS 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;
    onlie_manager *_online_user;
    std::vector<std::vector<int>> _board;

public:
    room(uint64_t room_id, user_table *tb_user, onlie_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 get_room_id() { return _room_id; }
    room_statu statu() { return _statu; }
    int player_count() { return _player_count; }
    void add_white_user(uint64_t uid)
    {
        _white_id = uid;
        _player_count++;
    }

    void add_black_user(uint64_t uid)
    {
        _black_id = uid;
        _player_count++;
    }

    uint64_t get_white_id() { return _white_id; }
    uint64_t get_black_id() { return _black_id; }

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

        // 一共大于5就获胜
        return (count >= 5);
    }

    // 判断是否获胜
    uint64_t check_win(int row, int col, int color)
    {
        // 从下棋位置的四个不同方向上检测是否出现了5个及以上相同颜色的棋子(横行,纵列,正斜,反斜)
        if (five(row, col, 0, 1, color) ||
            five(row, col, 1, 0, color) ||
            five(row, col, -1, 1, color) ||
            five(row, col, 1, 1, color))
        {
            return color = WHITE_CHESS ? _white_id : _black_id;
        }
        return 0;
    }

public:
    // 处理下棋动作
    /*{
         "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是谁,谁就赢了)
    }*/
    Json::Value handle_chess(Json::Value &req)
    {
        Json::Value json_resp = req;
        // 判断房间中的两个玩家是否都在线,任意一个不在线,就是另一方胜利
        // 获取下棋的位置
        int chess_row = req["row"].asInt();
        int chess_col = req["col"].asInt();

        // 获取当前下棋用户的id
        uint64_t cur_uid = req["uid"].asUInt64();

        // 如果是白棋玩家不在线,则黑棋玩家获胜
        if (_online_user->is_in_game_room(_white_id) == false)
        {
            json_resp["result"] = true;
            json_resp["reason"] = "运气真好!对方掉线,不战而胜";
            json_resp["winner"] = (Json::UInt64)_black_id;
            return json_resp;
        }

        // 如果是黑棋玩家不在线,则黑棋玩家获胜
        if (_online_user->is_in_game_room(_black_id) == false)
        {
            json_resp["result"] = true;
            json_resp["reason"] = "运气真好!对方掉线,不战而胜";
            json_resp["winner"] = (Json::UInt64)_white_id;
            return json_resp;
        }

        // 获取走棋的位置,判断当前走棋是否合理(位置是否已经被占用)
        if (_board[chess_row][chess_col] != 0)
        {
            json_resp["result"] = false;
            json_resp["reason"] = "当前位置已经有棋子了";
            return json_resp;
        }

        // 判断当前下棋棋子的颜色
        int cur_color = cur_uid == _white_id ? WHITE_CHESS : BLACK_CHESS;
        _board[chess_row][chess_col] = cur_color;

        // 判断是否有玩家胜利(从当前位置开始判断是否存在五星连珠)
        uint64_t winner_id = check_win(chess_row, chess_col, cur_color);

        if (winner_id != 0)
        {
            json_resp["reason"] = "五星连珠,战无敌";
        }
        json_resp["result"] = true;
        json_resp["winner"] = (Json::UInt64)winner_id;
        return json_resp;
    }

    // 处理聊天动作
    /*{
         "optype": "chat",
         "room_id": 222,
         "uid": 1,
         "message": "赶紧点"
    }
    {
         "optype": "chat",
         "result": false
         "reason": "聊天失败具体原因....⽐如有敏感词..."
    }
    {
        "optype": "chat",
        "result": true,
        "room_id": 222,
        "uid": 1,
        "message": "赶紧点"
    }*/
    Json::Value handle_chat(Json::Value &req)
    {
        Json::Value json_resp = req;
        // 检测是否包含敏感词
        std::string message = req["message"].asString();
        ssize_t pos = message.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 lose_id = winner_id == _white_id ? _black_id : _white_id;

            // 将胜者和败者分别增加分数和减少分数
            _tb_user->win(winner_id);
            _tb_user->lose(lose_id);

            // 将房间的状态改为游戏结束
            _statu = GAME_OVER;

            // 广播消息
            broadcast(json_resp);
        }

        // 房间中玩家数量--
        _player_count--;
        return;
    }

    // 总的请求处理函数,在函数内部,区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播
    void handle_request(Json::Value &req)
    {
        // 校验房间是否匹配
        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 broadcast(json_resp);
        }

        // 根据不同的请求类型调用不同的处理函数
        if (req["optype"].asString() == "put_chess")
        {
            json_resp = handle_chess(req);
            if (json_resp["winner"].asUInt64() != 0)
            {
                // 对胜利者和失败者分别加分和减分
                uint64_t winner_id = json_resp["winner"].asUInt64();
                uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
                _tb_user->win(winner_id);
                _tb_user->lose(loser_id);

                // 将房间的状态改为游戏结束
                _statu = GAME_OVER;
            }
        }
        else if (req["optype"].asString() == "chat")
        {
            json_resp = handle_chat(req);
        }
        else
        {
            json_resp["optype"] = req["optype"].asString();
            json_resp["result"] = false;
            json_resp["reason"] = "未知请求类型";
        }

        std::string body;
        json_util::serialize(json_resp, body);
        DLOG("房间-广播动作: %s", body.c_str());
        
        return broadcast(json_resp);
    }

    // 将指定的信息广播给房间中所有玩家
    void broadcast(Json::Value &resp)
    {
        // 对要响应的信息进行序列化,将Json::Value中的数据序列化为json格式的字符串
        std::string body;
        json_util::serialize(resp, body);

        // 获取房间中所有用户的通信连接
        // 发送响应信息
        wsserver_t::connection_ptr wconn = _online_user->get_conn_from_room(_white_id);
        if (wconn.get() != nullptr)
        {
            wconn->send(body);
        }
        else
        {
            DLOG("房间-白棋玩家连接获取失败");
        }
        wsserver_t::connection_ptr bconn = _online_user->get_conn_from_room(_black_id);
        if (bconn.get() != nullptr)
        {
            bconn->send(body);
        }
        else
        {
            DLOG("房间-黑棋玩家连接获取失败");
        }
        return;
    }
};

#endif

统计下棋位置的右边同色棋子数量:行不变,列++

统计下棋位置的左边同色棋子数量:行不变,列–

统计上方同色棋子数量:列不变,行–

统计下方同色棋子数量:列不变,行++

正斜:

右上:行–,列++

左下:行++,列–

反斜:

左上:行–,列–

右下:行++,列++

2.房间管理类实现

房间管理:

1.创建房间(两个玩家对战匹配完成了,为他们创建一个房间,需要传入两个玩家的用户ID)

2.查找房间(通过房间ID查找房间信息,通过用户ID查找所在房间的信息)

3.销毁房间(根据房间ID销毁房间,房间中所有的用户都退出了,销毁房间)

需要管理的数据:

1.数据管理模块的句柄

2.在线用户管理的句柄

3.房间ID分配计数器

4.互斥锁

5.using room_ptr = std::shared_ptr<room>;房间信息的空间使用智能指针shared_ptr进行管理

unordered_map<room_id,room_ptr>;房间信息管理(建立起房间ID与房间信息的映射关系)

6.unordered_map<uid,rid>;房间ID与用户ID的关联关系管理(先通过用户ID找到所在房间ID,再去查找房间信息)

主要框架:

using room_ptr = std::shared_ptr<room>;

class room_manager
{
private:
    uint64_t _next_rid;
    std::mutex _mutex;
    user_table *_tb_user;
    onlie_manager *_online_user;
    std::unordered_map<uint64_t, room_ptr> _rooms;
    std::unordered_map<uint64_t, uint64_t> _users;

public:
    // 初始化房间计数器
    room_manager(user_table *ut, onlie_manager *om) {}
    ~room_manager() {}
    // 为两个用户创建房间,并返回房间的指针指针管理对象
    room_ptr ctrate_room(uint64_t uid1, uint64_t uid2) {}
    // 通过房间ID获取房间信息
    room_ptr get_room_by_rid(uint64_t rid) {}
    // 通过用户ID获取房间信息
    room_ptr get_room_by_uid(uint64_t uid) {}
    // 通过房间ID销毁房间
    void remove_room(uint64_t rid) {}
    // 删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
    void remove_room_user(uint64_t uid) {}
};

具体实现:

using room_ptr = std::shared_ptr<room>;

class room_manager
{
private:
    uint64_t _next_rid;
    std::mutex _mutex;
    user_table *_tb_user;
    onlie_manager *_online_user;
    std::unordered_map<uint64_t, room_ptr> _rooms;
    std::unordered_map<uint64_t, uint64_t> _users;

public:
    // 初始化房间计数器
    room_manager(user_table *ut, onlie_manager *om)
        : _next_rid(1), _tb_user(ut), _online_user(om)
    {
        DLOG("房间创建完毕");
    }

    ~room_manager()
    {
        DLOG("房间即将销毁");
    }

    // 为两个用户创建房间,并返回房间的指针指针管理对象
    room_ptr ctrate_room(uint64_t uid1, uint64_t uid2)
    {
        // 两个用户在游戏大厅中进行匹配对战,匹配成功后创建房间
        // 1.校验两个用户是否都在游戏大厅,只有都在游戏大厅在需要创建房间
        if (_online_user->is_in_game_hall(uid1) == false)
        {
            DLOG("%lu 用户不在大厅中,房间创建失败");
            return room_ptr();
        }
        if (_online_user->is_in_game_hall(uid2) == false)
        {
            DLOG("%lu 用户不在大厅中,房间创建失败");
            return room_ptr();
        }

        // 只有两个用户都在大厅中才创建房间
        // 2. 创建房间,将用户信息添加到房间中

        std::unique_lock<std::mutex> lock(_mutex);
        room_ptr rp(new room(_next_rid, _tb_user, _online_user));

        // 3.添加用户信息
        rp->add_white_user(uid1);
        rp->add_black_user(uid2);

        // 4. 将房间信息管理起来
        _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++;

        // 5.返回房间信息
        return rp;
    }

    // 通过房间ID获取房间信息
    room_ptr get_room_by_rid(uint64_t room_id)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _rooms.find(room_id);
        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获取房间ID
        auto uit = _users.find(uid);
        if (uit == _users.end())
        {
            return room_ptr();
        }

        // 2.通过房间ID获取房间信息
        uint64_t room_id = uit->second;
        auto rit = _rooms.find(room_id);
        if (rit == _rooms.end())
        {
            return room_ptr();
        }

        return rit->second;
    }
    // 通过房间ID销毁房间
    void remove_room(uint64_t room_id)
    {
        // 因为房间信息是通过shared_ptr在_rooms中进行管理,因此只要将shared_ptr从_rooms中移除即可
        // 则shared_ptr计数器==0,外界没有对房间信息进行操作保存的情况下就会释放

        // 1.通过房间ID获取房间信息
        room_ptr rp = get_room_by_rid(room_id);
        if (rp.get() == nullptr)
        {
            return;
        }

        // 2.获取房间中的用户信息
        uint64_t uid1 = rp->get_black_id();
        uint64_t uid2 = rp->get_white_id();

        std::unique_lock<std::mutex> lock(_mutex);
        // 3.删除房间ID与用户ID的关联关系
        _users.erase(uid1);
        _users.erase(uid2);

        // 4.删除房间ID与房间信息的关联关系
        _rooms.erase(room_id);
    }

    // 删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
    void remove_room_user(uint64_t uid)
    {
        // 1.获取房间信息
        room_ptr rp = get_room_by_uid(uid);
        if (rp.get() == nullptr)
        {
            return;
        }

        // 2.用户退出
        rp->handle_exit(uid);

        // 3.判断此时房间中的人数,如果房间中没有用户了,则销毁房间
        if (rp->player_count() == 0)
        {
            remove_room(rp->get_room_id());
        }

        return;
    }
};

九、session管理模块设计

1.什么是session

在WEB开发中,HTTP协议是⼀种无状态短链接的协议,这就导致⼀个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。

因此,服务器为每个用户浏览器创建⼀个会话对象(session对象),注意:⼀个浏览器独占⼀个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务

2.session工作原理

我们使用用户名和密码进行登录之后,服务器收到之后,会在服务器内部会将用户信息进行记录—session 每一个session都有一个session id,服务器会在http响应中会包含Set-Cookie:ssid=XXX;将session id以cookie的形式返回给浏览器,浏览器就会在cookie保存区进行保存,以后进行访问的时候都会带有cookie信息,那么服务器就会根据cookie信息进行识别用户是否登录。

QQ截图20231203151839

使用cookie和session进行http在短连接通信的情况下进行用户状态管理,但是这个服务器上管理的session都会有过期时间,超过时间了,就会将对应的session删除,每次客户端与服务端的通信都需要延长session的过期时间

3.session类设计实现

session 模块:保存客户端的用户状态信息

1.有自己的标识:ssid

2.用户的状态信息

3.用户id

4.session相关的定时器(通过是否包含有定时器,确定当前session是否已经添加了定时销毁任务)

websocketpp库中定时器的使用:其中的定时器基于boost::asio::steady_timer;

这里我们简单的设计⼀个session类,但是session对象不能⼀直存在,这样是⼀种资源泄漏,因此需要使用定时器对每个创建的session对象进行定时销毁(⼀个客户端连接断开后,⼀段时间内都没有重新连接则销毁session)。

_ssid使用时间戳填充。实际上, 我们通常使用唯⼀id生成器生成⼀个唯⼀的id

_user保存当前用户的信息

timer_ptr _tp

主要框架:

// 定义用户状态:登录 未登录
typedef enum
{
    UNLOHIN,
    LOGIN
} ss_statu;

class session
{
private:
    uint64_t _ssid;            // 标识符
    uint64_t _uid;             // session对应的用户id
    ss_statu _statu;           // 用户状态:未登录 已登录
    wsserver_t::timer_ptr _tp; // session关联的定时器
public:
    session(uint64_t ssid);
    ~session();
     // 设置状态
    void set_statu(ss_statu statu);
    // 设置用户id
    void set_user(uint64_t uid);
    // 获取ssid
    uint64_t ssid();
    // 获取用户id
    uint64_t get_user();
    // 判断用户是否登录
    bool is_login();
    // 设置定时器
    void set_timer(const wsserver_t::timer_ptr &tp);
    // 获取定时器
    wsserver_t::timer_ptr &get_timer();
};

具体实现:

// 定义用户状态:登录 未登录
typedef enum
{
    UNLOHIN,
    LOGIN
} ss_statu;

class session
{
private:
    uint64_t _ssid;            // 标识符
    uint64_t _uid;             // session对应的用户id
    ss_statu _statu;           // 用户状态:未登录 已登录
    wsserver_t::timer_ptr _tp; // session关联的定时器
public:
    session(uint64_t ssid)
        : _ssid(ssid) { DLOG("session %p被创建", this); }

    ~session() { DLOG("session %p被释放", this); }

    // 设置状态
    void set_statu(ss_statu statu) { _statu = statu; }

    // 设置用户id
    void set_user(uint64_t uid) { _uid = uid; }

    // 获取ssid
    uint64_t ssid() { return _ssid; }

    // 获取用户id
    uint64_t get_user() { return _uid; }

    // 判断用户是否登录
    bool is_login() { return _statu == LOGIN; }

    // 设置定时器
    void set_timer(const wsserver_t::timer_ptr &tp) { _tp = tp; }

    // 获取定时器
    wsserver_t::timer_ptr &get_timer() { return _tp; }
};

4.session管理设计实现

session的管理主要包含以下几个点:

1.创建⼀个新的session

2.通过ssid获取session

3.通过ssid判断session是否存在

4.销毁session。

5.为session设置过期时间,过期后session被销毁

session管理:

1.创建session

2.为session设置过期时间

3.获取session

4.销毁session(过期自动销毁)

管理的数据:

1.ssid的计数器

2.互斥锁

3.管理器 unordered_map<ssid,session_ptr>

4.websocket的server对象–用于添加定时任务

主要框架:

using session_ptr = std::shared_ptr<session>;
class session_manager
{
private:
    uint64_t _next_ssid;                                // 下一个ssid
    std::mutex _mutex;                                  // 互斥锁
    std::unordered_map<uint64_t, session_ptr> _session; // session id 与session的关联关系
    wsserver_t *_server;                                // 服务器句柄
public:
    session_manager(wsserver_t *server);
    ~session_manager();
    // 创建session对象
    session_ptr create_session(uint64_t uid, ss_statu statu);
    // 通过ssid获取session信息
    session_ptr get_session_by_ssid(uint64_t ssid);
    // 销毁session
    void remove_session(uint64_t ssid);
    // 为session设置过期时间
    void set_session_expire_time(uint64_t ssid, int ms);
};

具体实现:

// 定义session的智能指针
using session_ptr = std::shared_ptr<session>;

// 定义删除时间
#define SESSION_TIMEOUT 30000
#define SESSION_FOREVER -1

class session_manager
{
private:
    uint64_t _next_ssid;                                // 下一个ssid
    std::mutex _mutex;                                  // 互斥锁
    std::unordered_map<uint64_t, session_ptr> _session; // session id 与session的关联关系
    wsserver_t *_server;                                // 服务器句柄
public:
    session_manager(wsserver_t *server)
        : _next_ssid(1), _server(server)
    {
        DLOG("session管理器初始化完成");
    }
    ~session_manager()
    {
        DLOG("session管理器销毁完成");
    }

    // 创建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;
    }

    // 通过ssid获取session信息
    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 append_session(const session_ptr &ssp)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _session.insert(std::make_pair(ssp->ssid(), ssp));
    }

    // 销毁session
    void remove_session(uint64_t ssid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _session.erase(ssid);
    }

    // 为session设置过期时间
    void set_session_expire_time(uint64_t ssid, int ms)
    {
        // 依赖于websocketpp的定时器来完成session生命周期的管理
        // 在http通信的时候(登录,注册)session应该具备生命周期,指定时间无通信后删除
        // 在客户端建立websocket长连接之后,session应该是永久存在的
        // 登录之后,创建session,session需要在指定时间无通信后删除
        // 但是进入游戏大厅,或者游戏房间,这个session就应该永久存在
        // 等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除

        // 根据ssp获取session对象的智能指针对象
        session_ptr ssp = get_session_by_ssid(ssid);
        if (ssp.get() == nullptr)
        {
            return;
        }

        // 获取定时器
        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 tmp_tp = _server->set_timer(ms,
                    	std::bind(&session_manager::remove_session, this, ssid));
            ssp->set_timer(tmp_tp);
        }

        // 3.在session设置了定时删除的情况下,将session设置为永久存在
        else if (tp.get() != nullptr && ms == SESSION_FOREVER)
        {
            // 删除定时任务---stready_timer删除定时任务会导致任务会立即执行
            // 因为这个取消取消定时任务并不是立即取消的
            tp->cancel();
            // 因此重新给session管理器中,添加一个session信息,且添加的时候需要使用定时器,而不是立即添加
            // 将session关联的定时器置为空
            ssp->set_timer(wsserver_t::timer_ptr());
            _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
        }

        // 4.在session设置了定时删除的情况下,将session重置删除时间
        else if (tp.get() != nullptr && ms != SESSION_FOREVER)
        {
            // 因为这个取消取消定时任务并不是立即取消的
            tp->cancel();
            // 将session关联的定时器置为空
            ssp->set_timer(wsserver_t::timer_ptr());
            _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));

            // 重新给session添加定时销毁任务
            wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms,
                                std::bind(&session_manager::remove_session, this, ssid));

            // 重新设置session关联的定时器
            ssp->set_timer(tmp_tp);
        }
    }
};

十、五子棋对战玩家匹配管理设计实现

1.匹配队列实现

五子棋对战的玩家匹配是根据自己的天梯分数进行匹配的,而服务器中将玩家天梯分数分为三个档次:

1.青铜:天梯分数小于2000分

2.白银:天梯分数介于2000~3000分之间

3.黄金:天梯分数大于3000分

而实现玩家匹配的思想非常简单,为不同的档次设计各自的匹配队列,当⼀个队列中的玩家数量大于等于2的时候,则意味着同⼀档次中,有2个及以上的人要进行实战匹配,则出队队列中的前两个用户,相当于队首2个玩家匹配成功,这时候为其创建房间,并将两个用户信息加入房间中

匹配对战:

1.将所有玩家,根据得分划分为三个档次

socre < 2000; socre >= 2000 && score <3000; score >=3000

2.为3个不同档次创建不同的匹配队列

3.如果有玩家要进行对战匹配,则根据玩家分数,将玩家的id,加入到指定的队列中

4.当一个队列中元素的数量 >= 2 个,则表示有两个玩家要进行匹配,此时匹配成功

5.出队列中的前两个元素,为这两个玩家创建房间

6.向匹配成功的玩家,发送匹配响应,对战匹配成功

设计:

1.设计一个匹配队列—阻塞队列

2.匹配管理:

因为匹配队列有3个,因此创建三个线程,阻塞等待指定队列中的玩家数量>=2

设计一个阻塞队列:(目的是用于实现玩家匹配队列)

功能:

1.入队数据

2.出队数据

3.移除指定的数据

4.线程安全

5.获取队列元素的个数

6.阻塞

7.判断队列是否为空

主要框架:

template <class T>
class match
{
private:
    /*用链表而不直接使用queue是因为我们有中间删除数据的需要*/
    std::list<T> _list; // 使用链表来模拟队列
    /*实现线程安全*/
    std::mutex _mutex;
    // 这个条件变量主要为了阻塞消费者,后边使用的时候:队列中元素个数<2则阻塞
    std::condition_variable _cond;

public:
    // 获取元素个数
    int size();
    // 判断是否为空
    bool empty();
    // 阻塞线程
    void wait();
    // 入队数据,并唤醒线程
    void push(const T &data);
    // 出队数据
    bool pop(T &data);
    // 删除指定的数据
    void remove(T &data);
};

具体实现:

template <class T>
class match
{
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();
    }

    // 入队数据,并唤醒线程
    void push(const 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);
    }
};

2.玩家匹配管理模块设计实现

匹配管理:

1.三个不同档次的队列

2.三个线程分别对三个队列中的玩家进行匹配

3.房间管理模块的句柄

4.在线用户管理模块的句柄

5.数据管理模块–用户表的句柄

功能:

1.添加用户到匹配队列

2.从匹配队列移除用户

3.线程入口函数

​ 判断指定队列是否人数大于2

​ 出队两个玩家

​ 创建房间将两个玩家添加到房间中

​ 向两个玩家发送对战匹配成功的消息

主要框架:

class matcher_manager
{
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;
    // 在线用户管理句柄
    onlie_manager *_om;

private:
    // 处理匹配任务的函数
    void handle_match(match_queue<uint64_t> &queue);

    // 不同的等级传入不同的参数即可
    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_manager(room_manager *rm, user_table *ut, onlie_manager *om);

    ~matcher_manager();
    
    // 添加用户到匹配队列
    // 根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列
    bool add(uint64_t uid);

    // 从匹配队列中移除指定玩家
    bool del(uint64_t uid);
};

具体实现:

处理匹配任务的函数分为一下几个步骤:

1.判断队列中人数是否大于2 小于2则等待

2.此时队列中的人数大于2,出队两个玩家

3.此时两个玩家都出队成功,判断两个玩家是否都在线,如果有人掉线,则把另外一个人入队列

4.为两个玩家创建房间,并将玩家加入房间

5.对两个玩家进行响应

添加用户到匹配队列分为以下步骤:

1.根据玩家id获取玩家信息 – 天梯分数

2.添加到指定的队列中

从匹配队列中移除指定玩家分为以下步骤:

1.根据玩家id获取玩家信息 – 天梯分数

2.将玩家从指定的队列中移除

class matcher_manager
{
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;
    // 在线用户管理句柄
    onlie_manager *_om;

private:
    // 处理匹配任务的函数
    void handle_match(match_queue<uint64_t> &queue)
    {
        while (true)
        {
            // 1.判断队列中人数是否大于2 小于2则等待
            while (queue.size() < 2)
            {
                queue.wait();
            }

            // 2.此时队列中的人数大于2,出队两个玩家
            uint64_t uid1, uid2;
            bool ret = queue.pop(uid1);
            // 如果玩家1出队列失败则不执行后面的语句,重新判断是否人数大于2
            if (ret == false)
            {
                continue;
            }
            
            ret = queue.pop(uid2);
            // 此时玩家1出队成功,而玩家2出队失败,此时就将玩家1入队,继续判断人数是否大于2
            if (ret == false)
            {
                this->add(uid1);
                continue;
            }

            // 3.此时两个玩家都出队成功,判断两个玩家是否都在线,如果有人掉线,则把另外一个人入队列
            wsserver_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);
            if (conn1.get() == nullptr)
            {
                this->add(uid2);
                continue;
            }

            wsserver_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);
            if (conn2.get() == nullptr)
            {
                this->add(uid1);
                continue;
            }

            // 4.为两个玩家创建房间,并将玩家加入房间
            room_ptr rp = _rm->ctrate_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_manager(room_manager *rm, user_table *ut, onlie_manager *om)
        : _rm(rm), _ut(ut), _om(om),
          _th_normal(std::thread(&matcher_manager::th_normal_entry, this)),
          _th_high(std::thread(&matcher_manager::th_high_entry, this)),
          _th_super(std::thread(&matcher_manager::th_super_entry, this))
    {
        DLOG("游戏匹配模块初始化完毕");
    }

    ~matcher_manager()
    {
        DLOG("游戏模块即将销毁");
    }
    
    // 添加用户到匹配队列
    // 根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列
    bool add(uint64_t uid)
    {
        // 1.根据玩家id获取玩家信息 -- 天梯分数
        Json::Value user;
        bool ret = _ut->select_by_id(uid, user);
        if (ret == false)
        {
            DLOG("获取玩家:%d 信息失败", uid);
            return false;
        }

        int score = user["score"].asInt();
        // 2.添加到指定的队列中
        if (score < 2000)
        {
            _q_normal.push(uid);
        }
        else if (score >= 2000 && score < 3000)
        {
            _q_high.push(uid);
        }
        else
        {
            _q_super.push(uid);
        }
        return true;
    }

    // 从匹配队列中移除指定玩家
    bool del(uint64_t uid)
    {
        Json::Value user;
        bool ret = _ut->select_by_id(uid, user);
        if (ret == false)
        {
            DLOG("获取玩家:%d 信息失败", uid);
            return false;
        }

        int score = user["score"].asInt();
        if (score < 2000)
        {
            _q_normal.remove(uid);
        }
        else if (score >= 2000 && score < 3000)
        {
            _q_high.remove(uid);
        }
        else
        {
            _q_super.remove(uid);
        }

        return true;
    }
};

十一、整合封装服务器模块设计实现

服务器的整合实现:

1.网络通信接口的设计

收到一个什么格式的数据,代表了什么样的请求,应该给与什么样的业务处理及响应

2.开始搭建服务器

2.1搭建websocket服务器,实现网络通信

2.2针对各种不同的请求进行不同的业务处理

客户端存在的请求:

http请求:

1.客户端从服务器获取一个注册页面

2.客户端给服务器发送一个注册的请求(提交了用户名&&密码)

3.客户端从服务器获取一个登录页面

4.客户端给服务器发送一个登录的请求(提交了用户名&&密码)

5.客户端从服务器获取一个游戏大厅页面

6.客户端给服务器发送了一个获取个人信息的请求(展示个人信息)

websocket

7.客户端给服务器发送了一个切换websocket协议通信的请求(建立游戏大厅长连接)

8.客户端给服务器发送了一个游戏对战匹配的请求

9.客户端给服务器发送了一个停止游戏匹配的请求

10.对战匹配成功,客户端从服务器获取一个游戏房间页面

11.客户端给服务器发送了一个切换websocket通信协议的请求(建立游戏房间长连接)

12.客户端给服务器发送了一个下棋的请求

13.客户端给服务器发送了一个聊天的请求

14.游戏结束,返回游戏大厅(客户端给服务器发送一个获取游戏大厅页面的请求–5)

1、3、5为静态资源的请求

2、4、6为动态功能切换的请求

1.通信接口设计(Restful风格)

1.1静态资源请求

静态资源页面,在后台服务器上就是个html/css/js⽂件
静态资源请求的处理,其实就是将文件中的内容发送给客户端
1. 注册页面请求
请求:GET /register.html HTTP/1.1
响应:
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
register.html⽂件的内容数据
2. 登录页面请求
请求:GET /login.html HTTP/1.1
3. 大厅页面请求
请求:GET /game_hall.html HTTP/1.1
4. 房间页面请求
请求:GET /game_room.html HTTP/1.1

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": "用户名已经被占⽤"}

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": "用户名或密码错误"}

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": "用户还未登录"}

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

/* ws://localhost:9000/match */
GET /match HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......
HTTP/1.1 101 Switching
......

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

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

1.6开始对战匹配

{
 "optype": "match_start"
}
/*后台正确处理后回复*/
{
 "optype": "match_start", //表示成功加入匹配队列
 "result": true
}
/*后台处理出错回复*/
{
 "optype": "match_start"
 "result": false,
 "reason": "具体原因...."
}
/*匹配成功了给客户端的回复*/
{
 "optype": "match_success", //表示成匹配成功
 "result": true
}

1.7停止匹配

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

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

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

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是谁,谁就赢了)
}

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": "赶紧点"
}

2.服务器模块实现

2.1静态资源请求的处理file_handler

1.获取到请求的uri–资源路径,了解客户端请求的页面文件的名称

2.组合出文件的实际路径 相对根目录 + uri

3.如果请求的是一个目录,增加一个后缀 login.html

4.读取文件内容

4.1文件不存在,读取文件内容失败,则打开404页面,默认打开404页面一定成功

5.设置响应正文

2.2用户注册功能请求的后端处理

1.获取请求正文

2.对正文进行Json反序列化,得到用户名和密码

3.进行数据库的用户新增操作

​ 3.1如果用户名和密码至少有一个为空,则返回错误

如果成功了,则返回200

如果失败了,则返回400

2.3用户注册功能请求的前端处理

1.给按钮添加点击事件,调用注册函数

2.封装实现注册函数

2.1获取两个输入框中的数据,组织成为一个json串

​ 2.2通过Ajax向后台发送用户注册请求

​ 2.2.1 如果请求失败了,则清空两个输入框中的内容,并提示错误原因

​ 2.2.2 如果请求成功,则跳转到登录页面

2.4用户登录功能请求的后端处理

1.获取请求正文,并进行json反序列化,得到用户名和密码

2.校验正文的完整性,进行数据库的用户信息验证

​ 2.1用户名密码不完整

​ 2.2用户密码错误

3.如果验证成功,则给客户端返回session

​ 3.1创建会话失败

4.设置过期时间

5.设置响应头部,Set-Cookie,将sessionid通过cookie返回

2.5用户登录功能请求的前端处理

1.给按钮添加点击事件,调用登录请求函数

2.封装登录请求函数

​ 2.1获取输入框中的用户名和密码。并组织json对象

​ 2.2通过Ajax向后台发送登录验证的请求

​ 2.3如果验证通过,则跳转游戏大厅页面

​ 2.4如果验证失败,则提示错误信息,并情况输入框中的内容

2.6用户信息获取功能请求的处理

1.获取请求信息中的Cookie,从Cookie中获取ssid

​ 如果没有cookie,返回错误,没有cookie信息,让用户重新登录

2.在session管理中查找对应的会话信息

​ 没有找到session,则认为登录已经过期,需要重新登录

3.从数据库中取出用户信息,进行序列化发送给客户端

4.刷新session的过期时间

cookie的格式如下:

Cookie: Cookie: name=value

Cookie:name=value; name2=value2; name3=value3

形式为名称-值对的列表 =,列表中的成对由分号和空格('; ')分隔

2.7建立游戏大厅的长连接

1.登录验证–判断当前用户是否已经成功登录

2.判断当前客户端是否重复登录

3.将当前客户端以及连接加入到游戏大厅

4.给客户端响应游戏大厅连接建立成功

5.将session设置为永久存在

2.8断开游戏大厅的长连接

1.登录验证–判断当前用户是否已经成功登录

2.将玩家从游戏大厅中移除

3.将session恢复生命周期,设置定时销毁

2.9游戏大厅信息处理

1.身份验证,当前客户端到底是哪个玩家

2.获取请求信息

3.对于请求进行处理

​ 3.1开始对战匹配:通过匹配模块,将用户添加到匹配队列中

​ 3.2取消对战匹配:通过匹配模块,将用户从匹配队列中移除

4.给客户端响应

2.10建立游戏房间的长连接

1.获取当前客户端的session

2.当前用户是否已经在游戏房间或者游戏大厅中

3.判断当前用户是否已经创建好了房间

4.将当前用户添加到在线用户管理的游戏房间中

5.将session设置为永久存在

6.回复房间准备完毕

2.11断开游戏房间的长连接

1.登录验证–判断当前用户是否已经成功登录–获取会话信息,识别客户端

2.将玩家从在线用户管理中移除

3.将session恢复生命周期,设置定时销毁

4.将玩家从游戏房间中移除,房间中所有用户退出就会销毁房间

2.12游戏房间信息处理

1.获取客户端session,识别客户端身份

2.获取客户端房间信息

3.对消息进行反序列化

4.通过房间模块进行消息请求的处理

2.13完整代码

#ifndef __M_SERVER_H__
#define __M_SERVER_H__

#include "db.hpp"
#include "matcher.hpp"
#include "online.hpp"
#include "room.hpp"
#include "session.hpp"
#include "util.hpp"

// web根目录
#define WEBROOT "./wwwroot/"
// 默认端口号
static const uint16_t defaultport = 8080;

class gobang_server
{
private:
    std::string _web_root; // 静态根目录 ./wwwroot/   /register.html -> ./wwwroot/register.html
    wsserver_t _wssrv;     // 服务器句柄
    user_table _ut;        // 用户管理模块句柄
    online_manager _om;    // 在线用户管理的句柄
    room_manager _rm;      // 游戏房间管理的句柄
    session_manager _sm;   // session管理的句柄
    matcher_manager _mm;   // 匹配管理的句柄

public:
    // 对成员进行初始化,以及服务器回调函数的设置
    gobang_server(const std::string &host, const std::string &username,
                  const std::string &password, const std::string dbname,
                  const uint16_t &port = 3306, const std::string &webroot = WEBROOT)
        : _web_root(webroot), _ut(host, username, password, dbname, port), _rm(&_ut, &_om),
          _sm(&_wssrv), _mm(&_rm, &_ut, &_om)
    {
        // 设置日志等级
        _wssrv.set_access_channels(websocketpp::log::alevel::none);
        // 初始化asio调度器
        _wssrv.init_asio();
        // 设置地址复用
        _wssrv.set_reuse_addr(true);
        // 4.设置回调函数
        _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 = defaultport)
    {
        // 设置监听端口
        _wssrv.listen(port);
        // 开始获取连接
        _wssrv.start_accept();
        // 启动服务器
        _wssrv.run();
    }

private:
    // 静态资源请求的处理
    void file_handler(wsserver_t::connection_ptr &conn)
    {
        // 1.获取到请求的uri--资源路径,了解客户端请求的页面文件的名称
        websocketpp::http::parser::request req = conn->get_request();
        // 2.组合出文件的实际路径  相对根目录 + uri
        std::string uri = req.get_uri();
        std::string realpath = _web_root + uri;

        // 3.如果请求的是一个目录,增加一个后缀 login.html
        if (realpath.back() == '/')
        {
            realpath += "login.html";
        }

        // 4.读取文件内容
        std::string body;
        bool ret = file_util::read(realpath, body);
        // 4.1文件不存在,读取文件内容失败,则打开404页面,默认打开404页面一定成功
        if (ret == false)
        {
            realpath = _web_root + "404.html";
            file_util::read(realpath, body);
            conn->set_status(websocketpp::http::status_code::not_found);
            conn->set_body(body);
            return;
        }

        // 5.设置响应正文
        conn->set_status(websocketpp::http::status_code::ok);
        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对象
        Json::Value resp_json;
        resp_json["result"] = result;
        resp_json["reason"] = reason;

        // 将json对象进行序列化
        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(wsserver_t::connection_ptr &conn)
    {
        // 1.获取请求正文
        websocketpp::http::parser::request req = conn->get_request();
        std::string req_body = conn->get_request_body();
        // 2.对正文进行Json反序列化,得到用户名和密码
        Json::Value log_info;
        bool ret = json_util::deserialize(req_body, log_info);
        if (ret == false)
        {
            DLOG("反序列化注册信息失败");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");
        }

        // 3.进行数据库的用户新增操作
        // 3.1如果用户名和密码至少有一个为空,则返回错误
        if (log_info["username"].isNull() || log_info["password"].isNull())
        {
            DLOG("用户名密码不完整");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");
        }

        // 插入数据
        ret = _ut.insert(log_info);

        // 3.2向数据插入失败
        if (ret == false)
        {
            DLOG("向数据库插入数据失败");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名已经被占用");
        }

        // 如果成功了,则返回200
        return http_resp(conn, true, websocketpp::http::status_code::ok, "用户注册成功");
    }

    // 用户登录功能请求的处理
    void login(wsserver_t::connection_ptr &conn)
    {
        // 1.获取请求正文,并进行json反序列化,得到用户名和密码
        websocketpp::http::parser::request req = conn->get_request();
        std::string req_body = conn->get_request_body();

        Json::Value log_info;
        bool ret = json_util::deserialize(req_body, log_info);
        if (ret == false)
        {
            DLOG("反序列化登录信息失败");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");
        }

        // 2.校验正文的完整性,进行数据库的用户信息验证
        // 2.1用户名密码不完整
        if (log_info["username"].isNull() || log_info["password"].isNull())
        {
            DLOG("用户名密码不完整");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入完整的用户名密码");
        }

        ret = _ut.login(log_info);

        // 2.2用户密码错误
        if (ret == false)
        {
            DLOG("用户密码错误");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户密码错误");
        }

        // 3.如果验证成功,则给客户端返回session
        uint64_t uid = log_info["id"].asInt64();
        session_ptr ssp = _sm.create_session(uid, LOGIN);

        // 3.1创建会话失败
        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, "用户登录成功");
    }

    // 从cookie中获取ssid
    // cookie的格式如下:
    // Cookie: <cookie-list>Cookie: name=value
    // Cookie:name=value; name2=value2; name3=value3
    // <cookie-list>形式为名称-值对的列表 <cookie-name>=<cookie-value>,列表中的成对由分号和空格('; ')分隔
    bool get_cookie_val(const std::string &cookie, 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, sep, cookie_arr);

        // 对单个字符串进行以 = 为间隔的分隔,得到key和value
        for (auto &str : cookie_arr)
        {
            std::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(wsserver_t::connection_ptr &conn)
    {
        // 1.获取请求信息中的Cookie,从Cookie中获取ssid
        std::string cookie_str = conn->get_request_header("Cookie");
        if (cookie_str.empty())
        {
            return http_resp(conn, false, websocketpp::http::status_code::bad_request,
                             "找不到cookie信息,请重新登录");
        }

        // 从Cookie中获取ssid
        std::string ssid_str;

        // SSID=ssid
        bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);
        if (ret == false)
        {
            // 如果没有cookie,返回错误,没有cookie信息,让用户重新登录
            return http_resp(conn, false, 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,则认为登录已经过期,需要重新登录
            http_resp(conn, false, websocketpp::http::status_code::bad_request, "没有找到session信息,需要重新登录");
        }

        // 3.从数据库中取出用户信息,进行序列化发送给客户端
        uint64_t uid = ssp->get_user_id();
        Json::Value user_info;
        ret = _ut.select_by_id(uid, user_info);
        if (ret == false)
        {
            // 获取用户信息失败
            http_resp(conn, false, 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);
    }

    // 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();
        // 从请求中获取请求方法和uri
        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(wsserver_t::connection_ptr conn, Json::Value &resp)
    {
        std::string body;
        json_util::serialize(resp, body);
        conn->send(body);
    }
	
    // 通过Cookie 获取session
    session_ptr get_session_by_cookie(wsserver_t::connection_ptr conn)
    {
        // 1.获取请求信息中的Cookie,从Cookie中获取ssid
        Json::Value resp_json;
        std::string cookie_str = conn->get_request_header("Cookie");
        if (cookie_str.empty())
        {
            // 如果没有cookie,返回错误:没有cookie信息,让客户端重新登录
            resp_json["optype"] = "hall_ready";
            resp_json["result"] = false;
            resp_json["reason"] = "没有找到cookie信息,请重新登录";
            ws_resp(conn, resp_json);
            return session_ptr();
        }

        // 2.从Cookie中获取ssid
        std::string ssid_str;
        bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);
        if (ret == false)
        {
            // 如果没有cookie,返回错误,没有cookie信息,让用户重新登录
            resp_json["optype"] = "hall_ready";
            resp_json["result"] = false;
            resp_json["reason"] = "没有找到ssid信息,需要重新登录";
            ws_resp(conn, resp_json);
            return session_ptr();
        }

        // 3.在session管理中查找对应的会话信息
        session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));
        if (ssp.get() == nullptr)
        {
            resp_json["optype"] = "hall_ready";
            resp_json["result"] = false;
            resp_json["reason"] = "没有找到session信息,需要重新登录";
            ws_resp(conn, resp_json);
            return session_ptr();
        }

        return ssp;
    }

    // 建立游戏大厅的长连接
    void wsopen_game_hall(wsserver_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_id()) || _om.is_in_game_room(ssp->get_user_id()))
        {
            resp_json["optype"] = "hall_ready";
            resp_json["result"] = false;
            resp_json["reason"] = "玩家重复登录";
            return ws_resp(conn, resp_json);
        }

        // 3.将当前客户端以及连接加入到游戏大厅
        _om.enter_game_hall(ssp->get_user_id(), 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);
        DLOG("玩家%d的session在游戏大厅中已经被设置为永久存在", ssp->get_user_id());
    }

    // 建立游戏房间的长连接
    void wsopen_game_room(wsserver_t::connection_ptr conn)
    {
        Json::Value resp_json;
        // 1. 获取当前客户端的session
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }

        // 2. 当前用户是否已经在在线用户管理的游戏房间或者游戏大厅中---在线用户管理
        if (_om.is_in_game_hall(ssp->get_user_id()) || _om.is_in_game_room(ssp->get_user_id()))
        {
            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_id());
        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_id(), conn);
        DLOG("玩家%d被添加到在线用户管理的游戏房间中", ssp->get_user_id());

        // 5. 将session重新设置为永久存在
        _sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);
        DLOG("玩家%d的session在游戏房间中已经被设置为永久存在", ssp->get_user_id());

        // 6. 回复房间准备完毕
        resp_json["optype"] = "room_ready";
        resp_json["result"] = true;
        resp_json["room_id"] = (Json::UInt64)rp->get_room_id();
        resp_json["uid"] = (Json::UInt64)ssp->get_user_id();
        resp_json["white_id"] = (Json::UInt64)rp->get_white_id();
        resp_json["black_id"] = (Json::UInt64)rp->get_black_id();

        return ws_resp(conn, resp_json);
    }

    // websocket握手成功回调处理函数
    void wsopen_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 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(wsserver_t::connection_ptr conn)
    {
        Json::Value resp_json;
        // 1.登录验证--判断当前用户是否已经成功登录
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }

        // 2.将玩家从游戏大厅中移除
        _om.exit_game_hall(ssp->get_user_id());
        // 3.将session恢复生命周期,设置定时销毁
        _sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);
    }

    // 断开游戏房间的长连接
    void wsclose_game_room(wsserver_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_id());

        // 2. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间
        _rm.remove_room_user(ssp->get_user_id());

        // 3. 将session回复生命周期的管理,设置定时销毁
        _sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);
    }

    // websocket连接关闭回调处理函数
    void wsclose_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 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(wsserver_t::connection_ptr conn, wsserver_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::deserialize(req_body, req_json);
        if (ret == false)
        {
            resp_json["result"] = false;
            resp_json["reason"] = "请求信息解析失败";
            return ws_resp(conn, resp_json);
        }

        // 3.对于请求进行处理
        // 3.1开始对战匹配:通过匹配模块,将用户添加到匹配队列中
        if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start")
        {
            _mm.add(ssp->get_user_id());
            resp_json["optype"] = "match_start";
            resp_json["result"] = true;
            DLOG("玩家%d开始匹配", ssp->get_user_id());
            return ws_resp(conn, resp_json);
        }

        // 3.2取消对战匹配:通过匹配模块,将用户从匹配队列中移除
        else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop")
        {
            _mm.del(ssp->get_user_id());
            resp_json["optype"] = "match_stop";
            resp_json["result"] = true;
            DLOG("玩家%d取消匹配", ssp->get_user_id());
            return ws_resp(conn, resp_json);
        }
        // 4.给客户端端响应
        resp_json["optype"] = "unknow";
        resp_json["result"] = false;
        return ws_resp(conn, resp_json);
    }

    // 游戏房间信息处理
    void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_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_id());
        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::deserialize(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);
    }

    // websocket消息回调处理函数
    void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg)
    {
        wsserver_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);
        }
    }
};

#endif

十二、客户端开发

1.登录页面: 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">
                <!-- 1.给按钮添加点击事件,调用登录请求函数 -->
                <button id="submit" onclick="login()">提交</button>
            </div>
        </div>

    </div>

    <script src="./js/jquery.min.js"></script>
    <script>
        // 2.封装登录请求函数
        function login() {
            // ​2.1获取输入框中的用户名和密码。并组织json对象
            var login_info = {
                username: document.getElementById("user_name").value,
                password: document.getElementById("password").value,
            };

            // ​2.2通过Ajax向后台发送登录验证的请求
            $.ajax({
                url: "/login",
                method: "post",
                data: JSON.stringify(login_info),
                success: function (res) {
                    // ​2.3如果验证通过,则跳转游戏大厅页面
                    alert("登录成功");
                    window.location.assign("/game_hall.html");
                },
                error: function (xhr) {
                    // ​2.4如果验证失败,则提示错误信息,并情况输入框中的内容
                    alert(JSON.stringify(xhr));
                    document.getElementById("user_name").value = "";
                    document.getElementById("password").value = "";
                }
            })
        }
    </script>
</body>

</html>

2.注册页面: 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() {
            // 2.1获取两个输入框中的数据,组织成为一个json串
            var reg_info =
            {
                username: document.getElementById("user_name").value,
                password: document.getElementById("password").value
            };
            console.log(JSON.stringify(reg_info));

            // 2.2通过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.reson);
                        }
                        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>

</html>

3.游戏大厅页面: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 button_element = document.getElementById("match-button");
        button_element.onclick = function () {
            // 1.没有进行匹配的状态下点击按钮,发送对战匹配请求
            if (button_flag == "stop") {
                var req_json = {
                    optype: "match_start"
                };
                ws_hdl.send(JSON.stringify(req_json));
            }
            // 2.正在进行匹配的状态下点击按钮,发送取消对战请求
            else
            {
                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_close;
                    ws_hdl.onerror = ws_error;
                    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_close() {
            console.log("websocket onclose");
        }
        function ws_error() {
            console.log("websocket error");
        }
        function ws_onmessage(evt) {
            var resp_json = JSON.parse(evt.data);
            if (resp_json.result == false) {
                alert(evt.data);
                location.replace("/login.html");
                return;
            }
            // 游戏大厅连接建立成功
            if(resp_json["optype"] == "hall_ready")
            {
                alert("游戏大厅连接建立成功");
            }
            // 对战匹配成功
            else if(resp_json["optype"] == "match_success")
            {
                alert("对战匹配成功,进入游戏房间");
                location.replace("/game_room.html");
            }
            // 开始匹配
            else if(resp_json["optype"] == "match_start")
            {
                console.log("玩家已经加入匹配队列");
                button_flag = "start";
                button_element.innerHTML = "匹配中....点击按钮停止匹配";
                return;
            }
            // 取消匹配
            else if(resp_json["optype"] == "match_stop")
            {
                console.log("玩家取消匹配");
                button_flag = "stop";
                button_element.innerHTML = "开始匹配";
                return;
            }
            else
            {
                alert(evt.data);
                location.replace("/login.html");
                return;
            }
        }
        get_user_info();
    </script>
</body>

</html>

4.游戏房间页面: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>
            </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');
        //获取chess控件区域2d画布
        let context = chess.getContext('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();
            // 背景图片
            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) {
            // 1.获取下棋位置,判断下棋操作十分正常
            // 1.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));
        }

        // 设置离开当前页面立即断开websocket连接
        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.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;
                if (info.result == false) {
                    alert(info.reason);
                    location.replace("/login.html");
                }
                else {
                    // 初始化棋盘
                    initGame();

                    // 输出谁先下棋
                    set_screen(is_me);
                }

            }

            // 下棋的信息
            else if (info.optype == "put_chess") {
                console.log("put_chess" + evt.data);
                // 1.2走棋操作
                // 3.收到走棋消息,进行棋子绘制
                if (info.result == false) {
                    alert(info.reason);
                    return;
                }

                // 当前走棋的用户id与我自己的用户id相同,就是我自己走棋,走棋之后,就轮到对方了
                is_me = info.uid == room_info.uid ? false : true;

                // 更新screen显示的内容
                set_screen(is_me);
                // 绘制棋子颜色,应该根据当前下棋角色的颜色确定
                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);

                // 游戏结束,取消下棋位置的点击事件
                document.getElementById("chess").onclick = null;
                if (room_info.uid == info.winner) {
                    alert("恭喜你获得游戏胜利");
                    alert("点击'五星连珠,战无敌'下方的返回大厅按钮即可回到大厅");
                }
                else {
                    alert("遗憾败北,下次再接再厉");
                    alert("点击'遗憾败北'下方的返回大厅按钮即可回到大厅");
                }
            }
            // 聊天的请求
            else if (info.optype == "chat") {
                // 收到一条消息,判断resul,如果为true则渲染一条消息到显示框中
                if (info.result == false) {
                    alert(info.reason);
                    document.getElementById("chat_input").value = "";
                    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.聊天动作
        // 3.1捕捉聊天输入框消息
        // 3.2给发送按钮添加点击事件,点击按钮的时候,获取输入框中的信息,发送给服务器
        var chat_button_div = document.getElementById("chat_button");
        chat_button_div.onclick = function () {
            var msg = document.getElementById("chat_input").value;
            // 将不好的消息进行屏蔽
            if (msg == "垃圾") {
                msg = "****";
            }
            var send_msg = {
                optype: "chat",
                room_id: room_info.room_id,
                uid: room_info.uid,
                message: msg
            };

            ws_hdl.send(JSON.stringify(send_msg));
        }

    </script>
</body>

</html>

5.common.css

/* 公共的样式 */

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    height: 100%;

    background-image: url(../image/cat.jpg);
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
}

.nav {
    height: 50px;

    background-color: rgb(50, 50, 50);
    color: white;

    line-height: 50px;
    padding-left: 20px;
}

.container {
    width: 100%;
    height: calc(100% - 50px);

    display: flex;
    align-items: center;
    justify-content: center;
}

6.game_hall.css


#screen {
    width: 400px;
    height: 200px;
    font-size: 20px;
    background-color: gray;
    color: white;
    border-radius: 10px;

    text-align: center;
    line-height: 100px;
}

#match-button {
    width: 400px;
    height: 50px;
    font-size: 20px;
    color: white;
    background-color: orange;
    border: none;
    outline: none;
    border-radius: 10px;

    text-align: center;
    line-height: 50px;
    margin-top: 20px;
}

#match-button:active {
    background-color: gray;
}

7.game_room.css

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_area {
    width: 404px;
    height: 400px;
    margin-top: 10px;
    margin-left: 150px;
    border: 2px solid #fff; border-radius: 5px;
    position: relative;
    background-color: #fff;
}
#chat_show {
    width: 400px;
    height: 300px; overflow-y: scroll;
    background-color: #fec;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_input {
    width: 300px;
    height: 50px;
    margin-top: 30px;
    background-color: #fff;
    font-size: 30px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: #fec;
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:hover {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(201, 145, 32);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:active {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(149, 127, 83);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}

#self_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: right;
    background-color: rgb(7, 190, 102);
    border-radius: 5px;
}
#peer_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: left;
    background-color: rgb(93, 218, 243);
    border-radius: 5px;
}

8.login.css

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_area {
    width: 404px;
    height: 400px;
    margin-top: 10px;
    margin-left: 150px;
    border: 2px solid #fff; border-radius: 5px;
    position: relative;
    background-color: #fff;
}
#chat_show {
    width: 400px;
    height: 300px; overflow-y: scroll;
    background-color: #fec;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_input {
    width: 300px;
    height: 50px;
    margin-top: 30px;
    background-color: #fff;
    font-size: 30px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: #fec;
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:hover {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(201, 145, 32);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:active {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(149, 127, 83);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}

#self_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: right;
    background-color: rgb(7, 190, 102);
    border-radius: 5px;
}
#peer_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: left;
    background-color: rgb(93, 218, 243);
    border-radius: 5px;
}

完整代码地址:项目完整代码地址

十三、项目总结

项目名称:在线五子棋对战

项目简介:实现五子棋服务器,能够让用户通过浏览器访问服务器,进行用户的注册,登录,对战匹配,实时对战,实时聊天等功能

开发环境:Linux-centos,vim/vscode/g++/gdb/makefile

项目实现:

数据管理模块:基于mysql数据进行数据管理以及封装数据管理模块实现数据库的访问

网络服务器模块:基于websocketpp库搭建websocket服务器,实现与客户端网络通信

session管理模块:封装session管理,实现http客户端通信状态的维护以及身份的识别

在线用户管理模块:对于进入游戏大厅&游戏房间的长连接通信进行管理,实现随时能够获取客户端连接进行消息的主动推送

游戏房间管理:对于同一个房间中的用户及动作进行处理(对战匹配,下棋,聊天,退出)

对战匹配管理模块:将所有玩家 根据天梯分数进行等级划分,进行不同等级的对战匹配

业务处理:通过网络通信获取到客户端的请求,提供不同的业务处理

十四、项目扩展

1.实现局时/步时

局时: ⼀局游戏中玩家能思考的总时间

步时: ⼀步落子过程中,玩家能思考的时间

2.保存棋谱&录像回放

服务器可以把每⼀局对局、玩家轮流落子的位置都记录下来

玩家可以在游戏大厅页面选定某个曾经的比赛,在页面上回放出对局的过程

3.观战功能

在游戏大厅显示当前所有的对局房间

玩家可以选中某个房间以观众的形式加入到房间中,实时的看到选手的对局情况

4.虚拟对手&人机对战

msg_show_div.appendChild(msg_div);
msg_show_div.appendChild(br_div);

            // 将聊天输入框中的内容清空
            document.getElementById("chat_input").value = "";
        }
    }

    // 3.聊天动作
    // 3.1捕捉聊天输入框消息
    // 3.2给发送按钮添加点击事件,点击按钮的时候,获取输入框中的信息,发送给服务器
    var chat_button_div = document.getElementById("chat_button");
    chat_button_div.onclick = function () {
        var msg = document.getElementById("chat_input").value;
        // 将不好的消息进行屏蔽
        if (msg == "垃圾") {
            msg = "****";
        }
        var send_msg = {
            optype: "chat",
            room_id: room_info.room_id,
            uid: room_info.uid,
            message: msg
        };

        ws_hdl.send(JSON.stringify(send_msg));
    }

</script>
```

5.common.css

/* 公共的样式 */

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    height: 100%;

    background-image: url(../image/cat.jpg);
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
}

.nav {
    height: 50px;

    background-color: rgb(50, 50, 50);
    color: white;

    line-height: 50px;
    padding-left: 20px;
}

.container {
    width: 100%;
    height: calc(100% - 50px);

    display: flex;
    align-items: center;
    justify-content: center;
}

6.game_hall.css


#screen {
    width: 400px;
    height: 200px;
    font-size: 20px;
    background-color: gray;
    color: white;
    border-radius: 10px;

    text-align: center;
    line-height: 100px;
}

#match-button {
    width: 400px;
    height: 50px;
    font-size: 20px;
    color: white;
    background-color: orange;
    border: none;
    outline: none;
    border-radius: 10px;

    text-align: center;
    line-height: 50px;
    margin-top: 20px;
}

#match-button:active {
    background-color: gray;
}

7.game_room.css

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_area {
    width: 404px;
    height: 400px;
    margin-top: 10px;
    margin-left: 150px;
    border: 2px solid #fff; border-radius: 5px;
    position: relative;
    background-color: #fff;
}
#chat_show {
    width: 400px;
    height: 300px; overflow-y: scroll;
    background-color: #fec;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_input {
    width: 300px;
    height: 50px;
    margin-top: 30px;
    background-color: #fff;
    font-size: 30px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: #fec;
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:hover {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(201, 145, 32);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:active {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(149, 127, 83);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}

#self_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: right;
    background-color: rgb(7, 190, 102);
    border-radius: 5px;
}
#peer_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: left;
    background-color: rgb(93, 218, 243);
    border-radius: 5px;
}

8.login.css

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_area {
    width: 404px;
    height: 400px;
    margin-top: 10px;
    margin-left: 150px;
    border: 2px solid #fff; border-radius: 5px;
    position: relative;
    background-color: #fff;
}
#chat_show {
    width: 400px;
    height: 300px; overflow-y: scroll;
    background-color: #fec;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
#chat_input {
    width: 300px;
    height: 50px;
    margin-top: 30px;
    background-color: #fff;
    font-size: 30px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: #fec;
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:hover {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(201, 145, 32);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}
#chat_button:active {
    width: 90px;
    height: 50px;
    margin-top: 30px;
    background-color: rgb(149, 127, 83);
    font-size: 15px;
    border: 2px solid #fec; border-radius: 5px;
    vertical-align: top;
}

#self_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: right;
    background-color: rgb(7, 190, 102);
    border-radius: 5px;
}
#peer_msg {
    height: 35px;
    line-height: 35px;
    font-size: 15px;
    float: left;
    background-color: rgb(93, 218, 243);
    border-radius: 5px;
}

完整代码地址:项目完整代码地址

十三、项目总结

项目名称:在线五子棋对战

项目简介:实现五子棋服务器,能够让用户通过浏览器访问服务器,进行用户的注册,登录,对战匹配,实时对战,实时聊天等功能

开发环境:Linux-centos,vim/vscode/g++/gdb/makefile

项目实现:

数据管理模块:基于mysql数据进行数据管理以及封装数据管理模块实现数据库的访问

网络服务器模块:基于websocketpp库搭建websocket服务器,实现与客户端网络通信

session管理模块:封装session管理,实现http客户端通信状态的维护以及身份的识别

在线用户管理模块:对于进入游戏大厅&游戏房间的长连接通信进行管理,实现随时能够获取客户端连接进行消息的主动推送

游戏房间管理:对于同一个房间中的用户及动作进行处理(对战匹配,下棋,聊天,退出)

对战匹配管理模块:将所有玩家 根据天梯分数进行等级划分,进行不同等级的对战匹配

业务处理:通过网络通信获取到客户端的请求,提供不同的业务处理

十四、项目扩展

1.实现局时/步时

局时: ⼀局游戏中玩家能思考的总时间

步时: ⼀步落子过程中,玩家能思考的时间

2.保存棋谱&录像回放

服务器可以把每⼀局对局、玩家轮流落子的位置都记录下来

玩家可以在游戏大厅页面选定某个曾经的比赛,在页面上回放出对局的过程

3.观战功能

在游戏大厅显示当前所有的对局房间

玩家可以选中某个房间以观众的形式加入到房间中,实时的看到选手的对局情况

4.虚拟对手&人机对战

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


http://www.kler.cn/a/597402.html

相关文章:

  • STM32 HAL库函数原理解析
  • VSCode配置C语言保姆课
  • 数据结构——最短路径BFS算法
  • taosdump恢复数据库
  • Qt窗口控件之对话框QDialog
  • 31天Python入门——第10天:深入理解值传递·引用传递以及深浅拷贝问题
  • 银河麒麟V10-SP3-aarch64操作系统版本 docker run时报错permission denied
  • Springboot之RequestContextHolder 学习笔记
  • 高防ip和高防服务器的区别?
  • Unity | Tag、Layer常量类生成工具
  • linux_vim
  • DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能
  • 一条sql语句在mysql中的执行流程(Mysql基础架构)
  • 基于Selenium Grid的分布式测试架构设计与深度实践
  • 视频播放器(watermelon Player)vue2使用体验(教程版)
  • u-net系列算法
  • Plant Simulation中怎么更改机器人3D模型
  • 华为终端将全面进入鸿蒙时代
  • 语法: ext_int_edge(source, edge)
  • JVM常用垃圾回收器