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

从零搭建 C++ 在线五子棋对战项目:从环境到上线,全流程保姆级教程

大家好!今天要带大家手把手实现一个网页版 C++ 在线五子棋对战项目—— 这个项目不仅能让你巩固 C++ 核心语法,还能串联起网络编程(WebSocket)、数据库(MySQL)、前后端交互(HTML/JS/AJAX)等多个技术领域,是提升全栈开发能力的绝佳练手项目。

本文会从环境搭建核心技术拆解,再到模块实现客户端开发,每一步都附带详细代码和注释,即使是 C++ 新手也能跟着做出来。话不多说,咱们开干!

一、项目先览:我们要做什么?

在动手前,先明确项目的核心目标和技术栈,做到 “心中有数”。

1.1 核心功能

这个在线五子棋对战系统主要实现 3 大核心能力:

  • 用户管理:注册、登录、查询用户信息(天梯分、比赛场次、胜率)
  • 匹配对战:根据天梯分匹配同水平玩家,进入房间实时对战(15x15 棋盘,五子连珠获胜)
  • 实时聊天:对战时玩家可发送消息,支持敏感词过滤

1.2 开发环境

项目基于 Linux 系统开发,支持两种主流发行版,下表列出了关键工具和版本:

类别推荐配置说明
操作系统CentOS 7.6 / Ubuntu 22.04服务器端开发首选,稳定且兼容性好
代码编辑器VSCode / VimVSCode 适合新手(带语法提示),Vim 适合服务器操作
编译器 / 调试器g++ 7.3+ / gdb需支持 C++11 标准(项目大量用智能指针、lambda)
项目构建工具Makefile / CMake管理代码编译流程,避免手动敲命令
版本控制Git管理代码版本,方便回溯

1.3 核心技术栈

项目是 “C++ 后端 + 前端 + 数据库” 的组合,每个技术都有明确的作用:

技术作用为什么选它?
HTTP/WebSocket前后端通信协议HTTP 用于短连接(注册 / 登录),WebSocket 用于长连接(实时对战 / 聊天)
WebSocket++C++ 实现 WebSocket 的库跨平台、仅头文件、支持 HTTP/WebSocket 双协议
JsonCppJSON 序列化 / 反序列化库处理前后端数据交互(比如用户信息、下棋请求)
MySQL关系型数据库存储用户数据(用户名、密码、天梯分)
C++11C++ 标准智能指针(shared_ptr)、线程(thread)、lambda 表达式简化代码
HTML/CSS/JS/AJAX前端页面开发实现用户交互界面,发送请求到后端

1.4 代码获取

项目源码已开源,直接克隆即可:

git clone https://gitee.com/small-entrepreneur/personal-project.git

二、环境搭建:从 0 到 1 配置开发环境

环境搭建是项目的第一步,也是最容易踩坑的一步。本节会分别讲解CentOS 7.6Ubuntu 22.04的配置流程,每个命令都附带注释,确保你能一次成功。

2.1 通用准备:理解 “软件源” 的重要性

Linux 系统默认软件源可能在国外,下载速度慢且容易失败,因此第一步先更换国内源(阿里源 / 清华源),后续安装工具会顺畅很多。

2.2 CentOS 7.6 环境搭建(详细步骤)

2.2.1 安装基础工具(wget、lrzsz)
  • wget:用于下载文件(比如软件源、库源码)
  • lrzsz:用于 Windows 和 Linux 之间传输文件(rz上传,sz下载)
# 安装wget
sudo yum install wget -y# 安装lrzsz(传输文件用)
sudo yum install lrzsz -y# 验证lrzsz是否安装成功(显示版本即成功)
rz --version
2.2.2 更换 CentOS 官方源为阿里源
# 1. 备份原来的软件源(防止出错后无法恢复)
sudo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak# 2. 下载阿里源的配置文件
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo# 3. 清理旧缓存,生成新缓存(让系统识别新源)
sudo yum clean all
sudo yum makecache
2.2.3 安装高版本 gcc/g++(支持 C++11)

CentOS 7.6 默认 gcc 版本是 4.8.5,不支持 C++11 的部分特性,需安装 7.3 版本:

# 1. 安装scl软件源(用于获取高版本gcc)
sudo yum install centos-release-scl-rh centos-release-scl -y# 2. 安装gcc 7.3和g++ 7.3
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++ -y# 3. 设置默认gcc版本(每次登录自动启用7.3)
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc# 4. 立即生效配置
source ~/.bashrc# 5. 验证版本(显示7.3.1即成功)
g++ -v
2.2.4 安装 MySQL 5.7(存储用户数据)

MySQL 是项目的核心数据库,用于存储用户信息,步骤较多但每步都关键:

获取 MySQL 官方 yum 源

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

安装官方源

sudo rpm -ivh mysql57-community-release-el7-10.noarch.rpm

安装 MySQL 服务

sudo yum install -y mysql-community-server

解决 GPG 密钥过期问题(若安装时报错):

# 导入新的GPG密钥
sudo rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022
# 重新安装
sudo yum install -y mysql-community-server

安装 MySQL 开发包(C++ 连接 MySQL 需要):

sudo yum install -y mysql-community-devel

配置 MySQL 字符集为 UTF-8(避免中文乱码):

# 编辑MySQL配置文件
sudo vim /etc/my.cnf# 在文件中添加以下内容:
[client]
default-character-set=utf8[mysql]
default-character-set=utf8[mysqld]
character-set-server=utf8

启动 MySQL 服务并设置开机自启

# 启动服务
sudo systemctl start mysqld# 设置开机自启(避免重启后需要手动启动)
sudo systemctl enable mysqld# 查看服务状态(显示active(running)即成功)
sudo systemctl status mysqld

获取 MySQL 临时密码(首次登录用):

sudo grep 'temporary password' /var/log/mysqld.log
# 输出示例:A temporary password is generated for root@localhost: abc123!@#

登录 MySQL 并修改密码

# 登录(输入上面获取的临时密码)
mysql -uroot -p# 降低密码强度要求(方便设置简单密码,比如qwer@wu.888)
mysql> set global validate_password_policy=0;
mysql> set global validate_password_length=1;# 修改密码(替换成你的密码)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'qwer@wu.888';# 刷新权限(让修改生效)
mysql> FLUSH PRIVILEGES;# 验证字符集(所有带chara的字段值为utf8即成功)
mysql> show variables like '%chara%';# 退出MySQL
mysql> quit
2.2.5 安装 WebSocketpp 库(实现 WebSocket 服务)

WebSocketpp 是仅头文件库,需要手动编译安装:

# 1. 克隆源码(从GitHub获取)
git clone https://github.com/zaphoyd/websocketpp.git# 2. 进入源码目录,创建build文件夹
cd websocketpp
mkdir build
cd build# 3. 编译安装(指定安装路径为/usr,方便后续引用)
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
sudo make install# 4. 验证安装(编译示例代码,无报错即成功)
cd ../examples/echo_server
g++ -std=c++11 echo_server.cpp -o echo_server -lpthread -lboost_system
# 若没有报错,说明安装成功
2.2.6 安装其他依赖库(JsonCpp、Boost、CMake、Git)
# 安装JsonCpp(JSON处理)
sudo yum install jsoncpp-devel -y# 安装Boost库(WebSocketpp依赖)
sudo yum install boost-devel.x86_64 -y# 安装CMake(项目构建)
sudo yum install cmake -y# 安装Git(版本控制)
sudo yum install git -y# 验证各库是否安装成功
cmake --version  # 显示2.8.12+
git --version    # 显示1.8.3+
ls /usr/include/jsoncpp/json/  # 有assertions.h等文件即成功

2.3 Ubuntu 22.04 环境搭建(关键差异步骤)

Ubuntu 和 CentOS 的命令略有不同,这里重点讲差异部分,相同步骤(如 WebSocketpp 安装)可参考上文。

2.3.1 更换 Ubuntu 源为阿里源
# 1. 备份原源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak# 2. 编辑源文件,替换为阿里源
sudo vim /etc/apt/sources.list# 3. 在vim底行模式执行替换(将所有cn.archive.ubuntu.com换成mirrors.aliyun.com)
:%s/cn.archive.ubuntu.com/mirrors.aliyun.com/g# 4. 更新缓存
sudo apt update
2.3.2 安装基础工具和依赖库
# 安装lrzsz、gcc、g++、gdb、git、cmake
sudo apt install lrzsz gcc g++ gdb git cmake -y# 安装Boost库
sudo apt install libboost-all-dev -y# 安装JsonCpp
sudo apt install libjsoncpp-dev -y# 验证版本
gcc --version  # 显示11.3.0+
g++ --version  # 显示11.3.0+
2.3.3 安装 MySQL 5.7(Ubuntu 默认是 8.0,需指定版本)
# 1. 下载MySQL 5.7的apt源
wget http://repo.mysql.com/mysql-apt-config_0.8.12-1_all.deb# 2. 安装源(过程中选择bionic → mysql-5.7 → OK)
sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb# 3. 更新缓存
sudo apt update# 4. 安装MySQL 5.7(指定版本,避免装8.0)
sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* libmysqlclient-dev=5.7* -y# 后续配置(字符集、密码修改)和CentOS一致,参考2.2.4

三、核心技术拆解:搞懂每个技术的 “底层逻辑”

环境搭好后,我们需要先吃透项目依赖的核心技术,再动手写代码。本节会用 “原理 + 代码示例” 的方式,让你不仅会用,还懂为什么这么用。

3.1 WebSocket:解决 “实时通信” 的关键

传统 HTTP 是 “一问一答” 的短连接,比如你刷网页需要手动刷新才能获取新内容。但五子棋对战需要服务器主动推送消息(比如对方下了一颗子),这时候就需要 WebSocket。

3.1.1 WebSocket 原理

WebSocket 本质是 “基于 TCP 的长连接协议”,通信流程分 3 步:

  1. 握手(协议升级):客户端发送 HTTP 请求,附带Upgrade: WebSocket头,请求升级为 WebSocket 协议;服务器返回101 Switching Protocols,表示升级成功。
  2. 长连接通信:握手成功后,客户端和服务器可双向发送数据(无需重复建立连接)。
  3. 断开连接:任意一方发送opcode=0x8的帧,关闭连接。
3.1.2 WebSocket 报文格式(关键字段)

WebSocket 数据以 “帧” 为单位传输,每个帧的结构如下(重点看这几个字段):

字段含义
FIN1 表示当前帧是消息的最后一帧(比如长消息分多帧发送,最后一帧 FIN=1)
opcode数据类型:0x1 = 文本帧(聊天消息)、0x2 = 二进制帧、0x8 = 关闭连接、0x9=Ping
Mask1 表示客户端发送的数据需要用 Mask-Key 解密(服务器发送给客户端不需要)
Payload Length数据长度:0~126 直接表示长度;126 表示后续 2 字节是长度;127 表示后续 8 字节
3.1.3 WebSocketpp 实战:实现简单的 “回声服务器”

用 WebSocketpp 写一个服务器,客户端发送什么消息,服务器就返回什么消息(回声功能),帮你理解核心接口。

服务器代码(echo_server.cpp)
#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>  // 无TLS版本(HTTP/WS)
#include <websocketpp/server.hpp>             // 服务器类// 定义服务器类型别名,简化代码
typedef websocketpp::server<websocketpp::config::asio> websocket_server;
// 定义消息指针类型
typedef websocket_server::message_ptr message_ptr;// 1. 连接成功的回调函数(客户端连上来时触发)
void on_open(websocket_server* server, websocketpp::connection_hdl hdl) {std::cout << "客户端连接成功!" << std::endl;
}// 2. 收到消息的回调函数(客户端发消息时触发)
void on_message(websocket_server* server, websocketpp::connection_hdl hdl, message_ptr msg) {std::cout << "收到客户端消息:" << msg->get_payload() << std::endl;// 回声:将收到的消息发回给客户端server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}// 3. 连接关闭的回调函数
void on_close(websocket_server* server, websocketpp::connection_hdl hdl) {std::cout << "客户端连接关闭!" << std::endl;
}int main() {// 创建服务器对象websocket_server server;try {// 1. 设置日志级别(none表示不打印日志,避免干扰)server.set_access_channels(websocketpp::log::alevel::none);// 2. 初始化asio(WebSocketpp基于asio实现网络IO)server.init_asio();// 3. 注册回调函数(连接、消息、关闭)server.set_open_handler(std::bind(on_open, &server, std::placeholders::_1));server.set_message_handler(std::bind(on_message, &server, std::placeholders::_1, std::placeholders::_2));server.set_close_handler(std::bind(on_close, &server, std::placeholders::_1));// 4. 监听8888端口server.listen(8888);std::cout << "服务器已启动,监听端口8888..." << std::endl;// 5. 开始接受客户端连接server.start_accept();// 6. 启动服务器(阻塞,处理IO事件)server.run();} catch (websocketpp::exception const& e) {std::cerr << "服务器异常:" << e.what() << std::endl;}return 0;
}
客户端代码(echo_client.html)

用浏览器作为客户端,通过 JS 创建 WebSocket 连接:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>WebSocket客户端</title>
</head>
<body><input type="text" id="msgInput" placeholder="输入消息"><button id="sendBtn">发送</button><script>// 1. 创建WebSocket连接(替换IP为你的服务器IP)const ws = new WebSocket("ws://192.168.51.100:8888");// 2. 连接成功回调ws.onopen = function() {console.log("已连接到服务器!");};// 3. 收到服务器消息回调ws.onmessage = function(e) {console.log("收到服务器回声:" + e.data);};// 4. 连接关闭回调ws.onclose = function() {console.log("与服务器断开连接!");};// 5. 点击按钮发送消息document.getElementById("sendBtn").onclick = function() {const msg = document.getElementById("msgInput").value;if (msg) {ws.send(msg);console.log("发送消息:" + msg);}};</script>
</body>
</html>
测试步骤

编译服务器代码:

g++ echo_server.cpp -o echo_server -std=c++11 -lpthread -lboost_system

运行服务器:

./echo_server

用浏览器打开echo_client.html,输入消息并发送,打开浏览器控制台(F12),可看到如下日志:

已连接到服务器!
发送消息:hello
收到服务器回声:hello

3.2 JsonCpp:处理前后端数据交互

前后端通信需要统一的数据格式,JSON 是首选(比 XML 更简洁)。JsonCpp 用于 C++ 代码中 “序列化”(C++ 对象→JSON 字符串)和 “反序列化”(JSON 字符串→C++ 对象)。

3.2.1 JSON 数据格式入门

比如表示一个用户信息,C++ 结构体和 JSON 的对比:

C++ 结构体:

struct User {char* username = "xiaobai";int age = 18;float score[3] = {88.5, 99, 58};
};

JSON 格式:

{"username": "xiaobai","age": 18,"score": [88.5, 99, 58]
}

JSON 的核心数据类型:

  • 对象:用{}包裹,键值对形式("key": value
  • 数组:用[]包裹,元素可以是任意类型(如[1, "a", true]
  • 字符串:用""包裹(如"username"
  • 数字:直接写(如1888.5
3.2.2 JsonCpp 核心类与用法

JsonCpp 有 3 个核心类,掌握它们就能应对 90% 的场景:

  1. Json::Value:表示 JSON 数据(对象、数组、字符串等),支持[]和赋值操作。
  2. Json::StreamWriter:将Json::Value序列化为 JSON 字符串。
  3. Json::CharReader:将 JSON 字符串反序列化为Json::Value
3.2.3 实战:序列化与反序列化
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>  // 引入JsonCpp头文件int main() {// --------------------------// 1. 序列化:C++对象 → JSON字符串// --------------------------Json::Value user;  // 创建JSON对象user["username"] = "xiaobai";  // 字符串user["age"] = 18;              // 整数user["score"].append(88.5);    // 数组添加元素user["score"].append(99);user["score"].append(58);// 创建StreamWriter,将Value序列化为字符串Json::StreamWriterBuilder writerBuilder;std::unique_ptr<Json::StreamWriter> writer(writerBuilder.newStreamWriter());std::stringstream ss;  // 用于存储JSON字符串writer->write(user, &ss);  // 序列化std::string jsonStr = ss.str();std::cout << "序列化结果:\n" << jsonStr << std::endl;// --------------------------// 2. 反序列化:JSON字符串 → C++对象// --------------------------Json::Value root;  // 存储反序列化后的结果Json::CharReaderBuilder readerBuilder;std::unique_ptr<Json::CharReader> reader(readerBuilder.newCharReader());std::string err;  // 存储错误信息// 反序列化(jsonStr是输入,root是输出)bool ok = reader->parse(jsonStr.c_str(), jsonStr.c_str() + jsonStr.size(), &root, &err);if (!ok) {std::cerr << "反序列化失败:" << err << std::endl;return -1;}// 从root中提取数据std::string username = root["username"].asString();int age = root["age"].asInt();float score1 = root["score"][0].asFloat();float score2 = root["score"][1].asFloat();float score3 = root["score"][2].asFloat();std::cout << "\n反序列化结果:" << std::endl;std::cout << "username: " << username << std::endl;std::cout << "age: " << age << std::endl;std::cout << "score: " << score1 << " " << score2 << " " << score3 << std::endl;return 0;
}
编译运行
# 编译(-ljsoncpp链接JsonCpp库)
g++ json_demo.cpp -o json_demo -std=c++11 -ljsoncpp# 运行
./json_demo
输出结果
序列化结果:
{"age" : 18,"score" : [ 88.5, 99, 58 ],"username" : "xiaobai"
}反序列化结果:
username: xiaobai
age: 18
score: 88.5 99 58
3.2.4 封装 Json 工具类(复用代码)

项目中会频繁用到序列化和反序列化,封装成工具类可减少冗余:

#include <jsoncpp/json/json.h>
#include <string>class JsonUtil {
public:// 序列化:Json::Value → std::stringstatic bool Serialize(const Json::Value& root, std::string& outStr) {Json::StreamWriterBuilder writerBuilder;std::unique_ptr<Json::StreamWriter> writer(writerBuilder.newStreamWriter());std::stringstream ss;if (writer->write(root, &ss) != 0) {std::cerr << "序列化失败!" << std::endl;return false;}outStr = ss.str();return true;}// 反序列化:std::string → Json::Valuestatic bool UnSerialize(const std::string& inStr, Json::Value& root) {Json::CharReaderBuilder readerBuilder;std::unique_ptr<Json::CharReader> reader(readerBuilder.newCharReader());std::string err;if (!reader->parse(inStr.c_str(), inStr.c_str() + inStr.size(), &root, &err)) {std::cerr << "反序列化失败:" << err << std::endl;return false;}return true;}
};

使用示例:

// 序列化
Json::Value user;
user["username"] = "xiaohong";
std::string jsonStr;
JsonUtil::Serialize(user, jsonStr);// 反序列化
Json::Value root;
JsonUtil::UnSerialize(jsonStr, root);
std::cout << root["username"].asString() << std::endl;  // 输出xiaohong

3.3 MySQL C API:用 C++ 操作数据库

项目需要存储用户信息(用户名、密码、天梯分等),MySQL 是常用的关系型数据库。我们用 MySQL C API 实现 “增删改查” 操作。

3.3.1 MySQL C API 核心函数

MySQL 操作流程是 “初始化→连接→操作→关闭”,核心函数如下:

函数作用关键参数说明
mysql_init()初始化 MySQL 句柄mysql:传入 NULL 则动态分配内存
mysql_real_connect()连接 MySQL 服务器host:IP(本地用 127.0.0.1)、user:用户名、passwd:密码
mysql_set_character_set()设置字符集csname:通常为 "utf8"
mysql_query()执行 SQL 语句stmt_str:SQL 字符串(如 "select * from user")
mysql_store_result()保存查询结果到本地返回MYSQL_RES*类型的结果集
mysql_fetch_row()遍历结果集返回MYSQL_ROW(一行数据,字符串数组)
mysql_free_result()释放结果集避免内存泄漏
mysql_close()关闭连接,销毁句柄
3.3.2 实战:实现用户表的增删改查

首先在 MySQL 中创建数据库和表:

-- 1. 创建数据库(online_gobang)
create database if not exists online_gobang;
use online_gobang;-- 2. 创建用户表(user)
create table if not exists user (id int primary key auto_increment,  -- 用户ID(自增)username varchar(32) unique,        -- 用户名(唯一,不能重复)password varchar(32),               -- 密码(存储MD5加密后的字符串)score int default 1000,             -- 天梯分(初始1000)total_count int default 0,          -- 总比赛场次win_count int default 0             -- 获胜场次
);-- 3. 插入测试数据
insert into user values(null, 'xiaobai', MD5('123'), 1000, 0, 0);
insert into user values(null, 'xiaohei', MD5('123'), 1000, 0, 0);

然后用 C++ 代码实现增删改查:

#include <iostream>
#include <string>
#include <mysql/mysql.h>  // 引入MySQL C API头文件// 数据库配置(替换成你的配置)
#define DB_HOST "127.0.0.1"
#define DB_USER "root"
#define DB_PASS "qwer@wu.888"
#define DB_NAME "online_gobang"
#define DB_PORT 3306// 初始化MySQL句柄并连接数据库
MYSQL* InitMySQL() {// 1. 初始化句柄MYSQL* mysql = mysql_init(NULL);if (mysql == NULL) {std::cerr << "初始化MySQL句柄失败!" << std::endl;return NULL;}// 2. 连接数据库mysql = mysql_real_connect(mysql, DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT, NULL, 0);if (mysql == NULL) {std::cerr << "连接MySQL失败:" << mysql_error(mysql) << std::endl;mysql_close(mysql);return NULL;}// 3. 设置字符集为UTF-8(避免中文乱码)if (mysql_set_character_set(mysql, "utf8") != 0) {std::cerr << "设置字符集失败:" << mysql_error(mysql) << std::endl;mysql_close(mysql);return NULL;}std::cout << "连接MySQL成功!" << std::endl;return mysql;
}// 新增用户(注册功能)
bool AddUser(MYSQL* mysql, const std::string& username, const std::string& password) {// SQL语句(用password()函数加密密码)char sql[4096];snprintf(sql, sizeof(sql), "insert into user values(null, '%s', password('%s'), 1000, 0, 0);",username.c_str(), password.c_str());// 执行SQLif (mysql_query(mysql, sql) != 0) {std::cerr << "新增用户失败:" << mysql_error(mysql) << std::endl;return false;}std::cout << "新增用户成功:" << username << std::endl;return true;
}// 查询用户信息(登录功能)
bool QueryUser(MYSQL* mysql, const std::string& username, const std::string& password) {char sql[4096];snprintf(sql, sizeof(sql), "select id, score, total_count, win_count from user where username='%s' and password=password('%s');",username.c_str(), password.c_str());// 执行SQLif (mysql_query(mysql, sql) != 0) {std::cerr << "查询用户失败:" << mysql_error(mysql) << std::endl;return false;}// 保存结果集MYSQL_RES* res = mysql_store_result(mysql);if (res == NULL) {std::cerr << "获取结果集失败:" << mysql_error(mysql) << std::endl;return false;}// 检查结果集行数(正常应为1行)if (mysql_num_rows(res) != 1) {std::cerr << "用户名或密码错误!" << std::endl;mysql_free_result(res);return false;}// 提取用户信息MYSQL_ROW row = mysql_fetch_row(res);std::cout << "\n用户信息:" << std::endl;std::cout << "ID:" << row[0] << std::endl;std::cout << "天梯分:" << row[1] << std::endl;std::cout << "总场次:" << row[2] << std::endl;std::cout << "获胜场次:" << row[3] << std::endl;// 释放结果集(避免内存泄漏)mysql_free_result(res);return true;
}int main() {// 1. 初始化并连接MySQLMYSQL* mysql = InitMySQL();if (mysql == NULL) {return -1;}// 2. 新增用户(测试注册)// AddUser(mysql, "xiaohong", "456");// 3. 查询用户(测试登录)QueryUser(mysql, "xiaobai", "123");// 4. 关闭连接mysql_close(mysql);return 0;
}
编译运行
# 编译(-lmysqlclient链接MySQL库)
g++ mysql_demo.cpp -o mysql_demo -std=c++11 -L/usr/lib64/mysql -lmysqlclient# 运行
./mysql_demo
输出结果
连接MySQL成功!用户信息:
ID:1
天梯分:1000
总场次:0
获胜场次:0

四、项目模块实现:从 “技术” 到 “产品” 的落地

掌握核心技术后,我们开始搭建项目的整体架构。项目分为 3 大模块:数据管理模块(MySQL)、业务处理模块(C++ 后端)、前端界面模块(HTML/JS)。

4.1 项目架构设计:模块划分与交互

先看整体架构图,理解模块之间的关系:

用户 → 前端界面(登录/大厅/房间)→ 业务处理模块(WebSocket服务器)→ 数据管理模块(MySQL)

业务处理模块又细分为 5 个子模块,职责如下:

子模块核心职责依赖技术
网络通信模块搭建 HTTP/WS 服务器,处理前后端通信WebSocketpp
会话管理模块用 Cookie/Session 识别用户身份(HTTP 短连接)C++、MySQL
在线管理模块记录用户是否在线,维护用户与 WS 连接的映射unordered_map、互斥锁
房间管理模块创建房间、处理下棋 / 聊天逻辑、判断胜负五子连珠算法、敏感词过滤
匹配管理模块根据天梯分匹配玩家,创建房间多线程、队列

4.2 数据管理模块:封装 MySQL 操作

为了让其他模块更方便地操作数据库,我们封装user_table类,统一管理用户数据的增删改查。

4.2.1 user_table 类实现(头文件:m_db.h)
#ifndef __M_DB_H__
#define __M_DB_H__#include <mysql/mysql.h>
#include <jsoncpp/json/json.h>
#include <mutex>
#include <string>// 封装MySQL工具类(简化操作)
class MysqlUtil {
public:// 创建MySQL连接static MYSQL* Create(const std::string& host, const std::string& user, const std::string& pass, const std::string& dbname, uint16_t port) {MYSQL* mysql = mysql_init(NULL);if (mysql == NULL) {std::cerr << "MySQL句柄初始化失败!" << std::endl;return NULL;}mysql = mysql_real_connect(mysql, host.c_str(), user.c_str(), pass.c_str(), dbname.c_str(), port, NULL, 0);if (mysql == NULL) {std::cerr << "MySQL连接失败:" << mysql_error(mysql) << std::endl;mysql_close(mysql);return NULL;}if (mysql_set_character_set(mysql, "utf8") != 0) {std::cerr << "设置字符集失败:" << mysql_error(mysql) << std::endl;mysql_close(mysql);return NULL;}return mysql;}// 关闭MySQL连接static void Destroy(MYSQL* mysql) {if (mysql != NULL) {mysql_close(mysql);}}// 执行SQL语句static bool Exec(MYSQL* mysql, const std::string& sql) {if (mysql_query(mysql, sql.c_str()) != 0) {std::cerr << "SQL执行失败:" << sql << " | 错误:" << mysql_error(mysql) << std::endl;return false;}return true;}
};// 用户表操作类(负责user表的所有操作)
class UserTable {
private:MYSQL* _mysql;          // MySQL操作句柄std::mutex _mutex;      // 互斥锁(保证线程安全,多线程操作数据库时避免冲突)public:// 构造函数:初始化MySQL连接UserTable(const std::string& host, const std::string& user, const std::string& pass, const std::string& dbname, uint16_t port = 3306) {_mysql = MysqlUtil::Create(host, user, pass, dbname, port);if (_mysql == NULL) {exit(1);  // 数据库连接失败,程序退出}}// 析构函数:关闭MySQL连接~UserTable() {MysqlUtil::Destroy(_mysql);_mysql = NULL;}// 1. 注册:新增用户bool Insert(Json::Value& user) {// 检查用户名和密码是否存在if (user["username"].isNull() || user["password"].isNull()) {std::cerr << "用户名或密码为空!" << std::endl;return false;}// 构造SQL语句(密码用MySQL的password()函数加密)char sql[4096] = {0};snprintf(sql, sizeof(sql), "insert into user values(null, '%s', password('%s'), 1000, 0, 0);",user["username"].asCString(), user["password"].asCString());// 加锁执行SQL(多线程安全)std::unique_lock<std::mutex> lock(_mutex);return MysqlUtil::Exec(_mysql, sql);}// 2. 登录:验证用户名密码,并返回用户完整信息bool Login(Json::Value& user) {if (user["username"].isNull() || user["password"].isNull()) {std::cerr << "用户名或密码为空!" << std::endl;return false;}char sql[4096] = {0};snprintf(sql, sizeof(sql), "select id, score, total_count, win_count from user where username='%s' and password=password('%s');",user["username"].asCString(), user["password"].asCString());std::unique_lock<std::mutex> lock(_mutex);if (!MysqlUtil::Exec(_mysql, sql)) {return false;}// 获取结果集MYSQL_RES* res = mysql_store_result(_mysql);if (res == NULL || mysql_num_rows(res) != 1) {mysql_free_result(res);return false;}// 提取用户信息到user中MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stol(row[0]);       // 用户IDuser["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;}// 3. 获胜:更新用户分数(+30)、总场次(+1)、获胜场次(+1)bool Win(uint64_t uid) {char sql[4096] = {0};snprintf(sql, sizeof(sql), "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id=%lu;", uid);std::unique_lock<std::mutex> lock(_mutex);return MysqlUtil::Exec(_mysql, sql);}// 4. 失败:更新用户分数(-30)、总场次(+1)bool Lose(uint64_t uid) {char sql[4096] = {0};snprintf(sql, sizeof(sql), "update user set score=score-30, total_count=total_count+1 where id=%lu;", uid);std::unique_lock<std::mutex> lock(_mutex);return MysqlUtil::Exec(_mysql, sql);}
};#endif  // __M_DB_H__

4.3 房间管理模块:实现对战与聊天逻辑

房间是对战的核心载体,每个房间包含 2 个玩家,负责处理 “下棋” 和 “聊天” 请求,判断胜负。

4.3.1 房间类(Room)实现
#include <iostream>
#include <vector>
#include <jsoncpp/json/json.h>
#include "m_db.h"  // 引入用户表操作类
#include "online_manager.h"  // 引入在线管理类(后续实现)// 棋盘大小(15x15)
#define BOARD_ROW 15
#define BOARD_COL 15// 棋子颜色
#define CHESS_WHITE 1  // 白棋
#define CHESS_BLACK 2  // 黑棋// 房间状态
typedef enum { GAME_START, GAME_OVER } RoomStatus;class Room {
private:uint64_t _room_id;              // 房间IDRoomStatus _status;             // 房间状态(游戏中/已结束)int _player_count;              // 玩家数量(固定2人)uint64_t _white_uid;            // 白棋玩家IDuint64_t _black_uid;            // 黑棋玩家IDUserTable* _user_table;         // 用户表操作对象(更新分数用)OnlineManager* _online_manager; // 在线管理对象(获取玩家连接用)std::vector<std::vector<int>> _board;  // 棋盘(0=空,1=白,2=黑)private:// 辅助函数:检查某个方向是否有五子连珠// row,col:当前下棋位置;row_off,col_off:方向偏移(如0,1表示水平向右);color:棋子颜色bool CheckFiveInLine(int row, int col, int row_off, int col_off, int color) {int count = 1;  // 当前位置已有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;  // 5颗及以上连珠则返回true}// 检查是否获胜(从当前下棋位置检查4个方向)uint64_t CheckWin(int row, int col, int color) {// 4个方向:水平、垂直、正斜、反斜if (CheckFiveInLine(row, col, 0, 1, color) ||  // 水平CheckFiveInLine(row, col, 1, 0, color) ||  // 垂直CheckFiveInLine(row, col, -1, 1, color) || // 正斜(左上→右下)CheckFiveInLine(row, col, -1, -1, color)) { // 反斜(右上→左下)// 返回获胜玩家IDreturn color == CHESS_WHITE ? _white_uid : _black_uid;}return 0;  // 未获胜}public:// 构造函数:初始化房间Room(uint64_t room_id, UserTable* user_table, OnlineManager* online_manager) : _room_id(room_id), _status(GAME_START), _player_count(0),_user_table(user_table), _online_manager(online_manager),_board(BOARD_ROW, std::vector<int>(BOARD_COL, 0)) {  // 棋盘初始化为全0std::cout << "房间" << _room_id << "创建成功!" << std::endl;}// 析构函数~Room() {std::cout << "房间" << _room_id << "销毁成功!" << std::endl;}// 获取房间IDuint64_t GetRoomId() const { return _room_id; }// 获取房间状态RoomStatus GetStatus() const { return _status; }// 获取玩家数量int GetPlayerCount() const { return _player_count; }// 添加白棋玩家void AddWhitePlayer(uint64_t uid) {_white_uid = uid;_player_count++;}// 添加黑棋玩家void AddBlackPlayer(uint64_t uid) {_black_uid = uid;_player_count++;}// 获取白棋玩家IDuint64_t GetWhiteUid() const { return _white_uid; }// 获取黑棋玩家IDuint64_t GetBlackUid() const { return _black_uid; }// 处理下棋请求Json::Value HandleChess(Json::Value& req) {Json::Value resp = req;  // 响应 = 请求的基础上添加结果int row = req["row"].asInt();    // 下棋行号int col = req["col"].asInt();    // 下棋列号uint64_t uid = req["uid"].asUInt64();  // 当前下棋玩家ID// 1. 检查玩家是否在线(任意一方离线则另一方获胜)if (!_online_manager->IsInGameRoom(_white_uid)) {resp["result"] = true;resp["reason"] = "对方掉线,不战而胜!";resp["winner"] = (Json::UInt64)_black_uid;_status = GAME_OVER;_user_table->Win(_black_uid);  // 黑棋获胜,更新分数_user_table->Lose(_white_uid); // 白棋失败,更新分数return resp;}if (!_online_manager->IsInGameRoom(_black_uid)) {resp["result"] = true;resp["reason"] = "对方掉线,不战而胜!";resp["winner"] = (Json::UInt64)_white_uid;_status = GAME_OVER;_user_table->Win(_white_uid);_user_table->Lose(_black_uid);return resp;}// 2. 检查当前位置是否已有棋子if (_board[row][col] != 0) {resp["result"] = false;resp["reason"] = "当前位置已有棋子,请重新选择!";return resp;}// 3. 记录棋子颜色并更新棋盘int color = (uid == _white_uid) ? CHESS_WHITE : CHESS_BLACK;_board[row][col] = color;// 4. 检查是否获胜uint64_t winner_uid = CheckWin(row, col, color);if (winner_uid != 0) {resp["result"] = true;resp["reason"] = "五子连珠,获胜!";resp["winner"] = (Json::UInt64)winner_uid;_status = GAME_OVER;_user_table->Win(winner_uid);  // 获胜者加分uint64_t loser_uid = (winner_uid == _white_uid) ? _black_uid : _white_uid;_user_table->Lose(loser_uid);  // 失败者减分} else {resp["result"] = true;resp["winner"] = 0;  // 未分胜负}return resp;}// 处理聊天请求(敏感词过滤)Json::Value HandleChat(Json::Value& req) {Json::Value resp = req;std::string msg = req["message"].asString();// 敏感词过滤(示例:过滤"垃圾")if (msg.find("垃圾") != std::string::npos) {resp["result"] = false;resp["reason"] = "消息包含敏感词,无法发送!";return resp;}resp["result"] = true;return resp;}// 广播消息给房间内所有玩家void Broadcast(Json::Value& resp) {std::string resp_str;JsonUtil::Serialize(resp, resp_str);  // 序列化响应// 获取白棋玩家的WebSocket连接并发送消息auto white_conn = _online_manager->GetConnFromGameRoom(_white_uid);if (white_conn) {white_conn->send(resp_str);}// 获取黑棋玩家的WebSocket连接并发送消息auto black_conn = _online_manager->GetConnFromGameRoom(_black_uid);if (black_conn) {black_conn->send(resp_str);}}
};

4.4 匹配管理模块:根据天梯分匹配玩家

匹配模块根据玩家的天梯分,将其分配到不同的队列(青铜 <2000 分、白银 2000~3000 分、黄金> 3000 分),当队列中有 2 人及以上时,创建房间并开始对战。

4.4.1 匹配队列实现(线程安全)
#include <list>
#include <mutex>
#include <condition_variable>
#include <iostream>// 匹配队列(模板类,支持任意类型的元素)
template <typename T>
class MatchQueue {
private:std::list<T> _list;                // 存储玩家ID的列表(支持中间删除)std::mutex _mutex;                 // 互斥锁(线程安全)std::condition_variable _cond;     // 条件变量(队列人数不足时阻塞)public:// 获取队列大小int Size() {std::unique_lock<std::mutex> lock(_mutex);return _list.size();}// 入队(添加玩家到队列)void Push(const T& data) {std::unique_lock<std::mutex> lock(_mutex);_list.push_back(data);_cond.notify_all();  // 唤醒等待的线程(可能人数够了)std::cout << "玩家" << data << "加入匹配队列,当前队列人数:" << _list.size() << std::endl;}// 出队(获取队首玩家)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(const T& data) {std::unique_lock<std::mutex> lock(_mutex);_list.remove(data);std::cout << "玩家" << data << "取消匹配,当前队列人数:" << _list.size() << std::endl;}// 阻塞等待(队列人数<2时阻塞)void Wait() {std::unique_lock<std::mutex> lock(_mutex);while (_list.size() < 2) {_cond.wait(lock);  // 释放锁并阻塞,直到被唤醒}}
};
4.4.2 匹配管理器实现
#include <thread>
#include "match_queue.h"
#include "room_manager.h"  // 房间管理器(后续实现)
#include "m_db.h"
#include "online_manager.h"class Matcher {
private:// 三个匹配队列(青铜、白银、黄金)MatchQueue<uint64_t> _queue_bronze;  // <2000分MatchQueue<uint64_t> _queue_silver;  // 2000~3000分MatchQueue<uint64_t> _queue_gold;    // >3000分// 处理三个队列的线程std::thread _th_bronze;std::thread _th_silver;std::thread _th_gold;RoomManager* _room_manager;  // 房间管理器(创建房间用)UserTable* _user_table;      // 用户表(获取玩家天梯分用)OnlineManager* _online_manager;  // 在线管理(检查玩家是否在线用)private:// 匹配处理函数(通用,处理某个队列)void HandleMatch(MatchQueue<uint64_t>& queue) {while (true) {// 1. 等待队列人数>=2queue.Wait();// 2. 出队两个玩家uint64_t uid1, uid2;if (!queue.Pop(uid1)) {continue;}if (!queue.Pop(uid2)) {queue.Push(uid1);  // 把第一个玩家放回队列continue;}// 3. 检查两个玩家是否还在线(防止匹配过程中掉线)if (!_online_manager->IsInGameHall(uid1)) {queue.Push(uid2);  // 玩家1离线,把玩家2放回队列continue;}if (!_online_manager->IsInGameHall(uid2)) {queue.Push(uid1);  // 玩家2离线,把玩家1放回队列continue;}// 4. 创建房间并将玩家加入房间auto room = _room_manager->CreateRoom(uid1, uid2);if (room == nullptr) {// 创建房间失败,把两个玩家放回队列queue.Push(uid1);queue.Push(uid2);continue;}// 5. 通知两个玩家匹配成功Json::Value resp;resp["optype"] = "match_success";resp["result"] = true;std::string resp_str;JsonUtil::Serialize(resp, resp_str);// 获取玩家的WebSocket连接并发送通知auto conn1 = _online_manager->GetConnFromGameHall(uid1);if (conn1) {conn1->send(resp_str);}auto conn2 = _online_manager->GetConnFromGameHall(uid2);if (conn2) {conn2->send(resp_str);}std::cout << "玩家" << uid1 << "和" << uid2 << "匹配成功,房间ID:" << room->GetRoomId() << std::endl;}}// 三个队列的线程入口函数void ThreadBronze() { HandleMatch(_queue_bronze); }void ThreadSilver() { HandleMatch(_queue_silver); }void ThreadGold() { HandleMatch(_queue_gold); }public:// 构造函数:初始化线程Matcher(RoomManager* room_manager, UserTable* user_table, OnlineManager* online_manager): _room_manager(room_manager), _user_table(user_table), _online_manager(online_manager),_th_bronze(std::thread(&Matcher::ThreadBronze, this)),_th_silver(std::thread(&Matcher::ThreadSilver, this)),_th_gold(std::thread(&Matcher::ThreadGold, this)) {std::cout << "匹配模块初始化成功!" << std::endl;}// 析构函数:等待线程结束~Matcher() {_th_bronze.join();_th_silver.join();_th_gold.join();}// 添加玩家到匹配队列(根据天梯分选择队列)bool AddPlayer(uint64_t uid) {// 获取玩家天梯分Json::Value user;if (!_user_table->SelectById(uid, user)) {std::cerr << "获取玩家" << uid << "信息失败!" << std::endl;return false;}int score = user["score"].asInt();// 根据分数选择队列if (score < 2000) {_queue_bronze.Push(uid);} else if (score <= 3000) {_queue_silver.Push(uid);} else {_queue_gold.Push(uid);}return true;}// 从匹配队列中移除玩家(取消匹配)bool RemovePlayer(uint64_t uid) {Json::Value user;if (!_user_table->SelectById(uid, user)) {std::cerr << "获取玩家" << uid << "信息失败!" << std::endl;return false;}int score = user["score"].asInt();if (score < 2000) {_queue_bronze.Remove(uid);} else if (score <= 3000) {_queue_silver.Remove(uid);} else {_queue_gold.Remove(uid);}return true;}
};

五、客户端开发:实现用户交互界面

客户端是用户直接接触的部分,需要实现 4 个核心页面:注册页登录页游戏大厅游戏房间

5.1 登录页面(login.html)

用户输入用户名和密码,发送 AJAX 请求到后端验证,成功后跳转游戏大厅。

<!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><style>/* 简单样式,让页面更美观 */.nav {height: 50px;line-height: 50px;text-align: center;font-size: 20px;background-color: #333;color: white;}.login-container {width: 300px;margin: 50px auto;border: 1px solid #ddd;padding: 20px;border-radius: 5px;}.row {margin: 15px 0;}.row span {display: inline-block;width: 80px;}.row input {width: 180px;height: 25px;padding: 0 5px;}.row button {width: 270px;height: 30px;background-color: #4CAF50;color: white;border: none;border-radius: 3px;cursor: pointer;}.row button:hover {background-color: #45a049;}</style>
</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="username" placeholder="请输入用户名"></div><div class="row"><span>密码</span><input type="password" id="password" placeholder="请输入密码"></div><div class="row"><button id="loginBtn">登录</button></div><div class="row" style="text-align: center;"><a href="register.html">还没有账号?去注册</a></div></div></div><!-- 引入jQuery(简化AJAX请求) --><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script><script>// 点击登录按钮触发$("#loginBtn").click(function() {// 1. 获取输入的用户名和密码let username = $("#username").val().trim();let password = $("#password").val().trim();// 2. 验证输入不为空if (!username || !password) {alert("用户名和密码不能为空!");return;}// 3. 发送AJAX POST请求到后端/login接口$.ajax({url: "/login",          // 后端接口地址type: "POST",           // 请求方法contentType: "application/json",  // 数据格式为JSONdata: JSON.stringify({  // 发送的数据(序列化为JSON字符串)"username": username,"password": password}),success: function(res) {  // 请求成功回调if (res.result) {alert("登录成功!即将进入游戏大厅");// 跳转到游戏大厅页面window.location.href = "/game_hall.html";} else {alert("登录失败:" + res.reason);}},error: function(xhr) {  // 请求失败回调let res = JSON.parse(xhr.responseText);alert("登录失败:" + res.reason);}});});</script>
</body>
</html>

5.2 游戏房间页面(game_room.html):实现对战核心交互

上一部分我们梳理了游戏房间的基础样式,接下来要完成棋盘绘制WebSocket 实时通信下棋逻辑聊天功能—— 这些是对战交互的核心,每一步都严格对应文档中的实现细节。

5.2.1 完整 HTML 结构(含 Canvas 与聊天区域)
<!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><style>.nav {height: 50px;line-height: 50px;text-align: center;font-size: 20px;background-color: #333;color: white;margin-bottom: 20px;}.container {display: flex;justify-content: center;gap: 50px;}/* 棋盘样式:木纹底色,模拟真实棋盘质感 */#chess {border: 2px solid #8B4513;background-color: #F5DEB3; /* 小麦色,接近木纹纸 */}/* 状态显示区域(轮到谁走棋) */#screen {text-align: center;font-size: 18px;margin: 10px 0;color: #333;font-weight: bold;}/* 聊天区域样式 */#chat_area {width: 300px;height: 450px;border: 1px solid #ddd;border-radius: 5px;overflow: hidden;}/* 聊天消息显示区 */#chat_show {height: 380px;padding: 10px;overflow-y: auto;background-color: #f9f9f9;}/* 自己的消息样式 */#chat_show #self_msg {text-align: right;color: #4CAF50;margin: 5px 0;}/* 对方的消息样式 */#chat_show #peer_msg {text-align: left;color: #2196F3;margin: 5px 0;}/* 聊天输入区 */#msg_show {display: flex;height: 70px;border-top: 1px solid #ddd;}#chat_input {flex: 1;padding: 0 10px;border: none;outline: none;font-size: 14px;}#chat_button {width: 80px;background-color: #2196F3;color: white;border: none;cursor: pointer;}#chat_button:hover {background-color: #1976D2;}/* 返回大厅按钮 */#back_hall {margin-top: 20px;padding: 8px 20px;background-color: #FF9800;color: white;border: none;border-radius: 3px;cursor: pointer;display: none; /* 初始隐藏,游戏结束后显示 */}</style>
</head>
<body><div class="nav">网络五子棋对战游戏</div><div class="container"><!-- 棋盘区域 --><div id="chess_area"><canvas id="chess" width="450px" height="450px"></canvas><div id="screen">等待玩家连接中...</div><button id="back_hall">返回游戏大厅</button></div><!-- 聊天区域 --><div id="chat_area"><div id="chat_show"><!-- 聊天消息会动态添加到这里 --></div><div id="msg_show"><input type="text" id="chat_input" placeholder="输入消息..."><button id="chat_button">发送</button></div></div></div><!-- 引入jQuery简化DOM操作 --><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script><script>// --------------------------// 1. 棋盘初始化与绘制(核心:Canvas实现)// --------------------------let chessBoard = []; // 存储棋盘状态:0=空,1=白棋,2=黑棋const BOARD_ROW_AND_COL = 15; // 15x15棋盘(文档规定)const chess = document.getElementById('chess');const context = chess.getContext('2d'); // 获取2D绘图上下文// 初始化棋盘数组(全部置0)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;}}}// 绘制棋盘网格线(每个格子30px,文档中棋盘尺寸450px=15*30px)function drawChessBoard() {context.strokeStyle = "#8B4513"; // 网格线颜色:棕色(模拟木纹棋盘)for (let i = 0; i < BOARD_ROW_AND_COL; i++) {// 横向线(x从15到435,y随i变化)context.beginPath();context.moveTo(15 + i * 30, 15); // 起点(左边界+偏移)context.lineTo(15 + i * 30, 435); // 终点(右边界)context.stroke();// 纵向线(y从15到435,x随i变化)context.beginPath();context.moveTo(15, 15 + i * 30); // 起点(上边界+偏移)context.lineTo(435, 15 + i * 30); // 终点(下边界)context.stroke();}}// 绘制棋子(含径向渐变,模拟真实棋子光泽)// i:列,j:行,isWhite:true=白棋,false=黑棋function oneStep(i, j, isWhite) {if (i < 0 || j < 0) return; // 边界判断context.beginPath();// 绘制圆形棋子:中心坐标(15+i*30, 15+j*30),半径13pxcontext.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);context.closePath();// 径向渐变(从外到内的颜色过渡)const gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, // 外圆:中心偏移2px,半径13px15 + i * 30 + 2, 15 + j * 30 - 2, 0   // 内圆:中心同外圆,半径0px);// 区分黑白棋颜色(文档规定:白棋#D1D1D1→#F9F9F9,黑棋#0A0A0A→#636766)if (!isWhite) {gradient.addColorStop(0, "#0A0A0A"); // 黑棋外色gradient.addColorStop(1, "#636766"); // 黑棋内色} else {gradient.addColorStop(0, "#D1D1D1"); // 白棋外色gradient.addColorStop(1, "#F9F9F9"); // 白棋内色}context.fillStyle = gradient;context.fill(); // 填充棋子}// 初始化游戏(加载背景+绘制棋盘)function initGame() {initBoard();// 加载棋盘背景图(文档中用sky.jpeg,可替换为木纹图)const bgImg = new Image();bgImg.src = "image/wood_bg.jpeg"; // 假设背景图放在image文件夹bgImg.onload = function() {// 绘制背景图(覆盖整个Canvas)context.drawImage(bgImg, 0, 0, 450, 450);// 绘制网格线(在背景图之上)drawChessBoard();}}// --------------------------// 2. WebSocket连接(进入房间必备)// --------------------------const wsUrl = "ws://" + window.location.host + "/room"; // 房间长连接地址let wsHdl = new WebSocket(wsUrl); // 创建WebSocket实例let roomInfo = null; // 存储房间信息:room_id、uid、white_id、black_idlet isMyTurn = false; // 是否轮到自己走棋// 窗口关闭前关闭WebSocket连接(避免服务器残留连接)window.onbeforeunload = function() {if (wsHdl.readyState === WebSocket.OPEN) {wsHdl.close();}}// 连接成功回调wsHdl.onopen = function() {console.log("游戏房间长连接建立成功");}// 连接关闭回调wsHdl.onclose = function() {console.log("游戏房间长连接断开");alert("与服务器断开连接,将返回大厅");window.location.href = "/game_hall.html";}// 连接错误回调wsHdl.onerror = function() {console.error("游戏房间长连接出错");alert("连接出错,请刷新页面重试");}// 更新状态显示(轮到谁走棋)function updateScreen(turn) {const screenDiv = document.getElementById('screen');screenDiv.innerHTML = turn ? "轮到你走棋(" + (isWhite ? "白棋" : "黑棋" ) + ")" : "对方思考中...";}// --------------------------// 3. 消息处理(核心逻辑:响应服务器指令)// --------------------------wsHdl.onmessage = function(evt) {const resp = JSON.parse(evt.data); // 解析服务器返回的JSON数据console.log("收到房间消息:", resp);// 3.1 处理"room_ready":房间初始化(首次进入房间时触发)if (resp.optype === "room_ready") {roomInfo = resp; // 保存房间信息const isWhite = roomInfo.uid === roomInfo.white_id; // 判断自己是否为白棋isMyTurn = isWhite; // 白棋先下(文档默认规则)updateScreen(isMyTurn); // 更新状态显示initGame(); // 初始化棋盘return;}// 3.2 处理"put_chess":走棋响应(自己或对方走棋后触发)if (resp.optype === "put_chess") {// 走棋失败(如位置被占用)if (!resp.result) {alert("走棋失败:" + resp.reason);return;}// 提取走棋信息const row = resp.row;const col = resp.col;const userId = resp.uid;const winner = resp.winner;// 绘制棋子(判断是白棋还是黑棋)const isWhite = userId === roomInfo.white_id;oneStep(col, row, isWhite); // 注意:col是列(x),row是行(y)chessBoard[row][col] = isWhite ? 1 : 2; // 更新棋盘状态// 切换走棋权isMyTurn = userId !== roomInfo.uid;updateScreen(isMyTurn);// 3.3 处理获胜结果(winner≠0表示有胜利者)if (winner !== 0) {const screenDiv = document.getElementById('screen');const backBtn = document.getElementById('back_hall');// 判断自己是否获胜if (winner === roomInfo.uid) {screenDiv.innerHTML = "恭喜!你赢了!" + resp.reason;} else {screenDiv.innerHTML = "很遗憾,你输了!" + resp.reason;}// 显示返回大厅按钮backBtn.style.display = "block";// 禁用棋盘点击(游戏结束)chess.onclick = null;return;}}// 3.4 处理"chat":聊天消息(自己或对方发送)if (resp.optype === "chat") {if (!resp.result) {alert("消息发送失败:" + resp.reason); // 如含敏感词return;}const chatShow = document.getElementById('chat_show');const msgP = document.createElement('p');// 区分自己和对方的消息(用不同ID和颜色)if (resp.uid === roomInfo.uid) {msgP.id = "self_msg";msgP.innerHTML = "我:" + resp.message;} else {msgP.id = "peer_msg";msgP.innerHTML = "对方:" + resp.message;}chatShow.appendChild(msgP);// 滚动到最新消息chatShow.scrollTop = chatShow.scrollHeight;}}// --------------------------// 4. 下棋逻辑(点击棋盘触发)// --------------------------chess.onclick = function(e) {// 非自己回合或游戏未初始化,不处理if (!isMyTurn || !roomInfo) {return;}// 计算点击位置对应的棋盘行列(每个格子30px)const x = e.offsetX; // 点击位置相对于Canvas的X坐标const y = e.offsetY; // 点击位置相对于Canvas的Y坐标const col = Math.floor(x / 30); // 列(0-14)const row = Math.floor(y / 30); // 行(0-14)// 检查当前位置是否已有棋子if (chessBoard[row][col] !== 0) {alert("当前位置已有棋子,请选择其他位置");return;}// 发送走棋请求到服务器(文档规定的put_chess格式)const chessReq = {"optype": "put_chess","room_id": roomInfo.room_id,"uid": roomInfo.uid,"row": row,"col": col};wsHdl.send(JSON.stringify(chessReq));console.log("发送走棋请求:", chessReq);}// --------------------------// 5. 聊天功能(实时交流)// --------------------------const chatInput = document.getElementById('chat_input');const chatBtn = document.getElementById('chat_button');// 点击发送按钮发送消息chatBtn.onclick = sendChatMsg;// 按Enter键发送消息chatInput.onkeydown = function(e) {if (e.key === "Enter") {sendChatMsg();}}// 发送聊天消息(文档规定的chat格式)function sendChatMsg() {const msg = chatInput.value.trim();if (!msg) {alert("消息不能为空");return;}// 构造聊天请求const chatReq = {"optype": "chat","room_id": roomInfo.room_id,"uid": roomInfo.uid,"message": msg};wsHdl.send(JSON.stringify(chatReq));// 清空输入框chatInput.value = "";}// --------------------------// 6. 返回大厅按钮事件// --------------------------document.getElementById('back_hall').onclick = function() {window.location.href = "/game_hall.html";}</script>
</body>
</html>

六、Ubuntu 22.04 环境搭建补充(文档重点)

之前详细讲解了 CentOS 7.6 的环境搭建,为了满足大多数用户,我还提供了 Ubuntu 22.04 的配置方案,适合习惯 Ubuntu 的开发者,以下是完整步骤(每一步对应文档内容)。

6.1 更换软件源(解决下载慢问题)

Ubuntu 默认源在国内下载慢,需替换为阿里源:

备份原源文件:

sudo cp /etc/apt/sources.list.d/original.list /etc/apt/sources.list.d/original.list.bak

编辑源文件:

sudo vim /etc/apt/sources.list.d/original.list

替换源地址(Vim 底行模式执行):

:%s/cn.archive.ubuntu.com/mirrors.aliyun.com/g

(含义:将所有cn.archive.ubuntu.com替换为阿里源mirrors.aliyun.com

更新源缓存:

sudo apt update

6.2 安装基础工具

# 安装文件传输工具lrzsz
sudo apt install lrzsz -y
# 验证:显示版本即成功
rz --version # 应输出"rz (GNU lrzsz) 0.12.21rc"# 安装gcc/g++编译器(支持C++11)
sudo apt install gcc g++ -y
# 验证:Ubuntu 22.04默认是11.3.0版本
gcc --version # 输出"gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"# 安装gdb调试器
sudo apt install gdb -y
# 验证:版本12.1
gdb --version # 输出"GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1"# 安装git和cmake
sudo apt install git cmake -y
git --version # 输出"git version 2.34.1"
cmake --version # 输出"cmake version 3.22.1"

6.3 安装依赖库(项目核心)

6.3.1 Boost 库(WebSocketpp 依赖)
sudo apt install libboost-all-dev -y
# 验证:检查头文件是否存在
ls /usr/include/boost/version.hpp # 应显示文件路径
6.3.2 JsonCpp 库(JSON 处理)
sudo apt install libjsoncpp-dev -y
# 验证:检查头文件和库文件
ls /usr/include/jsoncpp/json/ # 应包含assertions.h、json.h等
ls /usr/lib/x86_64-linux-gnu/libjsoncpp.so # 应显示库文件路径
6.3.3 MySQL 5.7(文档推荐版本)

Ubuntu 22.04 默认是 MySQL 8.0,需手动安装 5.7:

下载 MySQL 5.7 的 APT 源:

wget http://repo.mysql.com/mysql-apt-config_0.8.12-1_all.deb

安装源(过程中需选择bionicmysql-5.7):

sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb

解决 “无 Release 文件” 错误(文档中常见问题):

sudo vim /etc/apt/sources.list
# 删除含"file:/cdrom"的行,保存退出

导入 GPG 密钥(解决过期问题):

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29

更新源并安装 MySQL 5.7:

sudo apt update
# 指定安装5.7版本
sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* libmysqlclient-dev=5.7* -y

配置 MySQL(同 CentOS):

  • 启动服务:sudo systemctl start mysql
  • 设置字符集为 UTF-8(编辑/etc/mysql/my.cnf,参考)
  • 修改密码:sudo mysql_secure_installation(文档中推荐密码强度设为 LOW)
6.3.4 WebSocketpp 库(手动编译)
# 克隆源码
git clone https://github.com/zaphoyd/websocketpp.git
cd websocketpp
# 创建build目录并编译
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr .. # 安装到/usr目录(方便引用)
sudo make install
# 验证:编译示例代码
cd ../examples/echo_server
g++ -std=c++11 echo_server.cpp -o echo_server -lpthread -lboost_system
# 无报错即安装成功

七、项目测试与常见问题解决(文档经验总结)

7.1 测试流程(按文档推荐步骤)

启动服务器

# 进入项目目录
cd online_gobang
# 编译(假设用Makefile)
make
# 启动服务器(监听9000端口)
./gobang_server 9000

客户端访问

  • 打开浏览器,输入http://服务器IP:9000/login.html
  • 注册账号(用户名 + 密码)→ 登录 → 进入游戏大厅
  • 点击 “开始匹配”,等待另一个玩家加入(需打开 2 个浏览器或无痕模式,避免 Cookie 冲突)
  • 匹配成功后进入房间,开始对战

7.2 常见问题与解决方案(文档中高频问题)

问题现象原因解决方案(文档参考)
编译时提示 “undefined reference to Json::Value”未链接 JsonCpp 库编译命令加-ljsoncpp(参考)
MySQL 连接失败,提示 “Access denied”密码错误或权限不足1. 重置密码:mysqladmin -u root password "新密码"2. 授权远程访问:grant all on *.* to root@'%' identified by '密码';
WebSocket 连接失败,提示 “404 Not Found”服务器未监听 /room 或 /hall 路径检查服务器代码中set_http_handler是否正确注册了路径(参考)
下棋后无响应1. 未发送 put_chess 请求2. 服务器未广播消息1. 检查send_chess函数是否调用2. 确认房间broadcast函数是否正确发送消息(参考)
中文乱码字符集未设置为 UTF-81. MySQL 字符集设为 utf8(参考)2. 前端 HTML 设置<meta charset="UTF-8">

八、项目扩展方向(文档建议)

如果想进一步完善项目,文档中提供了 4 个优质扩展方向,适合进阶学习:

局时 / 步时功能

  • 局时:一局游戏总时间(如 10 分钟),超时判负
  • 步时:每步落子时间(如 30 秒),超时自动认输
  • 实现:用websocketpp::server::set_timer设置定时器(参考)

棋谱保存与回放

  • 服务器记录每步落子(row、col、time、uid),存储到 MySQL 的chess_record
  • 前端添加 “历史对局” 页面,选择对局后按时间顺序回放棋子

观战功能

  • 在游戏大厅显示当前活跃房间列表
  • 观众加入房间后,仅接收消息不发送(服务器判断用户角色为 “viewer”)

人机对战

  • 匹配超时(如 30 秒)后,自动创建 AI 对手
  • AI 逻辑:简单版用 “防守优先” 策略(阻挡对方五子连珠),复杂版用 Minimax 算法

九、总结(文档核心价值)

这个 C++ 在线五子棋项目是一个典型的 “全栈 C++” 练手项目,文档从环境搭建→核心技术→模块实现→客户端开发提供了完整流程,核心价值在于:

  1. 技术栈全面:覆盖 C++11、WebSocket、MySQL、前后端交互,适合巩固基础并串联知识
  2. 实战性强:每个模块都有可运行的代码,解决了 “光看理论不会写” 的问题
  3. 工程化思维:封装工具类(JsonUtil、MysqlUtil)、模块解耦(用户管理、房间管理),符合企业开发规范

如果你能跟着文档完整实现一遍,不仅能熟练掌握 C++ 网络编程和数据库操作,还能理解 “服务器如何与前端实时交互” 的核心逻辑 —— 这对后续学习分布式系统、游戏开发等方向都有极大帮助。

最后,文档源码已开源(:https://gitee.com/qigezi/online_gobang.git),建议克隆下来对照代码逐行调试,遇到问题多查看文档中的 “常见问题” 部分,祝你开发顺利!

http://www.dtcms.com/a/572754.html

相关文章:

  • 基于传输熵理论的通信网络拓扑结构推理算法matlab仿真
  • 【基于one-loop-per-thread的高并发服务器】--- 前置技术
  • 企业级调度器 LVS 基础知识总结
  • 多线程异常、MQ、Kafka(八股)
  • 好的做蛋糕网站如何做网站淘客推广
  • 中国风网站建设网站建设信息平台
  • IndexedDB开发示例:面向对象的方式
  • GitLab CI/CD 集成 Harbor 全面教程
  • MySQL慢查询优化实战:从日志分析到SQL重构全流程
  • 每日一练 1(双指针)(单调性)
  • 从云平台到系统内核:SmartMediakit如何重构实时视频系统
  • XC6SLX45T-3FGG484I Xilinx Spartan-6 FPGA
  • 函数栈帧的创建与销毁详解(C语言拓展版)
  • 从 Grok 4 多智能体协同到 RAG 范式革命:2025 年 AI 工作流的技术重构生成
  • Macos系统上搭建Hadoop详细过程
  • 景德镇市城市建设规划网站安徽建设信息网
  • 11.5 LeetCode 题目汇总与解题思路
  • 三维空间变换:矩阵正交规范化的作用
  • E-House市场迎来爆发期?2025年全球规模与投资前景深度分析
  • 【尚庭公寓152-157】[第6天]【配置阿里云号码认证服务】
  • 使用DrissionPage实现携程酒店信息智能爬取
  • 数据结构之**二叉树**超全秘籍宝典2
  • win32k!ProcessKeyboardInput函数分析---登录界面ctrl+alt+del键的处理
  • 网站版权信息修改wordpress釆集插件破解
  • Springcloud_day01
  • 理解提示词Prompt
  • iOS 抓不到包怎么办?工程化排查与替代抓包方案(抓包/HTTPS/Charles代理/tcpdump)
  • 告别密码和防火墙——将 Git 仓库从 HTTPS 切换到 SSH 连接
  • Fiddler抓包工具详解,HTTP/HTTPS抓包、代理设置与调试技巧一站式教程(含实战案例)
  • Go语言爬虫:采集百度热榜并将拼装后的json写入txt文件