
大家好!今天要带大家手把手实现一个网页版 C++ 在线五子棋对战项目------ 这个项目不仅能让你巩固 C++ 核心语法,还能串联起网络编程(WebSocket)、数据库(MySQL)、前后端交互(HTML/JS/AJAX)等多个技术领域,是提升全栈开发能力的绝佳练手项目。
本文会从环境搭建 到核心技术拆解 ,再到模块实现 和客户端开发,每一步都附带详细代码和注释,即使是 C++ 新手也能跟着做出来。话不多说,咱们开干!
一、项目先览:我们要做什么?
在动手前,先明确项目的核心目标和技术栈,做到 "心中有数"。
1.1 核心功能
这个在线五子棋对战系统主要实现 3 大核心能力:
- 用户管理:注册、登录、查询用户信息(天梯分、比赛场次、胜率)
- 匹配对战:根据天梯分匹配同水平玩家,进入房间实时对战(15x15 棋盘,五子连珠获胜)
- 实时聊天:对战时玩家可发送消息,支持敏感词过滤
1.2 开发环境
项目基于 Linux 系统开发,支持两种主流发行版,下表列出了关键工具和版本:
| 类别 | 推荐配置 | 说明 |
|---|---|---|
| 操作系统 | CentOS 7.6 / Ubuntu 22.04 | 服务器端开发首选,稳定且兼容性好 |
| 代码编辑器 | VSCode / Vim | VSCode 适合新手(带语法提示),Vim 适合服务器操作 |
| 编译器 / 调试器 | g++ 7.3+ / gdb | 需支持 C++11 标准(项目大量用智能指针、lambda) |
| 项目构建工具 | Makefile / CMake | 管理代码编译流程,避免手动敲命令 |
| 版本控制 | Git | 管理代码版本,方便回溯 |
1.3 核心技术栈
项目是 "C++ 后端 + 前端 + 数据库" 的组合,每个技术都有明确的作用:
| 技术 | 作用 | 为什么选它? |
|---|---|---|
| HTTP/WebSocket | 前后端通信协议 | HTTP 用于短连接(注册 / 登录),WebSocket 用于长连接(实时对战 / 聊天) |
| WebSocket++ | C++ 实现 WebSocket 的库 | 跨平台、仅头文件、支持 HTTP/WebSocket 双协议 |
| JsonCpp | JSON 序列化 / 反序列化库 | 处理前后端数据交互(比如用户信息、下棋请求) |
| MySQL | 关系型数据库 | 存储用户数据(用户名、密码、天梯分) |
| C++11 | C++ 标准 | 智能指针(shared_ptr)、线程(thread)、lambda 表达式简化代码 |
| HTML/CSS/JS/AJAX | 前端页面开发 | 实现用户交互界面,发送请求到后端 |
1.4 代码获取
项目源码已开源,直接克隆即可:
bash
git clone https://gitee.com/small-entrepreneur/personal-project.git
二、环境搭建:从 0 到 1 配置开发环境
环境搭建是项目的第一步,也是最容易踩坑的一步。本节会分别讲解CentOS 7.6 和Ubuntu 22.04的配置流程,每个命令都附带注释,确保你能一次成功。
2.1 通用准备:理解 "软件源" 的重要性
Linux 系统默认软件源可能在国外,下载速度慢且容易失败,因此第一步先更换国内源(阿里源 / 清华源),后续安装工具会顺畅很多。
2.2 CentOS 7.6 环境搭建(详细步骤)
2.2.1 安装基础工具(wget、lrzsz)
wget:用于下载文件(比如软件源、库源码)lrzsz:用于 Windows 和 Linux 之间传输文件(rz上传,sz下载)
bash
# 安装wget
sudo yum install wget -y
# 安装lrzsz(传输文件用)
sudo yum install lrzsz -y
# 验证lrzsz是否安装成功(显示版本即成功)
rz --version
2.2.2 更换 CentOS 官方源为阿里源
bash
# 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 版本:
bash
# 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 源:
bash
wget http://repo.mysql.com/mysql57-community-release-el7-10.noarch.rpm
安装官方源:
bash
sudo rpm -ivh mysql57-community-release-el7-10.noarch.rpm
安装 MySQL 服务:
bash
sudo yum install -y mysql-community-server
解决 GPG 密钥过期问题(若安装时报错):
bash
# 导入新的GPG密钥
sudo rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022
# 重新安装
sudo yum install -y mysql-community-server
安装 MySQL 开发包(C++ 连接 MySQL 需要):
bash
sudo yum install -y mysql-community-devel
配置 MySQL 字符集为 UTF-8(避免中文乱码):
bash
# 编辑MySQL配置文件
sudo vim /etc/my.cnf
# 在文件中添加以下内容:
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
character-set-server=utf8
启动 MySQL 服务并设置开机自启:
bash
# 启动服务
sudo systemctl start mysqld
# 设置开机自启(避免重启后需要手动启动)
sudo systemctl enable mysqld
# 查看服务状态(显示active(running)即成功)
sudo systemctl status mysqld
获取 MySQL 临时密码(首次登录用):
bash
sudo grep 'temporary password' /var/log/mysqld.log
# 输出示例:A temporary password is generated for root@localhost: abc123!@#
登录 MySQL 并修改密码:
bash
# 登录(输入上面获取的临时密码)
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 是仅头文件库,需要手动编译安装:
bash
# 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)
bash
# 安装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 源为阿里源
bash
# 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 安装基础工具和依赖库
bash
# 安装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,需指定版本)
bash
# 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 步:
- 握手(协议升级) :客户端发送 HTTP 请求,附带
Upgrade: WebSocket头,请求升级为 WebSocket 协议;服务器返回101 Switching Protocols,表示升级成功。 - 长连接通信:握手成功后,客户端和服务器可双向发送数据(无需重复建立连接)。
- 断开连接 :任意一方发送
opcode=0x8的帧,关闭连接。
3.1.2 WebSocket 报文格式(关键字段)
WebSocket 数据以 "帧" 为单位传输,每个帧的结构如下(重点看这几个字段):
| 字段 | 含义 |
|---|---|
| FIN | 1 表示当前帧是消息的最后一帧(比如长消息分多帧发送,最后一帧 FIN=1) |
| opcode | 数据类型:0x1 = 文本帧(聊天消息)、0x2 = 二进制帧、0x8 = 关闭连接、0x9=Ping |
| Mask | 1 表示客户端发送的数据需要用 Mask-Key 解密(服务器发送给客户端不需要) |
| Payload Length | 数据长度:0~126 直接表示长度;126 表示后续 2 字节是长度;127 表示后续 8 字节 |
3.1.3 WebSocketpp 实战:实现简单的 "回声服务器"
用 WebSocketpp 写一个服务器,客户端发送什么消息,服务器就返回什么消息(回声功能),帮你理解核心接口。
服务器代码(echo_server.cpp)
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 连接:
html
<!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>
测试步骤
编译服务器代码:
bash
g++ echo_server.cpp -o echo_server -std=c++11 -lpthread -lboost_system
运行服务器:
bash
./echo_server
用浏览器打开echo_client.html,输入消息并发送,打开浏览器控制台(F12),可看到如下日志:
bash
已连接到服务器!
发送消息:hello
收到服务器回声:hello
3.2 JsonCpp:处理前后端数据交互
前后端通信需要统一的数据格式,JSON 是首选(比 XML 更简洁)。JsonCpp 用于 C++ 代码中 "序列化"(C++ 对象→JSON 字符串)和 "反序列化"(JSON 字符串→C++ 对象)。
3.2.1 JSON 数据格式入门
比如表示一个用户信息,C++ 结构体和 JSON 的对比:
C++ 结构体:
css
struct User {
char* username = "xiaobai";
int age = 18;
float score[3] = {88.5, 99, 58};
};
JSON 格式:
css
{
"username": "xiaobai",
"age": 18,
"score": [88.5, 99, 58]
}
JSON 的核心数据类型:
- 对象:用
{}包裹,键值对形式("key": value) - 数组:用
[]包裹,元素可以是任意类型(如[1, "a", true]) - 字符串:用
""包裹(如"username") - 数字:直接写(如
18、88.5)
3.2.2 JsonCpp 核心类与用法
JsonCpp 有 3 个核心类,掌握它们就能应对 90% 的场景:
Json::Value:表示 JSON 数据(对象、数组、字符串等),支持[]和赋值操作。Json::StreamWriter:将Json::Value序列化为 JSON 字符串。Json::CharReader:将 JSON 字符串反序列化为Json::Value。
3.2.3 实战:序列化与反序列化
cpp
#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;
}
编译运行
bash
# 编译(-ljsoncpp链接JsonCpp库)
g++ json_demo.cpp -o json_demo -std=c++11 -ljsoncpp
# 运行
./json_demo
输出结果
bash
序列化结果:
{
"age" : 18,
"score" : [ 88.5, 99, 58 ],
"username" : "xiaobai"
}
反序列化结果:
username: xiaobai
age: 18
score: 88.5 99 58
3.2.4 封装 Json 工具类(复用代码)
项目中会频繁用到序列化和反序列化,封装成工具类可减少冗余:
cpp
#include <jsoncpp/json/json.h>
#include <string>
class JsonUtil {
public:
// 序列化:Json::Value → std::string
static 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::Value
static 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;
}
};
使用示例:
cpp
// 序列化
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 中创建数据库和表:
sql
-- 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++ 代码实现增删改查:
cpp
#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());
// 执行SQL
if (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());
// 执行SQL
if (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. 初始化并连接MySQL
MYSQL* mysql = InitMySQL();
if (mysql == NULL) {
return -1;
}
// 2. 新增用户(测试注册)
// AddUser(mysql, "xiaohong", "456");
// 3. 查询用户(测试登录)
QueryUser(mysql, "xiaobai", "123");
// 4. 关闭连接
mysql_close(mysql);
return 0;
}
编译运行
bash
# 编译(-lmysqlclient链接MySQL库)
g++ mysql_demo.cpp -o mysql_demo -std=c++11 -L/usr/lib64/mysql -lmysqlclient
# 运行
./mysql_demo
输出结果
cpp
连接MySQL成功!
用户信息:
ID:1
天梯分:1000
总场次:0
获胜场次:0
四、项目模块实现:从 "技术" 到 "产品" 的落地
掌握核心技术后,我们开始搭建项目的整体架构。项目分为 3 大模块:数据管理模块 (MySQL)、业务处理模块 (C++ 后端)、前端界面模块(HTML/JS)。
4.1 项目架构设计:模块划分与交互
先看整体架构图,理解模块之间的关系:
bash
用户 → 前端界面(登录/大厅/房间)→ 业务处理模块(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)
cpp
#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]); // 用户ID
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;
}
// 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)实现
cpp
#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; // 房间ID
RoomStatus _status; // 房间状态(游戏中/已结束)
int _player_count; // 玩家数量(固定2人)
uint64_t _white_uid; // 白棋玩家ID
uint64_t _black_uid; // 黑棋玩家ID
UserTable* _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)) { // 反斜(右上→左下)
// 返回获胜玩家ID
return 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)) { // 棋盘初始化为全0
std::cout << "房间" << _room_id << "创建成功!" << std::endl;
}
// 析构函数
~Room() {
std::cout << "房间" << _room_id << "销毁成功!" << std::endl;
}
// 获取房间ID
uint64_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++;
}
// 获取白棋玩家ID
uint64_t GetWhiteUid() const { return _white_uid; }
// 获取黑棋玩家ID
uint64_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 匹配队列实现(线程安全)
cpp
#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 匹配管理器实现
cpp
#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. 等待队列人数>=2
queue.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 请求到后端验证,成功后跳转游戏大厅。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>五子棋对战 - 登录</title>
<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", // 数据格式为JSON
data: 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 与聊天区域)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>五子棋对战 - 游戏房间</title>
<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),半径13px
context.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,半径13px
15 + 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_id
let 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 默认源在国内下载慢,需替换为阿里源:
备份原源文件:
bash
sudo cp /etc/apt/sources.list.d/original.list /etc/apt/sources.list.d/original.list.bak
编辑源文件:
bash
sudo vim /etc/apt/sources.list.d/original.list
替换源地址(Vim 底行模式执行):
bash
:%s/cn.archive.ubuntu.com/mirrors.aliyun.com/g
(含义:将所有cn.archive.ubuntu.com替换为阿里源mirrors.aliyun.com)
更新源缓存:
bash
sudo apt update
6.2 安装基础工具
bash
# 安装文件传输工具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 依赖)
bash
sudo apt install libboost-all-dev -y
# 验证:检查头文件是否存在
ls /usr/include/boost/version.hpp # 应显示文件路径
6.3.2 JsonCpp 库(JSON 处理)
bash
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 源:
bash
wget http://repo.mysql.com/mysql-apt-config_0.8.12-1_all.deb
安装源(过程中需选择bionic和mysql-5.7):
bash
sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb
解决 "无 Release 文件" 错误(文档中常见问题):
bash
sudo vim /etc/apt/sources.list
# 删除含"file:/cdrom"的行,保存退出
导入 GPG 密钥(解决过期问题):
bash
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
更新源并安装 MySQL 5.7:
bash
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 库(手动编译)
bash
# 克隆源码
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-8 | 1. 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++" 练手项目,文档从环境搭建→核心技术→模块实现→客户端开发提供了完整流程,核心价值在于:
- 技术栈全面:覆盖 C++11、WebSocket、MySQL、前后端交互,适合巩固基础并串联知识
- 实战性强:每个模块都有可运行的代码,解决了 "光看理论不会写" 的问题
- 工程化思维:封装工具类(JsonUtil、MysqlUtil)、模块解耦(用户管理、房间管理),符合企业开发规范
如果你能跟着文档完整实现一遍,不仅能熟练掌握 C++ 网络编程和数据库操作,还能理解 "服务器如何与前端实时交互" 的核心逻辑 ------ 这对后续学习分布式系统、游戏开发等方向都有极大帮助。
最后,文档源码已开源(:https://gitee.com/qigezi/online_gobang.git),建议克隆下来对照代码逐行调试,遇到问题多查看文档中的 "常见问题" 部分,祝你开发顺利!