# 在线五子棋对战 **Repository Path**: Wei_st_IT/online-gobang-battle ## Basic Information - **Project Name**: 在线五子棋对战 - **Description**: 本项⽬主要实现⼀个⽹⻚版的五⼦棋对战游戏,其主要⽀持以下核⼼功能: • ⽤⼾管理:实现⽤⼾注册,⽤⼾登录、获取⽤⼾信息、⽤⼾天梯分数记录、⽤⼾⽐赛场次记录等 • 匹配对战:实现两个玩家在⽹⻚端根据天梯分数匹配游戏对⼿,并进⾏五⼦棋游戏对战的功能 • 聊天功能:实现两个玩家在下棋的同时可以进⾏实时聊天的功能 - **Primary Language**: C/C++ - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-11-01 - **Last Updated**: 2024-03-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # C++ - 在线五子棋对战 代码链接:https://gitee.com/Wei_st_IT/online-gobang-battle # 一、项目介绍 本项目主要实现⼀个网页版的五子棋对战游戏,其主要⽀持以下核心功能 • 用户管理:实现用户注册,用户登录、获取用户信息、用户天梯分数记录、用户用赛场次记录等 • 匹配对战:实现两个玩家在网页端根据天梯分数匹配游戏对手,并进⾏五子棋游戏对战的功能 • 聊天功能:实现两个玩家在下棋的同时可以进⾏实时聊天的功能 # 二、开发环境 • Linux(Centos-7.6 / Ubuntu-22.04) • VSCode/Vim • g++/gdb • Makefile # 三、核心技术 • HTTP/WebSocket • Websocket++ • JsonCpp • Mysql • C++11 • BlockQueue • HTML/CSS/JS/AJAX # 四、环境搭建 ## 4.1 Ubuntu-22.04环境搭建 ### 4.1.1 更换软件源 ```cpp dev@wei:~$ sudo cp /etc/apt/sources.list.d/original.list /etc/apt/sources.list.d/original.list.bak dev@wei:~$ sudo vim /etc/apt/sources.list.d/original.list ``` 在底⾏模式下,进⾏字符串替换,将⽂档中的 '[cn.archive.ubuntu.com](http://cn.archive.ubuntu.com/)' 替换[mirrors.aliyun.com](http://mirrors.aliyun.com/)',替换方式为示例第18行的写法。 ```cpp 1 # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to 2 # newer versions of the distribution. 3 deb http://cn.archive.ubuntu.com/ubuntu jammy main restricted 4 # deb-src http://cn.archive.ubuntu.com/ubuntu jammy main restricted 5 6 ## Major bug fix updates produced after the final release of the 7 ## distribution. 8 deb http://cn.archive.ubuntu.com/ubuntu jammy-updates main restricted 9 # deb-src http://cn.archive.ubuntu.com/ubuntu jammy-updates main restricted 10 11 ## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu 12 ## team. Also, please note that software in universe WILL NOT receive any 13 ## review or updates from the Ubuntu security team. 14 deb http://cn.archive.ubuntu.com/ubuntu jammy universe 15 # deb-src http://cn.archive.ubuntu.com/ubuntu jammy universe 16 deb http://cn.archive.ubuntu.com/ubuntu jammy-updates universe 17 # deb-src http://cn.archive.ubuntu.com/ubuntu jammy-updates universe :%s/cn.archive.ubuntu.com/mirrors.aliyun.com/g ``` 更改完毕后,更新apt的source list ```cpp dev@wei:~$ sudo apt update ``` ### 4.1.2 安装lrzsz传输工具 ```c dev@wei:~$ sudo apt-get install lrzsz ... dev@wei:~$ rz --version rz (GNU lrzsz) 0.12.21rc dev@wei:~$ sz --version sz (lrzsz) 0.12.21rc ``` 安装完毕后使用 rz --version 查看工具版本,正常显示则表示安装成功。 ### 4.1.3 安装gcc/g++编译器 ```c dev@wei:~$ sudo apt-get install gcc g++ ... dev@wei:~$ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. dev@wei:~$ g++ --version g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. dev@wei:~$ ``` 安装完毕后,查看版本,正常显示则安装成功。 ### 4.1.4 安装gdb调试器 ```c dev@wei:~$ sudo apt-get install gdb ... dev@wei:~$ gdb --version GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1 Copyright (C) 2022 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. dev@wei:~$ ``` 安装完毕后,查看版本,正常显示则安装成功。 ### 4.1.5 安装git工具 ```c dev@wei:~$ sudo apt-get install git ... dev@wei:~$ git --version git version 1.8.3.1 ``` ### 4.1.6 安装cmake项目构建工具 ```c dev@wei:~$ sudo apt-get install cmake ... dev@wei:~$ cmake --version cmake version 3.22.1 CMake suite maintained and supported by Kitware (kitware.com/cmake). dev@wei:~$ ``` 安装完毕后,查看版本,正常显示则安装成功。 ### 4.1.7 安装boost库 ```c dev@wei:~$ sudo apt-get install libboost-all-dev ... dev@wei:~$ dpkg -S /usr/include/boost/version.hpp libboost1.74-dev:amd64: /usr/include/boost/version.hpp dev@wei:~$ ``` 安装完毕后,查看版本,正常显示则安装成功。其实只要 '/usr/include/' 下有 'boost' 目录,且其中有头⽂件就表示安装成功了。 ### 4.1.8 安装jsoncpp库 ```c dev@wei:~$ sudo apt-get install libjsoncpp-dev ... dev@wei:~$ ls /usr/include/jsoncpp/json/ allocator.h assertions.h config.h forwards.h json_features.h json.h reader.h value.h version.h writer.h dev@wei:~$ dpkg -S /usr/include/jsoncpp/json/version.h libjsoncpp-dev:amd64: /usr/include/jsoncpp/json/version.h dev@wei:~$ ls /usr/lib/x86_64-linux-gnu/libjsoncpp* /usr/lib/x86_64-linux-gnu/libjsoncpp.so ``` 查看 '/usr/include' 下有 'jsoncpp' 目录,且其中包含有头⽂件, '/usr/lib/x86_64-linux-gnu/'下有对应的库⽂件就表示成功了。 ### 4.1.9 安装mysql数据库 #### 4.1.9.1 获取mysql-5.7安装源 ```c dev@wei:~$ wget http://repo.mysql.com/mysql-apt-config_0.8.12-1_all.deb ... dev@wei:~$ ``` #### 4.1.9.2 安装mysql官方源 ```c dev@wei:~$ sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb ... #1. 过程中出现apt源的安装选择,则选择 bionic 然后 OK #2. 过程中出现mysql源版本选择时,选择mysql-5.7,然后 OK ... ``` #### 4.1.9.3 更新apt源 ```cpp dev@wei:~$ sudo apt-get update dev@wei:~$ ``` #### 4.1.9.4 出现错误:E: The repository 'file:/cdrom jammy Release' no longer has a Release file ```c dev@wei:~$ sudo vi /etc/apt/sources.list deb [check-date=no] file:///cdrom jammy main restricted 删除这⾏内容 ``` #### 4.1.9.5 出现gpg-key过期的情况解决方案 ```c dev@wei:~$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 ``` #### 4.1.9.6 出现更新警告:Key is stored inlegacy trusted.gpg keyring(/etc/apt/trusted.gpg) 这个警告的意思是,需要在trusted.gpg.d/目录下查找GPG密钥,因此将密钥⽂件拷⻉过去即可。 ```c dev@wei:~$ sudo cp /etc/apt/trusted.gpg /etc/apt/trusted.gpg.d/ dev@wei:~$ sudo apt-get update ``` #### 4.1.9.7 查看当前可安装mysql版本: ```c dev@wei:~$ sudo apt-cache policy mysql-server mysql-server: Installed: (none) Candidate: 8.0.33-0ubuntu0.22.04.2 Version table: 8.0.33-0ubuntu0.22.04.2 500 500 http://cn.archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages 500 http://cn.archive.ubuntu.com/ubuntu jammy-security/main amd64 Packages 8.0.28-0ubuntu4 500 500 http://cn.archive.ubuntu.com/ubuntu jammy/main amd64 Packages 5.7.42-1ubuntu18.04 500 500 http://repo.mysql.com/apt/ubuntu bionic/mysql-5.7 amd64 Packages dev@wei:~$ ``` #### 4.1.9.8 安装5.7版本mysql服务及开发包: ```c dev@wei:~$ sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* libmysqlclient-dev=5.7* ``` #### 4.1.9.9 修改配置⽂件:/etc/mysql/my.cnf ```apl The MariaDB configuration file # # The MariaDB/MySQL tools read configuration files in the following order: # 0. "/etc/mysql/my.cnf" symlinks to this file, reason why all the rest isread. # 1. "/etc/mysql/mariadb.cnf" (this file) to set global defaults, # 2. "/etc/mysql/conf.d/*.cnf" to set global options. # 3. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options. # 4. "~/.my.cnf" to set user-specific options. # # If the same option is defined multiple times, the last one will apply. # # One can use all long options that the program supports. # Run program with --help to get a list of available options and with # --print-defaults to see which it would actually understand and use. # # If you are new to MariaDB, check out https://mariadb.com/kb/en/basic-mariadbarticles/ # # This group is read both by the client and the server # use it for options that affect everything # [client] default-character-set = utf8 [mysql] default-character-set = utf8 [mysqld] character-set-server = utf8 bind-address = 0.0.0.0 [client-server] # Port or socket location where to connect # port = 3306 socket = /run/mysqld/mysqld.sock # Import all .cnf files from configuration directory !includedir /etc/mysql/conf.d/ !includedir /etc/mysql/mariadb.conf.d/ ``` #### 4.1.9.10 启动mysql服务 ```c dev@wei:~$ sudo systemctl start mysql ``` #### 4.1.9.11 查看字符集 ```mysql dev@wei:~$ mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 5.7.42 MySQL Community Server (GPL) Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show variables like '%chara%'; +--------------------------+----------------------------+ | Variable_name | Value | +--------------------------+----------------------------+ | character_set_client | utf8 | | character_set_connection | utf8 | | character_set_database | utf8 | | character_set_filesystem | binary | | character_set_results | utf8 | | character_set_server | utf8 | | character_set_system | utf8 | | character_sets_dir | /usr/share/mysql/charsets/ | +--------------------------+----------------------------+ 8 rows in set (0.001 sec) mysql> ``` #### 4.1.9.12 设置/修改mysql密码,以及设置密码强度等级 先进性安全配置: ```cpp dev@wei:~$ sudo mysql_secure_installation Securing the MySQL server deployment. Enter password for user root: VALIDATE PASSWORD PLUGIN can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD plugin? Press y|Y for Yes, any other key for No: y There are three levels of password validation policy: LOW Length >= 8 MEDIUM Length >= 8, numeric, mixed case, and special characters STRONG Length >= 8, numeric, mixed case, special characters and dictionary Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1 Using existing password for root. Estimated strength of the password: 25 Change the password for root ? ((Press y|Y for Yes, any other key for No) : no ... skipping. By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. Remove anonymous users? (Press y|Y for Yes, any other key for No) : y Success. Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y Success. By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. Remove test database and access to it? (Press y|Y for Yes, any other key for No) - Dropping test database... Success. - Removing privileges on test database... Success. Reloading the privilege tables will ensure that all changes made so far will take effect immediately. Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y Success. All done! ``` 然后进⾏内部密码强度等级的进⼀步设置与查看。 ```mysql dev@wei:~$ mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 4 Server version: 5.7.42 MySQL Community Server (GPL) Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> set global validate_password_policy=0; Query OK, 0 rows affected (0.00 sec) mysql> set global validate_password_length=1; Query OK, 0 rows affected (0.00 sec) mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码'; Query OK, 0 rows affected (0.00 sec) mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.00 sec) ``` ### 4.1.10 安装websocketpp库 ```c dev@wei:~$ git clone https://github.com/zaphoyd/websocketpp.git Cloning into 'websocketpp'... remote: Enumerating objects: 12791, done. remote: Counting objects: 100% (2911/2911), done. remote: Compressing objects: 100% (393/393), done. remote: Total 12791 (delta 2790), reused 2519 (delta 2518), pack-reused 9880 Receiving objects: 100% (12791/12791), 8.16 MiB | 8.20 MiB/s, done. Resolving deltas: 100% (8101/8101), done. dev@wei:~$ ls websocketpp dev@wei:~$ cd websocketpp/ dev@wei:~/websocketpp$ mkdir build dev@wei:~/websocketpp$ cd build/ dev@wei:~/websocketpp/build$ cmake -DCMAKE_INSTALL_PREFIX=/usr .. dev@wei:~/websocketpp/build$ ls CMakeCache.txt CMakeFiles cmake_install.cmake Makefile websocketpp dev@wei:~/websocketpp/build$ sudo make install dev@wei:~/websocketpp/build$ ls /usr/include/websocketpp/ base64 common connection_base.hpp endpoint.hpp frame.hpp logger random sha1 utf8_validator.hpp client.hpp concurrency connection.hpp error.hpp http message_buffer roles transport utilities.hpp close.hpp config endpoint_base.hpp extensions impl processors server.hpp uri.hpp version.hpp dev@wei:~/websocketpp/build$ cd .. dev@wei:~/websocketpp$ ls build changelog.md cmake CMakeLists.txt COPYING docs Doxyfile examples readme.md roadmap.md SConstruct test tutorials websocketpp websocketppconfig.cmake.in dev@wei:~/websocketpp$ cd examples/echo_server dev@wei:~/websocketpp/examples/echo_server$ ls CMakeLists.txt echo_handler.hpp echo_server.cpp SConscript dev@wei:~/websocketpp/examples/echo_server$ g++ -std=c++11 echo_server.cpp -o echo_server -lpthread -lboost_system dev@wei:~/websocketpp/examples/echo_server$ ls CMakeLists.txt echo_handler.hpp echo_server echo_server.cpp SConscript dev@wei:~/websocketpp/examples/echo_server$ ``` 查看'/usr/include/'下拥有了'websocketpp'目录,且其中包含有头⽂件就表示安装成功了。 或者在'examples/echo_server/'中对样例进⾏编译,编译通过就没问题了~。~ ## 4.2 Centos-7.6环境搭建 ### 4.2.1 安装wget⼯具 ```c [wei@localhost ~]$ sudo yum install wget ``` ### 4.2.2 更换软件源 ```c [wei@localhost ~]$ sudo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak [wei@localhost ~]$ sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo [wei@localhost ~]$ sudo yum clean all Loaded plugins: fastestmirror Cleaning repos: base extras updates Cleaning up list of fastest mirrors [wei@localhost ~]$ sudo yum makecache ... ``` ### 4.2.3 安装scl软件源 ```c [wei@localhost ~]$ sudo yum install centos-release-scl-rh centos-release ``` ### 4.2.4 安装epel软件源 ```c [wei@localhost ~]$ sudo yum install epel-release ``` ### 4.2.5 安装lrzsz传输工具 ```c [wei@localhost ~]$ sudo yum install lrzsz ... [wei@localhost ~]$ rz --version rz (lrzsz) 0.12.20 ``` ### 4.2.6 安装⾼版本gcc/g++编译器 ```c [wei@localhost ~]$ sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++ ... [wei@localhost ~]$ echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc [wei@localhost ~]$ source ~/.bashrc [wei@localhost ~]$ g++ -v Using built-in specs. COLLECT_GCC=g++ COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-7/root/usr/libexec/gcc/x86_64-redhatlinux/7/lto-wrapper Target: x86_64-redhat-linux Configured with: ../configure --enable-bootstrap --enablelanguages=c,c++,fortran,lto --prefix=/opt/rh/devtoolset-7/root/usr -- mandir=/opt/rh/devtoolset-7/root/usr/share/man --infodir=/opt/rh/devtoolset- 7/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla -- enable-shared --enable-threads=posix --enable-checking=release --enablemultilib --with-system-zlib --enable-__cxa_atexit --disable-libunwindexceptions --enable-gnu-unique-object --enable-linker-build-id --with-gccmajor-version-only --enable-plugin --with-linker-hash-style=gnu --enable-initfini-array --with-default-libstdcxx-abi=gcc4-compatible --withisl=/builddir/build/BUILD/gcc-7.3.1-20180303/obj-x86_64-redhat-linux/islinstall --enable-libmpx --enable-gnu-indirect-function --with-tune=generic -- with-arch_32=i686 --build=x86_64-redhat-linux Thread model: posix gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC) ``` ### 4.2.7 安装gdb调试器 ```c [wei@localhost ~]$ sudo yum install gdb ... [wei@localhost ~]$ gdb --version GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7 Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-redhat-linux-gnu". For bug reporting instructions, please see: . ``` ### 4.2.8 安装git ```c [wei@localhost ~]$ sudo yum install git ... [wei@localhost ~]$ git --version git version 1.8.3.1 ``` ### 4.2.9 安装cmake ```c [wei@localhost ~]$ sudo yum install cmake ... [wei@localhost ~]$ cmake --version cmake version 2.8.12.2 ``` ### 4.2.10 安装boost库 ```c [wei@localhost ~]$ sudo yum install boost-devel.x86_64 ``` ### 4.2.11 安装Jsoncpp库 ```c [wei@node ~]$ sudo yum install jsoncpp-devel ... [wei@node ~]$ ls /usr/include/jsoncpp/json/ assertions.h config.h forwards.h reader.h version.h autolink.h features.h json.h value.h writer.h ``` ### 4.2.12 安装Mysql数据库服务及开发包 #### 4.2.12.1 获取mysql官⽅yum源 ```c [wei@cloud-8-7centos online-gobang-battle]$ wget http://repo.mysql.com/mysql57-communityrelease-el7-10.noarch.rpm ``` #### 4.2.12.2 安装mysql官⽅yum源 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo rpm -ivh mysql57-community-release-el7- 10.noarch.rpm ``` #### 4.2.12.3 安装Mysql数据库服务 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo yum install -y mysql-community-server ``` #### 4.2.12.4 出错解决 如果因为GPG KEY的过期导致安装失败 ```c [wei@cloud-8-7centos online-gobang-battle]$ GPG Keys are configured as: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql ``` 则执⾏以下指令,然后重新安装 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo rpm --import https://repo.mysql.com/RPMGPG-KEY-mysql-2022 ``` #### 4.2.12.5 安装Mysql开发包 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo yum install -y mysql-community-devel ``` #### 4.2.12.6 进⾏Mysql配置修改 1.配置'/etc/my.cnf'字符集 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo vim /etc/my.cnf ``` ```c # For advice on how to change settings please see # http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html [client] default-character-set=utf8 [mysql] default-character-set=utf8 [mysqld] character-set-server=utf8 # # Remove leading # and set to the amount of RAM for the most important data # cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%. # innodb_buffer_pool_size = 128M # # Remove leading # to turn on a very important data integrity option: logging # changes to the binary log between backups. # log_bin # # Remove leading # to set options mainly useful for reporting servers. # The server defaults are faster for transactions and fast SELECTs. # Adjust sizes as needed, experiment to find the optimal values. # join_buffer_size = 128M # sort_buffer_size = 2M # read_rnd_buffer_size = 2M datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock # Disabling symbolic-links is recommended to prevent assorted security risks symbolic-links=0 log-error=/var/log/mysqld.log pid-file=/var/run/mysqld/mysqld.pid ``` #### 4.2.12.7 启动Mysql服务 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo systemctl start mysqld [wei@cloud-8-7centos online-gobang-battle]$ sudo systemctl status mysqld ● mysqld.service - MySQL Server Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled) Active: active (running) since Mon 2023-04-17 17:54:00 CST; 9min ago Docs: man:mysqld(8) http://dev.mysql.com/doc/refman/en/using-systemd.html Process: 20047 ExecStart=/usr/sbin/mysqld --daemonize --pidfile=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS) Process: 19988 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS) Main PID: 20051 (mysqld) Tasks: 28 Memory: 189.2M CGroup: /system.slice/mysqld.service └─20051 /usr/sbin/mysqld --daemonize --pidfile=/var/run/mysqld/mysqld.pid Apr 17 17:53:59 VM-8-12-centos systemd[1]: Starting MySQL Server... Apr 17 17:54:00 VM-8-12-centos systemd[1]: Started MySQL Server. ``` 注意,如果启动的时候遇到了以下情况,输⼊系统的root管理员密码即可 ```c [wei@cloud-8-7centos online-gobang-battle]$ systemctl start mysqld ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units === Authentication is required to manage system services or units. Authenticating as: Password: ``` #### 4.2.12.8 获取Mysql临时密码 ```c [wei@cloud-8-7centos online-gobang-battle]$ sudo grep 'temporary password' /var/log/mysqld.l ``` #### 4.2.12.9 设置mysql数据库密码 ```mysql [wei@cloud-8-7centos online-gobang-battle]$ mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 4 Server version: 5.7.41 MySQL Community Server (GPL) Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> set global validate_password_policy=0; Query OK, 0 rows affected (0.00 sec) mysql> set global validate_password_length=1; Query OK, 0 rows affected (0.00 sec) mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'qwer@wu.888'; Query OK, 0 rows affected (0.00 sec) mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.00 sec) ``` #### 4.2.12.10 登录查看Mysql字符集是否正常 ```mysql [wei@cloud-8-7centos online-gobang-battle]$ mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 4 Server version: 5.7.41 MySQL Community Server (GPL) Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show variables like '%chara%'; +--------------------------+----------------------------+ | Variable_name | Value | +--------------------------+----------------------------+ | character_set_client | utf8 | --客⼾端使⽤的字符集 | character_set_connection | utf8 | --客⼾端连接时使⽤的字符集 | character_set_database | utf8 | --数据库创建默认字符集 | character_set_filesystem | binary | --⽂件系统编码格式 | character_set_results | utf8 | --服务器返回结果时的字符集 | character_set_server | utf8 | --存储系统元数据的字符集 | character_set_system | utf8 | --系统使⽤的编码格式,不影响 | character_sets_dir | /usr/share/mysql/charsets/ | +--------------------------+----------------------------+ 8 rows in set (0.00 sec) mysql> quit ``` ### 4.2.13 安装Websocketpp库 ```c [wei@localhost ~]$ git clone https://github.com/zaphoyd/websocketpp.git Cloning into 'websocketpp'... remote: Enumerating objects: 12791, done. remote: Counting objects: 100% (1549/1549), done. remote: Compressing objects: 100% (197/197), done. remote: Total 12791 (delta 1504), reused 1353 (delta 1352), pack-reused 11242 Receiving objects: 100% (12791/12791), 8.37 MiB | 1.47 MiB/s, done. Resolving deltas: 100% (7985/7985), done. ``` + 安装websocketpp ``` c [wei@localhost ~]$ ls websocketpp [wei@localhost ~]$ cd websocketpp/ [wei@localhost websocketpp]$ mkdir build [wei@localhost websocketpp]$ cd build [wei@localhost build]$ cmake -DCMAKE_INSTALL_PREFIX=/usr .. ... [wei@localhost build]$ sudo make install ... ``` + 验证websocketpp是否安装成功 ```c [wei@localhost build]$ cd ../examples/echo_server [wei@localhost echo_server]$ ls CMakeLists.txt echo_handler.hpp echo_server.cpp SConscript [wei@localhost echo_server]$ g++ -std=c++11 echo_server.cpp -o echo_server - lpthread -lboost_system [wei@localhost echo_server]$ ``` 编译成功,则表示安装成功了。 依赖的第三方库 # 五、知识点代码用例 ## 5.1 Websocketpp Websocket介绍 WebSocket是从HTML5开始⽀持的⼀种网页端和服务端保持长连接的消息推送机制。 + 传统的web程序都是属于"⼀问⼀答"的形式,即客户端给服务器发送了⼀个HTTP请求,服务器给客户端返回⼀个HTTP响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应。 + 像网页即时聊天或者我们做的五⼦棋游戏这样的程序都是非常依赖"消息推送"的,即需要服务器主动推动消息到客户端。如果只是使用原⽣的HTTP协议,要想实现消息推送⼀般需要通过"轮询"的方式实现,而轮询的成本比较⾼并且也不能及时的获取到消息的响应。 原理解析 WebSocket 协议本质上是⼀个基于 TCP 的协议。为了建立⼀个 WebSocket 连接,客户端浏览器首先要向服务器发起⼀个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了⼀些附加头信息,通过这个附加头信息完成握手过程并升级协议的过程。 ![image-20231102120431999](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311021204200.png) 具体协议升级的过程如下: ![image-20231102120452790](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311021204988.png) 报⽂格式: ![image-20231102120513471](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311021205597.png) 报⽂字段比较多,我们重点关注这几个字段: • 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协议的讲述非常清晰, 大家下去可以去看看。 [https://www.bilibili.com](http://www.bilibili.com/video/BV1684y1k7VP/?buvid=ZC4691C539D91BA74044)[om/video/BV1684y1k7VP/?buvid=ZC4691C539D91BA74044](http://www.bilibili.com/video/BV1684y1k7VP/?buvid=ZC4691C539D91BA74044)… Websocketpp介绍 WebSocketpp是⼀个跨平台的开源(BSD许可证)头部专⽤C++库,它实现了RFC6455(WebSocket协议)和RFC7692(WebSocketCompression Extensions)。它允许将WebSocket客户端和服务器功能集成到C++程序中。在最常见的配置中,全功能网络I/O由Asio⽹络库提供。 WebSocketpp的主要特性包括: • 事件驱动的接口 • ⽀持HTTP/HTTPS、WS/WSS、IPv6 • 灵活的依赖管理 — Boost库/C++11标准库 • 可移植性:Posix/Windows、32/64bit、Intel/ARM • 线程安全 WebSocketpp同时⽀持HTTP和Websocket两种网络协议, 比较适用于我们本次的项目, 所以我们选 用该库作为项目的依赖库用来搭建HTTP和WebSocket服务器。 下⾯是该项目的⼀些常用网站, 大家多去学习。 • github:https://github.com/zaphoyd/websocketpp • 用户手册: http://docs.websocketpp.org/ • 官网:http://www.zaphoyd.com/websocketpp **Websocketpp使用** websocketpp常用接⼝介绍: ```cpp namespace websocketpp { typedef lib::weak_ptr connection_hdl; template class endpoint : public config::socket_type { typedef lib::shared_ptr timer_ptr; typedef typename connection_type::ptr connection_ptr; typedef typename connection_type::message_ptr message_ptr; typedef lib::function open_handler; typedef lib::function close_handler; typedef lib::function http_handler; typedef lib::function 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 class server : public endpoint,config> { /*初始化并启动服务端监听连接的accept事件处理*/ void start_accept(); }; template 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_header = 0x8; static level const frame_payload = 0x10; static level const message_header = 0x20; static level const message_payload = 0x40; static level const endpoint = 0x80; static level const debug_handshake = 0x100; static level const debug_close = 0x200; static level const devel = 0x400; static level const app = 0x800; static level const http = 0x1000; static level const fail = 0x2000; static level const access_core = 0x00003003; static level const all = 0xffffffff; }; } namespace http { namespace status_code { enum value { uninitialized = 0, continue_code = 100, switching_protocols = 101, ok = 200, created = 201, accepted = 202, non_authoritative_information = 203, no_content = 204, reset_content = 205, partial_content = 206, multiple_choices = 300, moved_permanently = 301, found = 302, see_other = 303, not_modified = 304, use_proxy = 305, temporary_redirect = 307, bad_request = 400, unauthorized = 401, payment_required = 402, forbidden = 403, not_found = 404, method_not_allowed = 405, not_acceptable = 406, proxy_authentication_required = 407, request_timeout = 408, conflict = 409, gone = 410, length_required = 411, precondition_failed = 412, request_entity_too_large = 413, request_uri_too_long = 414, unsupported_media_type = 415, request_range_not_satisfiable = 416, expectation_failed = 417, im_a_teapot = 418, upgrade_required = 426, precondition_required = 428, too_many_requests = 429, request_header_fields_too_large = 431, internal_server_error = 500, not_implemented = 501, bad_gateway = 502, service_unavailable = 503, gateway_timeout = 504, http_version_not_supported = 505, not_extended = 510, network_authentication_required = 511 };}} namespace frame { namespace opcode { enum value { continuation = 0x0, text = 0x1, binary = 0x2, rsv3 = 0x3, rsv4 = 0x4, rsv5 = 0x5, rsv6 = 0x6, rsv7 = 0x7, close = 0x8, ping = 0x9, pong = 0xA, control_rsvb = 0xB, control_rsvc = 0xC, control_rsvd = 0xD, control_rsve = 0xE, control_rsvf = 0xF, };}} } ``` **Simple http/websocket服务器** 使⽤Websocketpp实现⼀个简单的http和websocket服务器 ```cpp #include #include #include #include typedef websocketpp::server wsserver_t; void print(const std::string &str) { std::cout << str << std::endl; } void http_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) { //给客户端返回一个hello world的页面 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 = "

Hello websocketpp

"; conn->set_body(body); conn->append_header("Content-Type", "text/html"); //conn->set_body(conn->get_request_body()); conn->set_status(websocketpp::http::status_code::ok); // wsserver_t::timer_ptr tp = srv->set_timer(5000, std::bind(print, "bitejiuyeke")); // tp->cancel();//定时任务的取消,会导致定时任务立即被执行 } void wsopen_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) { std::cout << "websocket握手成功!!\n"; } void wsclose_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) { std::cout << "websocket连接断开!!\n"; } void wsmsg_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 rsp = "client say: " + msg->get_payload(); conn->send(rsp, websocketpp::frame::opcode::text); } int main() { //1. 实例化server对象 wsserver_t wssrv; //2. 设置日志等级 wssrv.set_access_channels(websocketpp::log::alevel::none); //3. 初始化asio调度器 wssrv.init_asio(); wssrv.set_reuse_addr(true); //4. 设置回调函数 wssrv.set_http_handler(std::bind(http_callback, &wssrv, std::placeholders::_1)); wssrv.set_open_handler(std::bind(wsopen_callback, &wssrv, std::placeholders::_1)); wssrv.set_close_handler(std::bind(wsclose_callback, &wssrv, std::placeholders::_1)); wssrv.set_message_handler(std::bind(wsmsg_callback, &wssrv, std::placeholders::_1, std::placeholders::_2)); //5. 设置监听端口 wssrv.listen(8088); //6. 开始获取新连接 wssrv.start_accept(); //7. 启动服务器 wssrv.run(); return 0; } ``` **Http客户端** 使用浏览器作为http客户端即可,访问服务器的8088端⼝。 ![image-20231116162441612](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311161627242.png) **WS客户端** ```cpp Test Websocket ``` 在控制台中我们可以看到连接建立、客户端和服务器通信以及断开连接的过程(关闭服务器就会看到断开连接的现象) ![image-20231116235535899](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311162355996.png) ## 5.2 JsonCpp使用 **Json数据格式** Json 是⼀种数据交换格式,它采用完全独立于编程语⾔的⽂本格式来存储和表示数据。 例如:我们想表示⼀个同学的学⽣信息 • C 代码表示 ```cpp char *name = "xx"; int age = 18; float score[3] = {88.5, 99, 58}; ``` • Json 表示 ```cpp { "姓名" : "xx", "年龄" : 18, "成绩" : [88.5, 99, 58] } [ {"姓名":"小明", "年龄":18, "成绩":[23, 65, 78]}, {"姓名":"小红", "年龄":19, "成绩":[88, 95, 78]} ] ``` Json 的数据类型包括对象,数组,字符串,数字等。 • 对象:使用花括号 {} 括起来的表示⼀个对象 • 数组:使用中括号 [] 括起来的表示⼀个数组 • 字符串:使用常规双引号 "" 括起来的表示⼀个字符串 • 数字:包括整形和浮点型,直接使用 **JsonCpp介绍** Jsoncpp 库主要是用于实现 Json 格式数据的序列化和反序列化,它实现了将多个数据对象组织成 为 json 格式字符串,以及将 Json 格式字符串解析得到多个数据对象的功能。 先看⼀下 Json 数据对象类的表示 ```cpp class Json::Value{ Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过 Value& operator[](const std::string& key);//简单的⽅式完成 val["name"] ="xx"; Value& operator[](const char* key); Value removeMember(const char* key);//移除元素 const Value& operator[](ArrayIndex index) const; //val["score"][0] Value& append(const Value& value);//添加数组元素val["score"].append(88); ArrayIndex size() const;//获取数组元素个数 val["score"].size(); bool isNull(); //⽤于判断是否存在某个字段 std::string asString() const;//转string string name =val["name"].asString(); const char* asCString() const;//转char* char *name =val["name"].asCString(); Int asInt() const;//转int int age = val["age"].asInt(); float asFloat() const;//转float float weight = val["weight"].asFloat(); bool asBool() const;//转 bool bool ok = val["ok"].asBool(); }; ``` **Jsoncpp 库主要借助三个类以及其对应的少量成员函数完成序列化及反序列化** • 序列化接⼝ ```cpp 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; } ``` • 反序列化接⼝ ```cpp 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功能代码用例编写** ```cpp #include #include #include #include #include int main() { // 序列化 Json::Value stu; stu["name"] = "zhangsan"; stu["age"] = 19; stu["socre"].append(77.5); stu["socre"].append(88); stu["socre"].append(99.5); Json::StreamWriterBuilder swb; std::unique_ptr sw(swb.newStreamWriter()); std::stringstream ss; int ret = sw->write(stu, &ss); if (ret != 0) { std::cout << "Serialize failed!\n"; return -1; } std::cout << "序列化结果:\n" << ss.str() << std::endl; // 反序列化 std::string str = ss.str(); Json::Value root; Json::CharReaderBuilder crb; std::unique_ptr cr(crb.newCharReader()); bool ret1 = cr->parse(str.c_str(), str.c_str() + str.size(), &root, nullptr); if (!ret1) { std::cout << "UnSerialize failed!" << std::endl; return -1; } std::cout << "反序列化结果:\n" << "name:" << root["name"].asString() << "\n" << "age:" << root["age"].asInt() << "\n" << "socre:" << root["socre"][0].asFloat() << " " << root["socre"][1].asInt() << " " << root["socre"][2].asFloat() << "\n"; return 0; } ``` 编译运⾏程序查看序列化和反序列化结果 ```cpp [wei@node test_jsoncpp]$ g++ test_json.cpp -o test_json -ljsoncpp -std=c++11 [wei@node test_jsoncpp]$ ./test_json 序列化结果: { "age" : 19, "name" : "zhangsan", "socre" : [ 77.5, 88, 99.5 ] } 反序列化结果: name:zhangsan age:19 socre:77.5 88 99.5 ``` 封装Json⼯具类 ```cpp class json_util { public: // 序列化: Json对象 -> 字符串 // 输⼊输出型参数 // root输⼊参数:表⽰要序列化的json对象 // str输出参数: 表⽰序列化之后的字符串 static bool serialize(const Json::Value &root, std::string &str) { Json::StreamWriterBuilder swb; std::unique_ptr sw(swb.newStreamWriter()); std::stringstream ss; int ret = sw->write(root, &ss); if (ret != 0) { std::cout << "Serialize failed!" << std::endl; return false; } str = ss.str(); return true; } // 反序列化: 字符串 ->Json对象 // 输⼊输出型参数 // str输⼊参数: 表⽰需要反序列化的字符串 // root输出参数:表⽰反序列化后的json对象 static bool unserialize(const std::string &str, Json::Value &root) { Json::CharReaderBuilder crb; std::unique_ptr cr(crb.newCharReader()); bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, nullptr); if (!ret) { std::cout << "UnSerialize failed!" << std::endl; return false; } return true; } }; ``` ## 5.3 MySQL API MySQL API介绍 • MySQL 是 C/S 模式, C API 其实就是⼀个 MySQL 客户端,提供⼀种用 C 语⾔代码操作数据 库的流程 • 课堂上同学们已经学习了 MySQL 数据库的学习,在这⾥我们主要介绍⼀下 MySQL 的 C API 接⼝ ```cpp // Mysql操作句柄初始化 // 参数说明: // mysql为空则动态申请句柄空间进⾏初始化 // 返回值: 成功返回句柄指针, 失败返回NULL MYSQL *mysql_init(MYSQL *mysql); // 连接mysql服务器 // 参数说明: // mysql--初始化完成的句柄 // host---连接的mysql服务器的地址 // user---连接的服务器的⽤⼾名 // passwd-连接的服务器的密码 // db ----默认选择的数据库名称 // port---连接的服务器的端⼝: 默认0是3306端⼝ // unix_socket---通信管道⽂件或者socket⽂件,通常置NULL // client_flag---客⼾端标志位,通常置0 // 返回值:成功返回句柄指针,失败返回NULL MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd,const char *db, unsigned intport, const char *unix_socket, unsigned long client_flag); // 设置当前客⼾端的字符集 // 参数说明: // mysql--初始化完成的句柄 // csname--字符集名称,通常:"utf8" // 返回值:成功返回0, 失败返回⾮0 int mysql_set_character_set(MYSQL *mysql, const char *csname) // 选择操作的数据库 // 参数说明: // mysql--初始化完成的句柄 // db-----要切换选择的数据库名称 // 返回值:成功返回0, 失败返回⾮0 int mysql_select_db(MYSQL *mysql, const char *db) // 执⾏sql语句 // 参数说明: // mysql--初始化完成的句柄 // stmt_str--要执⾏的sql语句 // 返回值:成功返回0, 失败返回⾮0 int mysql_query(MYSQL *mysql, const char *stmt_str) // 保存查询结果到本地 // 参数说明: // mysql--初始化完成的句柄 // 返回值:成功返回结果集的指针, 失败返回NULL MYSQL_RES *mysql_store_result(MYSQL *mysql) // 获取结果集中的⾏数 // 参数说明: // result--保存到本地的结果集地址 // 返回值:结果集中数据的条数 uint64_t mysql_num_rows(MYSQL_RES *result); // 获取结果集中的列数 // 参数说明: // result--保存到本地的结果集地址 // 返回值:结果集中每⼀条数据的列数 unsigned int mysql_num_fields(MYSQL_RES *result) // 遍历结果集, 并且这个接⼝会保存当前读取结果位置,每次获取的都是下⼀条数据 // 参数说明: // result--保存到本地的结果集地址 // 返回值:实际上是⼀个char **的指针,将每⼀条数据做成了字符串指针数组 // row[0]-第0列 row[1]-第1列 ... MYSQL_ROW mysql_fetch_row(MYSQL_RES *result) // 释放结果集 // 参数说明: // result--保存到本地的结果集地址 void mysql_free_result(MYSQL_RES *result) // 关闭数据库客⼾端连接,销毁句柄 // 参数说明: // mysql--初始化完成的句柄 void mysql_close(MYSQL *mysql) // 获取mysql接⼝执⾏错误原因 // 参数说明: // mysql--初始化完成的句柄 const char *mysql_error(MYSQL *mysql) ``` MySQL API使用 下⾯我们使用 C API 来实现 MySQL 的增删改查操作 • 创建测试数据库 ```mysql create database if not exists test_db; use test_db; create table stu( id int primary key auto_increment, -- 学⽣id age int, -- 学⽣年龄 name varchar(32) -- 学⽣姓名 ); ``` 连接MySQL服务,进⼊shell并执⾏sql语句 ```mysql [wei@node test_mysql]$ mysql -uroot -p123456 MariaDB [(none)]> create database if not exists test_db; Query OK, 1 row affected (0.01 sec) MariaDB [(none)]> use test_db; Database changed MariaDB [test_db]> create table stu( -> id int primary key auto_increment, -- 学⽣id -> age int, -- 学⽣年龄 -> name varchar(32) -- 学⽣姓名 -> ); Query OK, 0 rows affected (0.02 sec) ``` • 实现增删改查操作 ```cpp #include #include #include #include #include #define HOST "127.0.0.1" #define USER "root" #define PASSWD "123456" #define DBNAME "test_db" void add(MYSQL *mysql) { char *sql = "insert into stu values(null, 18, '张三'), (null, 17, '李四');"; int ret = mysql_query(mysql, sql); if (ret != 0) { printf("mysql query error:%s\n", mysql_error(mysql)); return; } return; } void del(MYSQL *mysql) { char *sql = "delete from stu where name='张三';"; int ret = mysql_query(mysql, sql); if (ret != 0) { printf("mysql query error:%s\n", mysql_error(mysql)); return; } return; } void mod(MYSQL *mysql) { char *sql = "update stu set age=15 where name='张三';"; int ret = mysql_query(mysql, sql); if (ret != 0) { printf("mysql query error:%s\n", mysql_error(mysql)); return; } return; } void get(MYSQL *mysql) { char *sql = "select * from stu;"; int ret = mysql_query(mysql, sql); if (ret != 0) { printf("mysql query error:%s\n", mysql_error(mysql)); return ; } MYSQL_RES *res = mysql_store_result(mysql); if (res == NULL) { printf("mysql store result error:%s\n", mysql_error(mysql)); return ; } int row = mysql_num_rows(res); int col = mysql_num_fields(res); printf("%10s%10s%10s\n", "ID", "年龄", "姓名"); for (int i = 0; i < row; i++) { MYSQL_ROW row_data = mysql_fetch_row(res); for (int i = 0; i < col; i++) { printf("%10s", row_data[i]); } printf("\n"); } mysql_free_result(res); return ; } int main() { MYSQL *mysql = mysql_init(NULL); if (mysql == NULL) { printf("init mysql handle failed!\n"); return -1; } if (mysql_real_connect(mysql, HOST, USER, PASSWD, DBNAME, 0, NULL, 0) == NULL) { printf("mysql connect error:%s\n", mysql_error(mysql)); return -1; } mysql_set_character_set(mysql, "utf8"); printf("===================== add =========================\n"); add(mysql); get(mysql); printf("===================== mod =========================\n"); mod(mysql); get(mysql); printf("===================== del =========================\n"); del(mysql); get(mysql); mysql_close(mysql); return 0; } ``` • 验证结果 ```cpp [wei@node test_mysql]$ g++ test_mysql.cpp -o test_mysql -L/usr/lib64/mysql -lmys [wei@node test_mysql]$ ./test_mysql ===================== add ========================= ID 年龄 姓名 11 18 张三 12 17 李四 ===================== mod ========================= ID 年龄 姓名 11 15 张三 12 17 李四 ===================== del ========================= ID 年龄 姓名 12 17 李四 ``` 封装MySQL⼯具类 ```cpp class mysql_util { public: // 创建mysql连接 static MYSQL *mysql_create(const std::string &host, const std::string &user, const std::string &pass, const std::string &name, uint16_t port) { // 初始化mysql句柄 MYSQL *mysql = mysql_init(nullptr); if (mysql == nullptr) { std::cout << "Init mysql instance failed!" << std::endl; return nullptr; } // 连接mysql服务 if (mysql_real_connect(mysql, host.c_str(), user.c_str(), pass.c_str(), name.c_str(), port, nullptr, 0) == nullptr) { std::cout << "Connect mysql server failed!" << std::endl; std::cout << mysql_error(mysql) << std::endl; mysql_close(mysql); return nullptr; } // 设置字符集为utf-8 mysql_set_character_set(mysql, "utf8"); return mysql; } // 关闭mysql连接 static void mysql_destroy(MYSQL *mysql) { if (mysql != nullptr) { mysql_close(mysql); } return; } // 执⾏sql语句 static bool mysql_exec(MYSQL *mysql, const std::string &sql) { int ret = mysql_query(mysql, sql.c_str()); if (ret != 0) { std::cout << sql << std::endl; std::cout << mysql_error(mysql) << std::endl; return false; } return true; } }; ``` ## 5.4 前端知识介绍 参考比特《前端扫盲》课件 # 六、项目结构设计 ## 6.1 项目模块划分说明 项目的实现,咱们将其划分为三个大模块来进⾏: • 数据管理模块:基于Mysql数据库进⾏用户数据的管理 • 前端界⾯模块:基于JS实现前端页面(注册,登录,游戏大厅,游戏房间)的动态控制以及与服务器 的通信。 • 业务处理模块:搭建WebSocket服务器与客户端进⾏通信,接收请求并进⾏业务处理。 在这⾥回顾⼀下我们要实现的项目功能,我们要实现的是⼀个在线五⼦棋对战服务器,提供用户通过浏览器进⾏用户注册,登录,以及实时匹配,对战,聊天等功能。 而如果要实现这些功能,那么就需要对业务处理模块再次进⾏细分为多个模块来实现各个功能。 ## 6.2 业务处理模块的子模块划分 • 网络通信模块:基于websocketpp库实现Http&WebSocket服务器的搭建,提供网络通信功能。 • 会话管理模块:对客户端的连接进⾏cookie&session管理,实现http短连接时客户端⾝份识别功能。 • 在线管理模块:对进⼊游戏大厅与游戏房间中用户进⾏管理,提供用户是否在线以及获取用户连接的功能。 • 房间管理模块:为匹配成功的用户创建对战房间,提供实时的五⼦棋对战与聊天业务功能。 • 用户匹配模块:根据天梯分数不同进⾏不同层次的玩家匹配,为匹配成功的玩家创建房间并加⼊房间。 ## 6.3 项目流程图 ### 6.3.1 玩家用户角度流程图 ![img](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311021210307.png) ### 6.3.2 服务器流程结构图 ![img](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311021210211.png) # 七、实用工具类模块代码实现 实用工具类模块主要是负责提前实现⼀些项目中会用到的边缘功能代码,提前实现好了就可以在项目中用到的时候直接使用了。 ## 7.1 日志宏封装 ```cpp #define INF 0 #define DBG 1 #define ERR 2 #define LOG_LEVEL DBG #define LOG(level, format, ...) do{\ if (level < LOG_LEVEL) break;\ time_t t = time(NULL);\ struct tm *ltm = localtime(&t);\ char tmp[32] = {0};\ strftime(tmp, 31, "%H:%M:%S", ltm);\ fprintf(stdout, "[%p %s %s:%d] " format "\n", (void*)pthread_self(), tmp, __FILE__, __LINE__, ##__VA_ARGS__);\ }while(0) #define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__) #define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__) #define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__) ``` ## 7.2 Mysql-API封装 ```cpp class mysql_util { public: static MYSQL *mysql_create( const std::string &host, const std::string &user, const std::string &pass, const std::string &db, int port) { MYSQL *mysql = mysql_init(NULL); if (mysql == NULL) { ERR_LOG("mysql init failed!"); return NULL; } if (mysql_real_connect(mysql, host.c_str(), user.c_str(), pass.c_str(), db.c_str(), port, NULL, 0) == NULL) { ERR_LOG("mysql connect server failed! %s", mysql_error(mysql)); mysql_close(mysql); return NULL; } if (mysql_set_character_set(mysql, "utf8mb4") != 0) { ERR_LOG("mysql set character failed!"); mysql_close(mysql); return NULL; } return mysql; } static void mysql_release(MYSQL *mysql) { if (mysql == NULL) { return; } mysql_close(mysql); return ; } static bool mysql_exec(MYSQL *mysql, const std::string &sql) { if (mysql_query(mysql, sql.c_str()) != 0) { ERR_LOG("SQL: %s", sql.c_str()); ERR_LOG("ERR: %s", mysql_error(mysql)); return false; } return true; } }; ``` ## 7.3 Jsoncpp-API封装 ```cpp class json_util { public: static bool serialize(const Json::Value &value, std::string &str) { Json::StreamWriterBuilder swb; std::unique_ptr sw(swb.newStreamWriter()); std::stringstream ss; int ret = sw->write(value, &ss); if (ret != 0) { std::cout << "json serialize failed!" << std::endl; return false; } str = ss.str(); return true; } static bool unserialize(const std::string &str, Json::Value &value) { Json::CharReaderBuilder crb; std::unique_ptr cr(crb.newCharReader()); bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &value, nullptr); if (!ret) { ERR_LOG("json unserialize failed!"); return false; } return true; } }; ``` ## 7.4 String-Split封装 ```cpp class string_util { public: static int split(const std::string &in, const std::string &sep, std::vector &arry) { arry.clear(); size_t pos, idx = 0; while(idx < in.size()) { pos = in.find(sep, idx); if (pos == std::string::npos) { arry.push_back(in.substr(idx)); break; } if (pos != idx) { arry.push_back(in.substr(idx, pos - idx)); } idx = pos + sep.size(); } return arry.size(); } }; ``` ## 7.5 File-read封装 ```cpp class file_util { public: static bool read(const std::string &filename, std::string &body) { std::ifstream file; // 打开⽂件 file.open(filename.c_str(), std::ios::in | std::ios::binary); if (!file) { std::cout << filename << " Open failed!" << std::endl; return false; } // 计算⽂件⼤小 file.seekg(0, std::ios::end); body.resize(file.tellg()); file.seekg(0, std::ios::beg); file.read(&body[0], body.size()); if (file.good() == false) { std::cout << filename << " Read failed!" << std::endl; file.close(); return false; } file.close(); return true; } }; ``` # 八、数据管理模块实现 数据管理模块主要负责对于数据库中数据进⾏统⼀的增删改查管理,其他模块要对数据操作都必须通过数据管理模块完成。 ## 8.1 数据库设计 创建user表,用来表示用户信息及积分信息 • 用户信息,用来实现登录、注册、游戏对战数据管理等功能 • 积分信息,用来实现匹配功能 ```mysql create database if not exists online_gobang; use online_gobang; create table if not exists user ( id int primary key auto_increment, username varchar(32), password varchar(32), score int, total_count int, win_count int ); -- 构造⼏个测试⽤⼾数据 insert into user values(null, 'xiaobai', MD5('123'), 1000, 0, 0); insert into user values(null, 'xiaohei', MD5('123'), 1000, 0, 0); ``` 验证数据库是否创建成功 ```mysql MariaDB [(none)]> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | online_gobang | | performance_schema | | sys | +--------------------+ 6 rows in set (0.000 sec) MariaDB [(none)]> use online_gobang; Database changed MariaDB [online_gobang]> show tables; +-------------------------+ | Tables_in_online_gobang | +-------------------------+ | user | +-------------------------+ 1 row in set (0.000 sec) MariaDB [online_gobang]> select * from user; +----+----------+----------------------------------+-------+-------------+-----------+ | id | username | password | score | total_count |win_count | +----+----------+----------------------------------+-------+-------------+-----------+ | 1 | xiaobai | 202cb962ac59075b964b07152d234b70 | 1030 | 1 | 1 | | 2 | xiaohei | 202cb962ac59075b964b07152d234b70 | 970 | 1 | 0 | +----+----------+----------------------------------+-------+-------------+-----------+ 2 rows in set (0.000 sec) MariaDB [online_gobang]> ``` ## 8.2 创建user_table类 数据库中有可能存在很多张表,每张表中管理的数据⼜有不同,要进⾏的数据操作也各不相同,因此 我们可以为每⼀张表中的数据操作都设计⼀个类,通过类实例化的对象来访问这张数据库表中的数 据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。 创建user_table类,该类的作用是负责通过 MySQL 接⼝管理用户数据。主要提供了四个方法: • select_by_name:根据用户名查找用户信息,用于实现登录功能 • insert:新增用户,用户实现注册功能 • login:登录验证,并获取完整的用户信息 • win:⽤于给获胜玩家修改分数 • lose:⽤户给失败玩家修改分数 ```cpp #ifndef __M_DB_H__ #define __M_DB_H__ #include "util.hpp" #include #include class user_table{ private: MYSQL *_mysql; //mysql操作句柄 std::mutex _mutex;//互斥锁保护数据库的访问操作 public: user_table(const std::string &host, const std::string &username, const std::string &password, const std::string &dbname, uint16_t port = 3306) { _mysql = mysql_util::mysql_create(host, username, password, dbname, port); assert(_mysql != NULL); } ~user_table() { mysql_util::mysql_destroy(_mysql); _mysql = NULL; } //注册时新增用户 bool insert(Json::Value &user) { #define INSERT_USER "insert user values(null, '%s', password('%s'), 1000, 0, 0);" // sprintf(void *buf, char *format, ...) if (user["password"].isNull() || user["username"].isNull()) { DLOG("INPUT PASSWORD OR USERNAME"); return false; } char sql[4096] = {0}; sprintf(sql, INSERT_USER, user["username"].asCString(), user["password"].asCString()); bool ret = mysql_util::mysql_exec(_mysql, sql); if (ret == false) { DLOG("insert user info failed!!\n"); return false; } return true; } //登录验证,并返回详细的用户信息 bool login(Json::Value &user) { if (user["password"].isNull() || user["username"].isNull()) { DLOG("INPUT PASSWORD OR USERNAME"); return false; } //以用户名和密码共同作为查询过滤条件,查询到数据则表示用户名密码一致,没有信息则用户名密码错误 #define LOGIN_USER "select id, score, total_count, win_count from user where username='%s' and password=password('%s');" char sql[4096] = {0}; sprintf(sql, LOGIN_USER, user["username"].asCString(), user["password"].asCString()); MYSQL_RES *res = NULL; { std::unique_lock lock(_mutex); bool ret = mysql_util::mysql_exec(_mysql, sql); if (ret == false) { DLOG("user login failed!!\n"); return false; } //按理说要么有数据,要么没有数据,就算有数据也只能有一条数据 res = mysql_store_result(_mysql); if (res == NULL) { DLOG("have no login user info!!"); return false; } } int row_num = mysql_num_rows(res); if (row_num != 1) { DLOG("the user information queried is not unique!!"); return false; } MYSQL_ROW row = mysql_fetch_row(res); user["id"] = (Json::UInt64)std::stol(row[0]); user["score"] = (Json::UInt64)std::stol(row[1]); user["total_count"] = std::stoi(row[2]); user["win_count"] = std::stoi(row[3]); mysql_free_result(res); return true; } // 通过用户名获取用户信息 bool select_by_name(const std::string &name, Json::Value &user) { #define USER_BY_NAME "select id, score, total_count, win_count from user where username='%s';" char sql[4096] = {0}; sprintf(sql, USER_BY_NAME, name.c_str()); MYSQL_RES *res = NULL; { std::unique_lock lock(_mutex); bool ret = mysql_util::mysql_exec(_mysql, sql); if (ret == false) { DLOG("get user by name failed!!\n"); return false; } //按理说要么有数据,要么没有数据,就算有数据也只能有一条数据 res = mysql_store_result(_mysql); if (res == NULL) { DLOG("have no user info!!"); return false; } } int row_num = mysql_num_rows(res); if (row_num != 1) { DLOG("the user information queried is not unique!!"); return false; } MYSQL_ROW row = mysql_fetch_row(res); user["id"] = (Json::UInt64)std::stol(row[0]); user["username"] = name; 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(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 = NULL; { std::unique_lock lock(_mutex); bool ret = mysql_util::mysql_exec(_mysql, sql); if (ret == false) { DLOG("get user by id failed!!\n"); return false; } //按理说要么有数据,要么没有数据,就算有数据也只能有一条数据 res = mysql_store_result(_mysql); if (res == NULL) { DLOG("have no user info!!"); return false; } } int row_num = mysql_num_rows(res); if (row_num != 1) { DLOG("the user information queried is not unique!!"); return false; } MYSQL_ROW row = mysql_fetch_row(res); user["id"] = (Json::UInt64)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(uint64_t id) { #define USER_WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id=%d;" char sql[4096] = {0}; sprintf(sql, USER_WIN, id); bool ret = mysql_util::mysql_exec(_mysql, sql); if (ret == false) { DLOG("update win user info failed!!\n"); return false; } return true; } //失败时天梯分数减少30,战斗场次增加1,其他不变 bool lose(uint64_t id) { #define USER_LOSE "update user set score=score-30, total_count=total_count+1 where id=%d;" char sql[4096] = {0}; sprintf(sql, USER_LOSE, id); bool ret = mysql_util::mysql_exec(_mysql, sql); if (ret == false) { DLOG("update lose user info failed!!\n"); return false; } return true; } }; #endif ``` # 九、在线用户管理模块实现 在线用户管理,是对于当前游戏大厅和游戏房间中的用户进⾏管理,主要是建立起用户与Socket连接 的映射关系,这个模块具有两个功能: 1. 能够让程序中根据用户信息,进而找到能够与用户客户端进⾏通信的Socket连接,进而实现与客户端的通信。 2. 判断⼀个用户是否在线,或者判断用户是否已经掉线 ```cpp #ifndef __M_ONLINE_H__ #define __M_ONLINE_H__ #include "util.hpp" #include #include class online_manager{ private: std::mutex _mutex; //用于建立游戏大厅用户的用户ID与通信连接的关系 std::unordered_map _hall_user; //用于建立游戏房间用户的用户ID与通信连接的关系 std::unordered_map _room_user; public: //websocket连接建立的时候才会加入游戏大厅&游戏房间在线用户管理 void enter_game_hall(uint64_t uid, wsserver_t::connection_ptr &conn) { std::unique_lock 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 lock(_mutex); _room_user.insert(std::make_pair(uid, conn)); } //websocket连接断开的时候,才会移除游戏大厅&游戏房间在线用户管理 void exit_game_hall(uint64_t uid) { std::unique_lock lock(_mutex); _hall_user.erase(uid); } void exit_game_room(uint64_t uid) { std::unique_lock lock(_mutex); _room_user.erase(uid); } //判断当前指定用户是否在游戏大厅/游戏房间 bool is_in_game_hall(uint64_t uid) { std::unique_lock lock(_mutex); auto it = _hall_user.find(uid); if (it == _hall_user.end()) { return false; } return true; } bool is_in_game_room(uint64_t uid) { std::unique_lock lock(_mutex); auto it = _room_user.find(uid); if (it == _room_user.end()) { return false; } return true; } //通过用户ID在游戏大厅/游戏房间用户管理中获取对应的通信连接 wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid) { std::unique_lock 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 lock(_mutex); auto it = _room_user.find(uid); if (it == _room_user.end()) { return wsserver_t::connection_ptr(); } return it->second; } }; #endif ``` # 十、游戏房间管理模块 ## 10.1 房间类实现 首先,需要设计⼀个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建立⼀个小范围的关联关系,⼀个房间中任意⼀个用户发⽣的任何动作,都会被⼴播给房间中的其他用户。 而房间中的动作主要包含两类: 1. 棋局对战 2. 实时聊天 ```cpp #ifndef __M_ROOM_H__ #define __M_ROOM_H__ #include "util.hpp" #include "logger.hpp" #include "online.hpp" #include "db.hpp" #define BOARD_ROW 15 #define BOARD_COL 15 #define CHESS_WHITE 1 #define CHESS_BLACK 2 typedef enum { GAME_START, GAME_OVER }room_statu; class room { private: uint64_t _room_id; room_statu _statu; int _player_count; uint64_t _white_id; uint64_t _black_id; user_table *_tb_user; online_manager *_online_user; std::vector> _board; private: bool five(int row, int col, int row_off, int col_off, int color) { //row和col是下棋位置, row_off和col_off是偏移量,也是方向 int count = 1; int search_row = row + row_off; int search_col = col + col_off; while(search_row >= 0 && search_row < BOARD_ROW && search_col >= 0 && search_col < BOARD_COL && _board[search_row][search_col] == color) { //同色棋子数量++ count++; //检索位置继续向后偏移 search_row += row_off; search_col += col_off; } search_row = row - row_off; search_col = col - col_off; while(search_row >= 0 && search_row < BOARD_ROW && search_col >= 0 && search_col < BOARD_COL && _board[search_row][search_col] == color) { //同色棋子数量++ count++; //检索位置继续向后偏移 search_row -= row_off; search_col -= col_off; } return (count >= 5); } uint64_t check_win(int row, int col, int color) { // 从下棋位置的四个不同方向上检测是否出现了5个及以上相同颜色的棋子(横行,纵列,正斜,反斜) if (five(row, col, 0, 1, color) || five(row, col, 1, 0, color) || five(row, col, -1, 1, color)|| five(row, col, -1, -1, color)) { //任意一个方向上出现了true也就是五星连珠,则设置返回值 return color == CHESS_WHITE ? _white_id : _black_id; } return 0; } public: room(uint64_t room_id, user_table *tb_user, online_manager *online_user): _room_id(room_id), _statu(GAME_START), _player_count(0), _tb_user(tb_user), _online_user(online_user), _board(BOARD_ROW, std::vector(BOARD_COL, 0)){ DLOG("%lu 房间创建成功!!", _room_id); } ~room() { DLOG("%lu 房间销毁成功!!", _room_id); } uint64_t id() { return _room_id; } room_statu statu() { return _statu; } int player_count() { return _player_count; } void add_white_user(uint64_t uid) { _white_id = uid; _player_count++; } void add_black_user(uint64_t uid) { _black_id = uid; _player_count++; } uint64_t get_white_user() { return _white_id; } uint64_t get_black_user() { return _black_id; } /*处理下棋动作*/ Json::Value handle_chess(Json::Value &req) { Json::Value json_resp = req; // 2. 判断房间中两个玩家是否都在线,任意一个不在线,就是另一方胜利。 int chess_row = req["row"].asInt(); int chess_col = req["col"].asInt(); uint64_t cur_uid = req["uid"].asUInt64(); if (_online_user->is_in_game_room(_white_id) == false) { json_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; } // 3. 获取走棋位置,判断当前走棋是否合理(位置是否已经被占用) if (_board[chess_row][chess_col] != 0) { json_resp["result"] = false; json_resp["reason"] = "当前位置已经有了其他棋子!"; return json_resp; } int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK; _board[chess_row][chess_col] = cur_color; // 4. 判断是否有玩家胜利(从当前走棋位置开始判断是否存在五星连珠) uint64_t winner_id = check_win(chess_row, chess_col, cur_color); if (winner_id != 0) { json_resp["reason"] = "五星连珠,战无敌!"; } json_resp["result"] = true; json_resp["winner"] = (Json::UInt64)winner_id; return json_resp; } /*处理聊天动作*/ Json::Value handle_chat(Json::Value &req) { Json::Value json_resp = req; //检测消息中是否包含敏感词 std::string msg = req["message"].asString(); size_t pos = msg.find("垃圾"); if (pos != std::string::npos) { json_resp["result"] = false; json_resp["reason"] = "消息中包含敏感词,不能发送!"; return json_resp; } //广播消息---返回消息 json_resp["result"] = true; return json_resp; } /*处理玩家退出房间动作*/ void handle_exit(uint64_t uid) { //如果是下棋中退出,则对方胜利,否则下棋结束了退出,则是正常退出 Json::Value json_resp; if (_statu == GAME_START) { uint64_t winner_id = (Json::UInt64)(uid == _white_id ? _black_id : _white_id); json_resp["optype"] = "put_chess"; json_resp["result"] = true; json_resp["reason"] = "对方掉线,不战而胜!"; json_resp["room_id"] = (Json::UInt64)_room_id; json_resp["uid"] = (Json::UInt64)uid; json_resp["row"] = -1; json_resp["col"] = -1; json_resp["winner"] = (Json::UInt64)winner_id; uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id; _tb_user->win(winner_id); _tb_user->lose(loser_id); _statu = GAME_OVER; broadcast(json_resp); } //房间中玩家数量-- _player_count--; return; } /*总的请求处理函数,在函数内部,区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播*/ void handle_request(Json::Value &req) { //1. 校验房间号是否匹配 Json::Value json_resp; uint64_t room_id = req["room_id"].asUInt64(); if (room_id != _room_id) { json_resp["optype"] = req["optype"].asString(); json_resp["result"] = false; json_resp["reason"] = "房间号不匹配!"; return broadcast(json_resp); } //2. 根据不同的请求类型调用不同的处理函数 if (req["optype"].asString() == "put_chess") { json_resp = handle_chess(req); if (json_resp["winner"].asUInt64() != 0) { uint64_t winner_id = json_resp["winner"].asUInt64(); uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id; _tb_user->win(winner_id); _tb_user->lose(loser_id); _statu = GAME_OVER; } }else if (req["optype"].asString() == "chat") { json_resp = handle_chat(req); }else { json_resp["optype"] = req["optype"].asString(); json_resp["result"] = false; json_resp["reason"] = "未知请求类型"; } std::string body; json_util::serialize(json_resp, body); DLOG("房间-广播动作: %s", body.c_str()); return broadcast(json_resp); } /*将指定的信息广播给房间中所有玩家*/ void broadcast(Json::Value &rsp) { //1. 对要响应的信息进行序列化,将Json::Value中的数据序列化成为json格式字符串 std::string body; json_util::serialize(rsp, body); //2. 获取房间中所有用户的通信连接 //3. 发送响应信息 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; } }; ``` ## 10.2 房间管理类实现 实现对所有的游戏房间进⾏管理 ```cpp #ifndef __M_ROOM_H__ #define __M_ROOM_H__ #include "util.hpp" #include "logger.hpp" #include "online.hpp" #include "db.hpp" using room_ptr = std::shared_ptr; class room_manager{ private: uint64_t _next_rid; std::mutex _mutex; user_table *_tb_user; online_manager *_online_user; std::unordered_map _rooms; std::unordered_map _users; public: /*初始化房间ID计数器*/ room_manager(user_table *ut, online_manager *om): _next_rid(1), _tb_user(ut), _online_user(om) { DLOG("房间管理模块初始化完毕!"); } ~room_manager() { DLOG("房间管理模块即将销毁!"); } //为两个用户创建房间,并返回房间的智能指针管理对象 room_ptr create_room(uint64_t uid1, uint64_t uid2) { //两个用户在游戏大厅中进行对战匹配,匹配成功后创建房间 //1. 校验两个用户是否都还在游戏大厅中,只有都在才需要创建房间。 if (_online_user->is_in_game_hall(uid1) == false) { DLOG("用户:%lu 不在大厅中,创建房间失败!", uid1); return room_ptr(); } if (_online_user->is_in_game_hall(uid2) == false) { DLOG("用户:%lu 不在大厅中,创建房间失败!", uid2); return room_ptr(); } //2. 创建房间,将用户信息添加到房间中 std::unique_lock lock(_mutex); room_ptr rp(new room(_next_rid, _tb_user, _online_user)); rp->add_white_user(uid1); rp->add_black_user(uid2); //3. 将房间信息管理起来 _rooms.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++; //4. 返回房间信息 return rp; } /*通过房间ID获取房间信息*/ room_ptr get_room_by_rid(uint64_t rid) { std::unique_lock lock(_mutex); auto it = _rooms.find(rid); if (it == _rooms.end()) { return room_ptr(); } return it->second; } /*通过用户ID获取房间信息*/ room_ptr get_room_by_uid(uint64_t uid) { std::unique_lock lock(_mutex); //1. 通过用户ID获取房间ID auto uit = _users.find(uid); if (uit == _users.end()) { return room_ptr(); } uint64_t rid = uit->second; //2. 通过房间ID获取房间信息 auto rit = _rooms.find(rid); if (rit == _rooms.end()) { return room_ptr(); } return rit->second; } /*通过房间ID销毁房间*/ void remove_room(uint64_t rid) { //因为房间信息,是通过shared_ptr在_rooms中进行管理,因此只要将shared_ptr从_rooms中移除 //则shared_ptr计数器==0,外界没有对房间信息进行操作保存的情况下就会释放 //1. 通过房间ID,获取房间信息 room_ptr rp = get_room_by_rid(rid); if (rp.get() == nullptr) { return; } //2. 通过房间信息,获取房间中所有用户的ID uint64_t uid1 = rp->get_white_user(); uint64_t uid2 = rp->get_black_user(); //3. 移除房间管理中的用户信息 std::unique_lock lock(_mutex); _users.erase(uid1); _users.erase(uid2); //4. 移除房间管理信息 _rooms.erase(rid); } /*删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用*/ void remove_room_user(uint64_t uid) { room_ptr rp = get_room_by_uid(uid); if (rp.get() == nullptr) { return; } //处理房间中玩家退出动作 rp->handle_exit(uid); //房间中没有玩家了,则销毁房间 if (rp->player_count() == 0) { remove_room(rp->id()); } return ; } }; #endif ``` # 十一、session管理模块设计 ## 11.1 什么是session 在WEB开发中,HTTP协议是⼀种无状态短链接的协议,这就导致⼀个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。 因此,服务器为每个用户浏览器创建⼀个会话对象(session对象),注意:⼀个浏览器独占⼀个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。 ## 11.2 session⼯作原理 ![image-20231102120727594](https://wei-figure-bed.oss-cn-beijing.aliyuncs.com/img/202311021207688.png) ## 11.3 session类设计实现 • 这⾥我们简单的设计⼀个session类,但是session对象不能⼀直存在,这样是⼀种资源泄漏,因此需要使用定时器对每个创建的session对象进⾏定时销毁(⼀个客户端连接断开后,⼀段时间内都没有重新连接则销毁session)。 + _ ssid使用时间戳填充。实际上,我们通常使用唯⼀id⽣成器⽣成⼀个唯⼀的id + _ user保存当前用户的信息 + timer_ptr_tp保存当前session对应的定时销毁任务 ```cpp typedef enum {UNLOGIN, 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); } uint64_t ssid() { return _ssid; } void set_statu(ss_statu statu) { _statu = statu; } void set_user(uint64_t uid) { _uid = uid; } uint64_t get_user() { return _uid; } bool is_login() { return (_statu == LOGIN); } void set_timer(const wsserver_t::timer_ptr &tp) { _tp = tp;} wsserver_t::timer_ptr& get_timer() { return _tp; } }; ``` ## 11.4 session管理设计实现 session的管理主要包含以下几个点: 1. 创建⼀个新的session 2. 通过ssid获取session 3. 通过ssid判断session是否存在 4. 销毁session。 5. 为session设置过期时间,过期后session被销毁 ```cpp #ifndef __M_SS_H__ #define __M_SS_H__ #include "util.hpp" #include #include #include typedef enum {UNLOGIN, 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); } uint64_t ssid() { return _ssid; } void set_statu(ss_statu statu) { _statu = statu; } void set_user(uint64_t uid) { _uid = uid; } uint64_t get_user() { return _uid; } bool is_login() { return (_statu == LOGIN); } void set_timer(const wsserver_t::timer_ptr &tp) { _tp = tp;} wsserver_t::timer_ptr& get_timer() { return _tp; } }; #define SESSION_TIMEOUT 30000 #define SESSION_FOREVER -1 using session_ptr = std::shared_ptr; class session_manager { private: uint64_t _next_ssid; std::mutex _mutex; std::unordered_map _session; wsserver_t *_server; public: session_manager(wsserver_t *srv): _next_ssid(1), _server(srv){ DLOG("session管理器初始化完毕!"); } ~session_manager() { DLOG("session管理器即将销毁!"); } session_ptr create_session(uint64_t uid, ss_statu statu) { std::unique_lock lock(_mutex); session_ptr ssp(new session(_next_ssid)); ssp->set_statu(statu); ssp->set_user(uid); _session.insert(std::make_pair(_next_ssid, ssp)); _next_ssid++; return ssp; } void append_session(const session_ptr &ssp) { std::unique_lock lock(_mutex); _session.insert(std::make_pair(ssp->ssid(), ssp)); } session_ptr get_session_by_ssid(uint64_t ssid) { std::unique_lock lock(_mutex); auto it = _session.find(ssid); if (it == _session.end()) { return session_ptr(); } return it->second; } void remove_session(uint64_t ssid) { std::unique_lock lock(_mutex); _session.erase(ssid); } void set_session_expire_time(uint64_t ssid, int ms) { //依赖于websocketpp的定时器来完成session生命周期的管理。 // 登录之后,创建session,session需要在指定时间无通信后删除 // 但是进入游戏大厅,或者游戏房间,这个session就应该永久存在 // 等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除 session_ptr ssp = get_session_by_ssid(ssid); if (ssp.get() == nullptr) { return; } wsserver_t::timer_ptr tp = ssp->get_timer(); if (tp.get() == nullptr && ms == SESSION_FOREVER) { // 1. 在session永久存在的情况下,设置永久存在 return ; }else if (tp.get() == nullptr && ms != SESSION_FOREVER) { // 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务 wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssid)); ssp->set_timer(tmp_tp); }else if (tp.get() != nullptr && ms == SESSION_FOREVER) { // 3. 在session设置了定时删除的情况下,将session设置为永久存在 // 删除定时任务--- stready_timer删除定时任务会导致任务直接被执行 tp->cancel();//因为这个取消定时任务并不是立即取消的 //因此重新给session管理器中,添加一个session信息, 且添加的时候需要使用定时器,而不是立即添加 ssp->set_timer(wsserver_t::timer_ptr());//将session关联的定时器设置为空 _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp)); }else if (tp.get() != nullptr && ms != SESSION_FOREVER) { // 4. 在session设置了定时删除的情况下,将session重置删除时间。 tp->cancel();//因为这个取消定时任务并不是立即取消的 ssp->set_timer(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, ssp->ssid())); //重新设置session关联的定时器 ssp->set_timer(tmp_tp); } } }; #endif ``` # 十二、五子棋对战玩家匹配管理设计实现 ## 12.1 匹配队列实现 五⼦棋对战的玩家匹配是根据自己的天梯分数进⾏匹配的,而服务器中将玩家天梯分数分为三个档 次: 1. ⻘铜:天梯分数小于2000分 2. 白银:天梯分数介于2000~3000分之间 3. ⻩⾦:天梯分数大于3000分 而实现玩家匹配的思想非常简单,为不同的档次设计各自的匹配队列,当⼀个队列中的玩家数量大于等于2的时候,则意味着同⼀档次中,有2个及以上的⼈要进行实战匹配,则出队队列中的前两个用户,相当于队首2个玩家匹配成功,这时候为其创建房间,并将两个用户信息加⼊房间中。 ```cpp template class match_queue { private: /*用链表而不直接使用queue是因为我们有中间删除数据的需要*/ std::list _list; /*实现线程安全*/ std::mutex _mutex; /*这个条件变量主要为了阻塞消费者,后边使用的时候:队列中元素个数<2则阻塞*/ std::condition_variable _cond; public: /*获取元素个数*/ int size() { std::unique_lock lock(_mutex); return _list.size(); } /*判断是否为空*/ bool empty() { std::unique_lock lock(_mutex); return _list.empty(); } /*阻塞线程*/ void wait() { std::unique_lock lock(_mutex); _cond.wait(lock); } /*入队数据,并唤醒线程*/ void push(const T &data) { std::unique_lock lock(_mutex); _list.push_back(data); _cond.notify_all(); } /*出队数据*/ bool pop(T &data) { std::unique_lock lock(_mutex); if (_list.empty() == true) { return false; } data = _list.front(); _list.pop_front(); return true; } /*移除指定的数据*/ void remove(T &data) { std::unique_lock lock(_mutex); _list.remove(data); } }; ``` ## 12.2 玩家匹配管理模块设计实现 ```cpp #ifndef __M_MATCHER_H__ #define __M_MATCHER_H__ #include "util.hpp" #include "online.hpp" #include "db.hpp" #include "room.hpp" #include #include #include template class match_queue { private: /*用链表而不直接使用queue是因为我们有中间删除数据的需要*/ std::list _list; /*实现线程安全*/ std::mutex _mutex; /*这个条件变量主要为了阻塞消费者,后边使用的时候:队列中元素个数<2则阻塞*/ std::condition_variable _cond; public: /*获取元素个数*/ int size() { std::unique_lock lock(_mutex); return _list.size(); } /*判断是否为空*/ bool empty() { std::unique_lock lock(_mutex); return _list.empty(); } /*阻塞线程*/ void wait() { std::unique_lock lock(_mutex); _cond.wait(lock); } /*入队数据,并唤醒线程*/ void push(const T &data) { std::unique_lock lock(_mutex); _list.push_back(data); _cond.notify_all(); } /*出队数据*/ bool pop(T &data) { std::unique_lock lock(_mutex); if (_list.empty() == true) { return false; } data = _list.front(); _list.pop_front(); return true; } /*移除指定的数据*/ void remove(T &data) { std::unique_lock lock(_mutex); _list.remove(data); } }; class matcher { private: /*普通选手匹配队列*/ match_queue _q_normal; /*高手匹配队列*/ match_queue _q_high; /*大神匹配队列*/ match_queue _q_super; /*对应三个匹配队列的处理线程*/ std::thread _th_normal; std::thread _th_high; std::thread _th_super; room_manager *_rm; user_table *_ut; online_manager *_om; private: void handle_match(match_queue &mq) { while(1) { //1. 判断队列人数是否大于2,<2则阻塞等待 while (mq.size() < 2) { mq.wait(); } //2. 走下来代表人数够了,出队两个玩家 uint64_t uid1, uid2; bool ret = mq.pop(uid1); if (ret == false) { continue; } ret = mq.pop(uid2); 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->create_room(uid1, uid2); if (rp.get() == nullptr) { this->add(uid1); this->add(uid2); continue; } //5. 对两个玩家进行响应 Json::Value resp; resp["optype"] = "match_success"; resp["result"] = true; std::string body; json_util::serialize(resp, body); conn1->send(body); conn2->send(body); } } void th_normal_entry() { return handle_match(_q_normal); } void th_high_entry() { return handle_match(_q_high); } void th_super_entry() { return handle_match(_q_super); } public: matcher(room_manager *rm, user_table *ut, online_manager *om): _rm(rm), _ut(ut), _om(om), _th_normal(std::thread(&matcher::th_normal_entry, this)), _th_high(std::thread(&matcher::th_high_entry, this)), _th_super(std::thread(&matcher::th_super_entry, this)){ DLOG("游戏匹配模块初始化完毕...."); } bool add(uint64_t 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(); // 2. 添加到指定的队列中 if (score < 2000) { _q_normal.remove(uid); }else if (score >= 2000 && score < 3000) { _q_high.remove(uid); }else { _q_super.remove(uid); } return true; } }; #endif ``` # 十三、整合封装服务器模块设计实现 服务器模块,是对当前所实现的所有模块的⼀个整合,并进⾏服务器搭建的⼀个模块,最终封装实现出⼀个gobang_server的服务器模块类,向外提供搭建五⼦棋对战服务器的接⼝。通过实例化的对象可以简便的完成服务器的搭建。 ## 13.1 通信接口设计(Restful风格) ### 13.1.1 静态资源请求 ```http 静态资源⻚⾯,在后台服务器上就是个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 ``` ### 13.1.2 注册用户 ```http 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": "⽤⼾名已经被占⽤"} ``` ### 13.1.3 用户登录 ```http 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": "⽤⼾名或密码错误"} ``` ### 13.1.4 获取客户端信息 ```http 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": "⽤⼾还未登录"} ``` ### 13.1.5 websocket长连接协议切换请求(进⼊游戏大厅) ```http /* ws://localhost:9000/match */ GET /match HTTP/1.1 Connection: Upgrade Upgrade: WebSocket ...... 1 2 3 4 5 HTTP/1.1 101 Switching ...... ``` WebSocket握手成功后的回复:表示游戏大厅已经进⼊成功 ```json { "optype": "hall_ready", "uid": 1 } ``` ### 13.1.6 开始对战匹配 ```http { "optype": "match_start" } /*后台正确处理后回复*/ { "optype": "match_start", //表⽰成功加⼊匹配队列 "result": true } /*后台处理出错回复*/ { "optype": "match_start" "result": false, "reason": "具体原因...." } /*匹配成功了给客⼾端的回复*/ { "optype": "match_success", //表⽰成匹配成功 "result": true } ``` ### 13.1.7 停⽌匹配 ```json { "optype": "match_stop" } /*后台正确处理后回复*/ { "optype": "match_stop" "result": true } /*后台处理出错回复*/ { "optype": "match_stop" "result": false, "reason": "具体原因...." } ``` ### 13.1.8 websocket长连接协议切换请求(进入游戏房间) ```json /* ws://localhost:9000/game */ GET /game HTTP/1.1 Connection: Upgrade Upgrade: WebSocket ...... 1 2 3 4 5 HTTP/1.1 101 Switching ...... ``` WebSocket握手成功后的回复:表示游戏房间已经进⼊成功。 ```json /*协议切换成功, 房间已经建⽴*/ { "optype": "room_ready", "room_id": 222, //房间ID "self_id": 1, //⾃⾝ID "white_id": 1, //⽩棋ID "black_id": 2, //⿊棋ID } ``` ### 13.1.9 走棋 ```json { "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是谁,谁就赢了) } ``` ### 13.1.10 聊天 ```json { "optype": "chat", "room_id": 222, "uid": 1, "message": "赶紧点" } { "optype": "chat", "result": false "reason": "聊天失败具体原因....⽐如有敏感词..." } { "optype": "chat", "result": true, "room_id": 222, "uid": 1, "message": "赶紧点" } ``` ## 13.2 服务器模块实现 ```cpp #ifndef __M_SRV_H__ #define __M_SRV_H__ #include "db.hpp" #include "matcher.hpp" #include "online.hpp" #include "room.hpp" #include "session.hpp" #include "util.hpp" #define WWWROOT "./wwwroot/" class gobang_server{ private: std::string _web_root;//静态资源根目录 ./wwwroot/ /register.html -> ./wwwroot/register.html wsserver_t _wssrv; user_table _ut; online_manager _om; room_manager _rm; matcher _mm; session_manager _sm; private: void file_handler(wsserver_t::connection_ptr &conn) { //静态资源请求的处理 //1. 获取到请求uri-资源路径,了解客户端请求的页面文件名称 websocketpp::http::parser::request req = conn->get_request(); std::string uri = req.get_uri(); //2. 组合出文件的实际路径 相对根目录 + uri std::string realpath = _web_root + uri; //3. 如果请求的是个目录,增加一个后缀 login.html, / -> /login.html if (realpath.back() == '/') { realpath += "login.html"; } //4. 读取文件内容 Json::Value resp_json; std::string body; bool ret = file_util::read(realpath, body); // 1. 文件不存在,读取文件内容失败,返回404 if (ret == false) { body += ""; body += ""; body += ""; body += ""; body += ""; body += "

Not Found

"; body += ""; conn->set_status(websocketpp::http::status_code::not_found); conn->set_body(body); return; } //5. 设置响应正文 conn->set_body(body); conn->set_status(websocketpp::http::status_code::ok); } void http_resp(wsserver_t::connection_ptr &conn, bool result, websocketpp::http::status_code::value code, const std::string &reason) { Json::Value resp_json; resp_json["result"] = result; resp_json["reason"] = reason; std::string resp_body; json_util::serialize(resp_json, resp_body); conn->set_status(code); conn->set_body(resp_body); conn->append_header("Content-Type", "application/json"); return; } void reg(wsserver_t::connection_ptr &conn) { //用户注册功能请求的处理 websocketpp::http::parser::request req = conn->get_request(); //1. 获取到请求正文 std::string req_body = conn->get_request_body(); //2. 对正文进行json反序列化,得到用户名和密码 Json::Value login_info; bool ret = json_util::unserialize(req_body, login_info); if (ret == false) { DLOG("反序列化注册信息失败"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误"); } //3. 进行数据库的用户新增操作 if (login_info["username"].isNull() || login_info["password"].isNull()) { DLOG("用户名密码不完整"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码"); } ret = _ut.insert(login_info); if (ret == false) { DLOG("向数据库插入数据失败"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名已经被占用!"); } // 如果成功了,则返回200 return http_resp(conn, true, websocketpp::http::status_code::ok, "注册用户成功"); } void login(wsserver_t::connection_ptr &conn) { //用户登录功能请求的处理 //1. 获取请求正文,并进行json反序列化,得到用户名和密码 std::string req_body = conn->get_request_body(); Json::Value login_info; bool ret = json_util::unserialize(req_body, login_info); if (ret == false) { DLOG("反序列化登录信息失败"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误"); } //2. 校验正文完整性,进行数据库的用户信息验证 if (login_info["username"].isNull() || login_info["password"].isNull()) { DLOG("用户名密码不完整"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码"); } ret = _ut.login(login_info); if (ret == false) { // 1. 如果验证失败,则返回400 DLOG("用户名密码错误"); return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名密码错误"); } //3. 如果验证成功,给客户端创建session uint64_t uid = login_info["id"].asUInt64(); session_ptr ssp = _sm.create_session(uid, LOGIN); if (ssp.get() == nullptr) { DLOG("创建会话失败"); return http_resp(conn, false, websocketpp::http::status_code::internal_server_error , "创建会话失败"); } _sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT); //4. 设置响应头部:Set-Cookie,将sessionid通过cookie返回 std::string cookie_ssid = "SSID=" + std::to_string(ssp->ssid()); conn->append_header("Set-Cookie", cookie_ssid); return http_resp(conn, true, websocketpp::http::status_code::ok , "登录成功"); } bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val) { // Cookie: SSID=XXX; path=/; //1. 以 ; 作为间隔,对字符串进行分割,得到各个单个的cookie信息 std::string sep = "; "; std::vector cookie_arr; string_util::split(cookie_str, sep, cookie_arr); for (auto str : cookie_arr) { //2. 对单个cookie字符串,以 = 为间隔进行分割,得到key和val std::vector 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) { //用户信息获取功能请求的处理 Json::Value err_resp; // 1. 获取请求信息中的Cookie,从Cookie中获取ssid std::string cookie_str = conn->get_request_header("Cookie"); if (cookie_str.empty()) { //如果没有cookie,返回错误:没有cookie信息,让客户端重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到cookie信息,请重新登录"); } // 1.5. 从cookie中取出ssid std::string ssid_str; bool ret = get_cookie_val(cookie_str, "SSID", ssid_str); if (ret == false) { //cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到ssid信息,请重新登录"); } // 2. 在session管理中查找对应的会话信息 session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str)); if (ssp.get() == nullptr) { //没有找到session,则认为登录已经过期,需要重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "登录过期,请重新登录"); } // 3. 从数据库中取出用户信息,进行序列化发送给客户端 uint64_t uid = ssp->get_user(); Json::Value user_info; ret = _ut.select_by_id(uid, user_info); if (ret == false) { //获取用户信息失败,返回错误:找不到用户信息 return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到用户信息,请重新登录"); } std::string body; json_util::serialize(user_info, body); conn->set_body(body); conn->append_header("Content-Type", "application/json"); conn->set_status(websocketpp::http::status_code::ok); // 4. 刷新session的过期时间 _sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT); } void http_callback(websocketpp::connection_hdl hdl) { wsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl); websocketpp::http::parser::request req = conn->get_request(); std::string method = req.get_method(); std::string uri = req.get_uri(); if (method == "POST" && uri == "/reg") { 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); } session_ptr get_session_by_cookie(wsserver_t::connection_ptr conn) { Json::Value err_resp; // 1. 获取请求信息中的Cookie,从Cookie中获取ssid std::string cookie_str = conn->get_request_header("Cookie"); if (cookie_str.empty()) { //如果没有cookie,返回错误:没有cookie信息,让客户端重新登录 err_resp["optype"] = "hall_ready"; err_resp["reason"] = "没有找到cookie信息,需要重新登录"; err_resp["result"] = false; ws_resp(conn, err_resp); return session_ptr(); } // 1.5. 从cookie中取出ssid std::string ssid_str; bool ret = get_cookie_val(cookie_str, "SSID", ssid_str); if (ret == false) { //cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录 err_resp["optype"] = "hall_ready"; err_resp["reason"] = "没有找到SSID信息,需要重新登录"; err_resp["result"] = false; ws_resp(conn, err_resp); return session_ptr(); } // 2. 在session管理中查找对应的会话信息 session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str)); if (ssp.get() == nullptr) { //没有找到session,则认为登录已经过期,需要重新登录 err_resp["optype"] = "hall_ready"; err_resp["reason"] = "没有找到session信息,需要重新登录"; err_resp["result"] = false; ws_resp(conn, err_resp); return session_ptr(); } return ssp; } void wsopen_game_hall(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()) || _om.is_in_game_room(ssp->get_user())) { resp_json["optype"] = "hall_ready"; resp_json["reason"] = "玩家重复登录!"; resp_json["result"] = false; return ws_resp(conn, resp_json); } //3. 将当前客户端以及连接加入到游戏大厅 _om.enter_game_hall(ssp->get_user(), conn); //4. 给客户端响应游戏大厅连接建立成功 resp_json["optype"] = "hall_ready"; resp_json["result"] = true; ws_resp(conn, resp_json); //5. 记得将session设置为永久存在 _sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER); } void wsopen_game_room(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()) || _om.is_in_game_room(ssp->get_user())) { resp_json["optype"] = "room_ready"; resp_json["reason"] = "玩家重复登录!"; resp_json["result"] = false; return ws_resp(conn, resp_json); } //3. 判断当前用户是否已经创建好了房间 --- 房间管理 room_ptr rp = _rm.get_room_by_uid(ssp->get_user()); if (rp.get() == nullptr) { resp_json["optype"] = "room_ready"; resp_json["reason"] = "没有找到玩家的房间信息"; resp_json["result"] = false; return ws_resp(conn, resp_json); } //4. 将当前用户添加到在线用户管理的游戏房间中 _om.enter_game_room(ssp->get_user(), conn); //5. 将session重新设置为永久存在 _sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER); //6. 回复房间准备完毕 resp_json["optype"] = "room_ready"; resp_json["result"] = true; resp_json["room_id"] = (Json::UInt64)rp->id(); resp_json["uid"] = (Json::UInt64)ssp->get_user(); resp_json["white_id"] = (Json::UInt64)rp->get_white_user(); resp_json["black_id"] = (Json::UInt64)rp->get_black_user(); return ws_resp(conn, resp_json); } void wsopen_callback(websocketpp::connection_hdl hdl) { //websocket长连接建立成功之后的处理函数 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) { //游戏大厅长连接断开的处理 //1. 登录验证--判断当前客户端是否已经成功登录 session_ptr ssp = get_session_by_cookie(conn); if (ssp.get() == nullptr) { return; } //1. 将玩家从游戏大厅中移除 _om.exit_game_hall(ssp->get_user()); //2. 将session恢复生命周期的管理,设置定时销毁 _sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT); } void wsclose_game_room(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()); //2. 将session回复生命周期的管理,设置定时销毁 _sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT); //3. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间 _rm.remove_room_user(ssp->get_user()); } void wsclose_callback(websocketpp::connection_hdl hdl) { //websocket连接断开前的处理 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::unserialize(req_body, req_json); if (ret == false) { resp_json["result"] = false; resp_json["reason"] = "请求信息解析失败"; return ws_resp(conn, resp_json); } //3. 对于请求进行处理: if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start"){ // 开始对战匹配:通过匹配模块,将用户添加到匹配队列中 _mm.add(ssp->get_user()); resp_json["optype"] = "match_start"; resp_json["result"] = true; return ws_resp(conn, resp_json); }else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop") { // 停止对战匹配:通过匹配模块,将用户从匹配队列中移除 _mm.del(ssp->get_user()); resp_json["optype"] = "match_stop"; resp_json["result"] = true; return ws_resp(conn, resp_json); } resp_json["optype"] = "unknow"; resp_json["reason"] = "请求类型未知"; resp_json["result"] = false; return ws_resp(conn, resp_json); } void wsmsg_game_room(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()); if (rp.get() == nullptr) { resp_json["optype"] = "unknow"; resp_json["reason"] = "没有找到玩家的房间信息"; resp_json["result"] = false; DLOG("房间-没有找到玩家房间信息"); return ws_resp(conn, resp_json); } //3. 对消息进行反序列化 Json::Value req_json; std::string req_body = msg->get_payload(); bool ret = json_util::unserialize(req_body, req_json); if (ret == false) { resp_json["optype"] = "unknow"; resp_json["reason"] = "请求解析失败"; resp_json["result"] = false; DLOG("房间-反序列化请求失败"); return ws_resp(conn, resp_json); } DLOG("房间:收到房间请求,开始处理...."); //4. 通过房间模块进行消息请求的处理 return rp->handle_request(req_json); } void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) { //websocket长连接通信处理 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); } } public: /*进行成员初始化,以及服务器回调函数的设置*/ gobang_server(const std::string &host, const std::string &user, const std::string &pass, const std::string &dbname, uint16_t port = 3306, const std::string &wwwroot = WWWROOT): _web_root(wwwroot), _ut(host, user, pass, dbname, port), _rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om) { _wssrv.set_access_channels(websocketpp::log::alevel::none); _wssrv.init_asio(); _wssrv.set_reuse_addr(true); _wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1)); _wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1)); _wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1)); _wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2)); } /*启动服务器*/ void start(int port) { _wssrv.listen(port); _wssrv.start_accept(); _wssrv.run(); } }; #endif ``` # 十四、客户端开发 登录页面:login.html ```html 登录 ``` 注册页面:register.html ```html 注册 ``` 游戏大厅页面:game_hall.html ```html 游戏大厅
开始匹配
``` 游戏房间页面:game_room.html 在游戏房间页面中,关于棋盘的绘制部分已经直接提供 ```html 游戏房间
等待玩家连接中...

你好!


你好!


leihoua~


``` 我们必须使用两个浏览器或者⼀个浏览器的无痕模式打开两个标签页,避免cookie和session相互影响导致检测到多开。 # 十五、项目扩展 ## 15.1 实现局时/步时 • 局时:⼀局游戏中玩家能思考的总时间 • 步时:⼀步落⼦过程中,玩家能思考的时间 ## 15.2 保存棋谱&录像回放 • 服务器可以把每⼀局对局、玩家轮流落⼦的位置都记录下来 • 玩家可以在游戏大厅页面选定某个曾经的比赛,在页面上回放出对局的过程 ## 15.3 观战功能 • 在游戏大厅显示当前所有的对局房间 • 玩家可以选中某个房间以观众的形式加⼊到房间中,实时的看到选手的对局情况 ## 15.4 虚拟对手&⼈机对战 • 如果当前长时间匹配不到选手,则自动分配⼀个AI对手,实现⼈机对战