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

大家好!今天要带大家手把手实现一个网页版 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.6Ubuntu 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 步:

  1. 握手(协议升级) :客户端发送 HTTP 请求,附带Upgrade: WebSocket头,请求升级为 WebSocket 协议;服务器返回101 Switching Protocols,表示升级成功。
  2. 长连接通信:握手成功后,客户端和服务器可双向发送数据(无需重复建立连接)。
  3. 断开连接 :任意一方发送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"
  • 数字:直接写(如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 实战:序列化与反序列化
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

安装源(过程中需选择bionicmysql-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++" 练手项目,文档从环境搭建→核心技术→模块实现→客户端开发提供了完整流程,核心价值在于:

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

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

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

相关推荐
卡卡酷卡BUG4 小时前
2025年Java面试题及详细解答(MySQL篇)
java·开发语言·mysql
野生工程师4 小时前
【Python爬虫基础-1】爬虫开发基础
开发语言·爬虫·python
wuwu_q4 小时前
彻底讲清楚 Kotlin 的 when 表达式
android·开发语言·kotlin
一匹电信狗4 小时前
【C++】哈希表详解(开放定址法+哈希桶)
服务器·c++·leetcode·小程序·stl·哈希算法·散列表
Larry_Yanan4 小时前
QML学习笔记(五十一)QML与C++交互:数据转换——基本数据类型
c++·笔记·学习
北城以北88884 小时前
SSM--MyBatis框架之动态SQL
java·开发语言·数据库·sql·mybatis
梵尔纳多4 小时前
ffmpeg 使用滤镜实现播放倍速
c++·qt·ffmpeg
木易 士心4 小时前
Android 开发核心技术深度解析
android·开发语言·python
程序员烧烤5 小时前
【Java基础14】函数式接口、lamba表达式、方法引用一网打尽(下)
java·开发语言