☰
C++ WebSocket五子棋源码拆解:从连接匹配到落子广播的完整链路
2026/9/28 12:52:35 网站建设 项目流程

简介:这是一套面向C++后端与网络编程学习者的在线五子棋对战游戏完整源码,适合作为课程设计、毕业设计或个人练手项目,帮助理解WebSocket实时通信在真实业务中的落地方式。压缩包共33个文件,约6.13MB,以13个hpp头文件为核心,承载服务端会话、房间、匹配、数据库与日志等模块;另有4个HTML页面与4个CSS样式构成前端界面,配合JavaScript脚本、SQL建表文件、Makefile构建脚本及JSON配置,形成从注册登录到对战匹配的完整链路。项目实现用户注册、登录、对战匹配、实时对战与实时聊天等功能,目录按服务端、前端资源与工具类分层组织,便于按模块阅读与二次开发。目前已有406人学习,可作为掌握C++网络编程与前后端协作的实践参考。

1. 从一份 C++ 源码拆开 WebSocket 五子棋:它到底能跑出什么效果

很多人第一次看到「基于 WebSocket 的在线五子棋对战游戏设计源码」这类资源,第一反应是「又一个课程设计」。但真正拆开这份 33 个文件的包,你会发现它把 WebSocket 长连接、房间匹配、会话管理、MySQL 落库这几件事串成了一条完整链路,而不是只画个棋盘就交差。它用 C++ 写服务端,前端是 HTML + CSS + JavaScript,服务端核心逻辑集中在gobang目录下的server.hpp、room.hpp、session.hpp、matcher.hpp、online.hpp这几个头文件里,配合gobang.cc作为入口。能解决的核心问题是:让你在一个可编译、可运行的项目里,看到「两个浏览器窗口如何通过一条 WebSocket 连接实时同步落子、聊天、匹配对手」的全过程。适合已经学过 C++ 基础语法、想找一个能跑通网络编程和数据库交互的练手项目的人,也适合需要课程设计参考的在校生。它不教你从零写一个框架,但能让你把「连接建立 → 会话绑定 → 匹配房间 → 落子广播 → 数据落库」这条线走一遍。

2. 服务端骨架拆解:session、room、matcher 三件套怎么协作

2.1 从 gobang.cc 入口看服务启动顺序

拿到源码后不要急着编译,先看gobang.cc和server.hpp。这个项目的服务端启动逻辑通常是:初始化数据库连接池 → 创建 WebSocket 服务器实例 → 注册 HTTP 路由(登录、注册页面)→ 注册 WebSocket 回调(连接建立、消息到达、连接关闭)→ 启动监听。server.hpp里一般会封装一个Server类,内部持有online.hpp定义的在线用户管理器、matcher.hpp定义的匹配器、room.hpp定义的房间管理器。下面是一段典型的启动骨架,你对照自己拿到的源码看结构是否一致:

// gobang.cc 入口示意,具体函数名以你拿到的源码为准 #include "server.hpp" #include "db.hpp" int main() { // 1. 初始化 MySQL 连接池,db.hpp 里通常封装了 mysql_util gobang::DbManager::getInstance().init("127.0.0.1", "root", "password", "gobang", 3306); // 2. 创建服务器对象,绑定静态资源目录 wwwroot gobang::Server server(8080, "wwwroot"); // 3. 注册 WebSocket 事件回调 server.setOpenHandler([](gobang::SessionPtr session){ // 新连接加入在线列表 gobang::OnlineManager::getInstance().add(session); }); server.setMessageHandler([](gobang::SessionPtr session, const std::string& msg){ // 根据消息类型分发:匹配、落子、聊天、认输 gobang::Dispatcher::dispatch(session, msg); }); server.setCloseHandler([](gobang::SessionPtr session){ gobang::OnlineManager::getInstance().remove(session); gobang::Matcher::getInstance().remove(session); }); // 4. 阻塞运行 server.run(); return 0; }

逻辑说明:DbManager负责数据库连接,Server负责网络层,三个回调分别对应连接生命周期。参数说明:端口 8080 可改,静态目录wwwroot必须和实际 HTML 文件所在目录一致,数据库连接参数要和你本地 MySQL 匹配。常见做法是把这些配置写进config.json或直接硬编码,改的时候注意别漏了db.sql里的建表语句。

2.2 session 与 online:连接和用户的绑定关系

session.hpp和online.hpp是理解整个项目状态管理的关键。Session通常封装一条 WebSocket 连接,持有websocketpp::connection_hdl或类似句柄,以及用户 ID、用户名、当前所在房间 ID。OnlineManager则是一个全局单例,用unordered_map<uint64_t, SessionPtr>维护「用户 ID → 会话」的映射。为什么要有这一层?因为 WebSocket 连接本身只认句柄,不认业务身份,登录成功后必须把用户 ID 和句柄绑定,后续匹配、落子才能找到正确的人。

// session.hpp 关键字段示意 class Session { public: uint64_t userId = 0; // 登录后赋值 std::string username; uint64_t roomId = 0; // 0 表示未进房间 websocketpp::connection_hdl hdl; }; // online.hpp 关键方法示意 class OnlineManager { public: void add(SessionPtr s) { std::lock_guard<std::mutex> lk(mtx_); map_[s->userId] = s; } SessionPtr get(uint64_t uid) { std::lock_guard<std::mutex> lk(mtx_); return map_[uid]; } void remove(SessionPtr s) { std::lock_guard<std::mutex> lk(mtx_); map_.erase(s->userId); } private: std::unordered_map<uint64_t, SessionPtr> map_; std::mutex mtx_; // 多线程下必须加锁 };

逻辑说明:add在登录成功或连接建立后调用,get在需要给指定用户推送消息时调用。参数说明:userId来自数据库自增主键,roomId为 0 表示空闲。注意这里的std::mutex不能省,WebSocket 服务端通常是多线程的,不加锁会出现数据竞争,表现为「偶尔找不到对手」或「消息发错人」。

2.3 matcher 与 room:匹配队列和房间状态机

matcher.hpp负责把等待中的玩家两两配对,room.hpp负责一局游戏的状态。匹配器一般用一个std::queue<SessionPtr>或std::list存等待者,新玩家进来先入队,队列长度达到 2 就弹出两人创建房间。房间创建后要做的几件事:分配执黑执白、初始化棋盘数组、把房间 ID 写回两个 session、给双方推送「匹配成功」消息。下面是一个匹配逻辑的简化版:

// matcher.hpp 匹配核心示意 void Matcher::push(SessionPtr s) { std::lock_guard<std::mutex> lk(mtx_); queue_.push(s); if (queue_.size() >= 2) { auto p1 = queue_.front(); queue_.pop(); auto p2 = queue_.front(); queue_.pop(); // 创建房间,p1 执黑,p2 执白 uint64_t rid = RoomManager::getInstance().createRoom(p1, p2); p1->roomId = rid; p2->roomId = rid; // 推送匹配成功,前端据此跳转 game_room.html p1->send(R"({"type":"matched","roomId":)" + std::to_string(rid) + R"(,"color":"black"})"); p2->send(R"({"type":"matched","roomId":)" + std::to_string(rid) + R"(,"color":"white"})"); } }

逻辑说明:入队和配对必须在同一把锁内完成,否则两个线程可能同时判断size() >= 2导致重复配对。参数说明:color字段前端用来决定谁先手,黑棋先走。常见坑是匹配成功后没有及时把玩家从队列移除,导致同一个人被匹配两次,表现为「一局没结束又弹出新对局」。

3. 前端页面与 WebSocket 消息协议:落子、聊天、认输怎么传

3.1 login.html 与 register.html 的表单提交

wwwroot下的login.html、register.html、game_hall.html、game_room.html是四个核心页面。登录和注册走的是普通 HTTP 表单或 fetch 请求,服务端在server.hpp里注册对应路由,收到请求后调用db.hpp里的用户查询/插入逻辑。注册时密码通常做一次 MD5 或 SHA1 再存库,db.sql里能看到user表的字段定义。下面是一个前端注册请求的写法:

// register.html 中的提交逻辑示意 async function doRegister() { const username = document.getElementById('username').value.trim(); const password = document.getElementById('password').value.trim(); if (!username || !password) { alert('用户名和密码不能为空'); return; } const resp = await fetch('/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await resp.json(); if (data.code === 0) { alert('注册成功,请登录'); location.href = 'login.html'; } else { alert('注册失败:' + data.msg); } }

逻辑说明:前端只负责收集和发送,校验逻辑服务端必须再做一遍。参数说明:Content-Type必须是application/json,否则服务端解析会失败。常见坑是注册成功后没有跳转登录页,或者服务端返回的code字段和前端判断不一致,导致「明明注册成功却提示失败」。

3.2 game_hall.html 发起匹配与接收 matched 消息

登录成功后进入game_hall.html,这个页面建立 WebSocket 连接并发送匹配请求。连接地址一般是ws://localhost:8080/ws或类似路径,具体看server.hpp里注册的 WebSocket 路由。连接建立后,前端要监听onmessage,根据type字段区分消息类型。匹配请求发出去后,服务端matcher.hpp处理,配对成功推matched消息,前端收到后跳转game_room.html并把房间 ID 和执棋颜色存进sessionStorage。

// game_hall.html 中的 WebSocket 连接与匹配 const ws = new WebSocket('ws://' + location.host + '/ws'); ws.onopen = () => { // 连接建立后发送匹配请求 ws.send(JSON.stringify({ type: 'match' })); }; ws.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.type === 'matched') { sessionStorage.setItem('roomId', msg.roomId); sessionStorage.setItem('color', msg.color); location.href = 'game_room.html'; } }; ws.onclose = () => { console.log('连接断开,尝试重连'); };

逻辑说明:onopen里发匹配请求,onmessage里处理服务端推送。参数说明:location.host自动取当前域名和端口,避免硬编码。注意 WebSocket 地址的协议是ws://,如果页面是 HTTPS 则要用wss://。常见坑是页面跳转后原来的 WebSocket 连接没有关闭,导致服务端认为玩家还在大厅,匹配逻辑出现重复。

3.3 game_room.html 落子广播与聊天消息

game_room.html是核心对战页。棋盘一般用 Canvas 或 CSS 网格绘制,点击事件换算成行列坐标,发送{type:'move', x, y}给服务端。服务端room.hpp校验是否轮到该玩家、该位置是否为空,合法则更新棋盘、广播给房间内两人、检查五连。聊天消息则是{type:'chat', content},服务端直接转发给对手。下面是一段落子发送和接收的代码:

// game_room.html 落子与消息处理 const ws = new WebSocket('ws://' + location.host + '/ws'); const myColor = sessionStorage.getItem('color'); // black 或 white let isMyTurn = (myColor === 'black'); ws.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.type === 'move') { drawPiece(msg.x, msg.y, msg.color); // 在棋盘上画子 isMyTurn = (msg.color !== myColor); // 切换回合 } else if (msg.type === 'chat') { appendChat(msg.from, msg.content); } else if (msg.type === 'gameover') { alert(msg.winner === myColor ? '你赢了' : '你输了'); } }; function onBoardClick(x, y) { if (!isMyTurn) { alert('还没轮到你'); return; } ws.send(JSON.stringify({ type: 'move', x, y })); }

逻辑说明:isMyTurn由收到的落子颜色反推,保证双方状态一致。参数说明:x、y是棋盘坐标,通常 0 到 14。常见坑是前端没有做「该位置已有棋子」的本地判断,导致重复发送同一位置,服务端虽然会拒绝但用户体验差。另一个坑是聊天消息没有做长度限制,长文本可能撑爆消息帧。

4. 数据库与编译部署:db.sql 建表、Makefile 编译、静态资源挂载

4.1 db.sql 建表与用户表字段

db.sql是项目落库的入口,通常包含user表(存用户名、密码哈希、注册时间)和可选的game_record表(存对局记录)。导入方式很简单,用 MySQL 命令行或图形工具执行即可。下面是一个典型的建表语句,你对照自己拿到的db.sql看字段是否一致:

-- db.sql 建表示意 CREATE DATABASE IF NOT EXISTS gobang DEFAULT CHARSET utf8mb4; USE gobang; CREATE TABLE IF NOT EXISTS user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password CHAR(32) NOT NULL, -- MD5 后 32 位 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS game_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, black_id INT UNSIGNED NOT NULL, white_id INT UNSIGNED NOT NULL, winner_id INT UNSIGNED, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:username加唯一索引防止重复注册,password存哈希不存明文。参数说明:CHAR(32)对应 MD5 长度,如果你改用 SHA256 要改成CHAR(64)。常见坑是数据库字符集不是utf8mb4,中文用户名会乱码;另一个坑是db.hpp里的连接参数和db.sql里的库名不一致,表现为「连不上数据库」。

4.2 Makefile 编译与依赖检查

项目根目录有Makefile,说明编译方式已经给你写好了。通常依赖g++、mysqlclient、websocketpp、jsoncpp或nlohmann/json。编译前先确认这些库是否安装,缺哪个补哪个。下面是一个典型的编译流程:

# 查看 Makefile 内容,确认编译目标和依赖 cat Makefile # 安装常见依赖(Ubuntu/Debian 示例) sudo apt-get install g++ make libmysqlclient-dev libboost-system-dev # 编译 make # 如果报找不到 websocketpp,通常是头文件库,下载后放到 include 路径 # 如果报 json 相关错误,检查 util/json_util.hpp 用的是哪个 json 库

逻辑说明:make会根据Makefile里的规则编译gobang.cc和各个.hpp。参数说明:libmysqlclient-dev提供 MySQL C API 头文件,libboost-system-dev是 websocketpp 的常见依赖。常见坑是Makefile里的路径写死了作者本机的路径,需要手动改成你的路径;另一个坑是 C++ 标准版本,如果代码用了 C++11 以上特性,Makefile里要有-std=c++11或更高。

4.3 静态资源挂载与端口配置

wwwroot目录下的 HTML、CSS、JS、图片需要被服务端正确挂载,否则浏览器访问会 404。server.hpp里一般会设置静态文件根目录,收到 HTTP 请求时先查静态文件,找不到再走业务路由。端口默认可能是 8080 或 9000,改端口要同时改前端 WebSocket 地址或确保前端用的是location.host动态获取。下面是一个静态资源挂载的配置示意:

// server.hpp 静态资源与路由注册示意 Server::Server(int port, const std::string& wwwroot) : port_(port), wwwroot_(wwwroot) { // 注册静态文件处理:/login.html /css/style.css /js/game.js 等 server_.set_http_handler([this](auto hdl, auto msg){ auto req = server_.get_con_from_hdl(hdl)->get_request(); std::string path = wwwroot_ + req.get_uri(); if (file_util::exists(path)) { // 读取文件并返回,Content-Type 根据后缀设置 server_.send(hdl, file_util::read(path), websocketpp::http::status_code::ok); } else { server_.send(hdl, "404", websocketpp::http::status_code::not_found); } }); }

逻辑说明:静态文件优先匹配,匹配不到返回 404。参数说明:wwwroot_必须是绝对路径或相对于运行目录的正确路径。常见坑是运行目录不对,比如在build目录下执行却把wwwroot放在上一级,导致所有页面 404;另一个坑是 CSS 和 JS 的引用路径用了绝对路径/css/style.css,但服务端没有正确映射。

5. 避坑与排查:连接、匹配、落子、数据库四类高频问题

5.1 WebSocket 连接建立但收不到消息

现象:浏览器控制台显示 WebSocket 已连接,但发送匹配请求后没有任何反应。原因通常是服务端消息回调没有正确注册,或者消息分发逻辑里type字段判断不匹配。解决:在server.hpp的setMessageHandler里加日志,打印收到的原始消息;检查前端发送的 JSON 字段名和服务端解析的字段名是否一致,比如前端发type服务端读msg_type就会静默失败。

5.2 匹配成功但进入房间后棋盘不同步

现象:两个玩家都收到matched消息并跳转,但一方落子另一方看不到。原因一般是房间内广播时只发给了自己,或者room.hpp里保存的 session 句柄失效。解决:检查RoomManager::broadcast是否遍历了房间内两个 session 并分别调用send;确认 session 在跳转页面后没有重新建立连接导致旧句柄失效。常见做法是房间内保存用户 ID,广播时通过OnlineManager重新获取当前有效 session。

5.3 落子后服务端不校验回合导致连下

现象:一方可以连续落子,对手没有机会。原因通常是room.hpp里没有维护currentTurn状态,或者校验逻辑写反了。解决:在房间对象里加一个currentColor字段,每次落子后切换;收到move消息时先判断session->color == currentColor,不相等直接丢弃并回错误消息。参数说明:color在匹配成功时分配,黑先白后。

5.4 数据库连接失败或中文乱码

现象:注册时提示失败,或者用户名显示为问号。原因可能是 MySQL 服务未启动、连接参数错误、字符集不是utf8mb4。解决:先用命令行mysql -u root -p确认能登录;检查db.hpp里的 host、user、password、dbname 四个参数;建库时指定DEFAULT CHARSET utf8mb4,连接时执行SET NAMES utf8mb4。常见坑是密码里有特殊字符没有转义,导致连接字符串解析错误。

5.5 编译报错找不到头文件

现象:make时报fatal error: websocketpp/...: No such file or directory。原因是对应库没有安装或头文件路径不对。解决:确认websocketpp是头文件库,下载后把整个目录放到/usr/local/include或项目include目录;Makefile里用-I指定路径。另一个常见坑是json_util.hpp依赖的 json 库版本不兼容,换用头文件版本的nlohmann/json通常能解决。

6. 进阶验证:用 wscat 和浏览器双开做端到端联调

把项目跑起来只是第一步,真正要确认它「能用」,得做端到端验证。我一般会先用wscat模拟一个客户端,手动发匹配和落子消息,看服务端返回是否符合预期。wscat是个命令行 WebSocket 客户端,安装和用法如下:

# 安装 wscat npm install -g wscat # 连接服务端 wscat -c ws://localhost:8080/ws # 连接成功后手动发送匹配请求 {"type":"match"} # 再开一个终端连接第二个客户端,同样发送匹配 # 观察两个终端是否都收到 matched 消息

逻辑说明:wscat能让你绕过前端页面直接和服务端对话,快速定位是前端问题还是服务端问题。参数说明:-c后面跟完整的 WebSocket 地址,注意协议是ws://不是http://。如果wscat连不上但浏览器能连上,检查是不是服务端对路径做了区分,比如浏览器走/ws而wscat少写了路径。

验证完连接层,再用浏览器双开做完整对局。开两个不同浏览器或无痕窗口,分别注册两个账号,登录后同时点匹配,确认能配对成功并进入同一房间。然后交替落子,观察棋盘是否同步、聊天是否互通、五连是否判胜。这里有个容易被忽略的点:五连判断要在服务端做,不能只靠前端。前端判断容易被篡改,而且双方状态可能不一致。服务端room.hpp里应该有一个checkWin(x, y, color)函数,从落子点向四个方向延伸计数,任一方向达到 5 就判胜并广播gameover。

// room.hpp 五连判断示意 bool Room::checkWin(int x, int y, int color) { // 四个方向:横、竖、左上-右下、右上-左下 int dx[] = {1, 0, 1, 1}; int dy[] = {0, 1, 1, -1}; for (int d = 0; d < 4; ++d) { int count = 1; // 正方向延伸 for (int i = 1; i < 5; ++i) { int nx = x + dx[d] * i, ny = y + dy[d] * i; if (nx < 0 || nx >= 15 || ny < 0 || ny >= 15) break; if (board_[nx][ny] != color) break; ++count; } // 反方向延伸 for (int i = 1; i < 5; ++i) { int nx = x - dx[d] * i, ny = y - dy[d] * i; if (nx < 0 || nx >= 15 || ny < 0 || ny >= 15) break; if (board_[nx][ny] != color) break; ++count; } if (count >= 5) return true; } return false; }

逻辑说明:从落子点向四个方向的正反两侧计数,总数达到 5 即胜。参数说明:board_是 15×15 的二维数组,0 表示空,1 表示黑,2 表示白。注意边界判断不能少,否则数组越界会直接崩溃。常见坑是只检查了正方向没检查反方向,导致「中间落子连成五连却不判胜」。

最后说一个我自己的习惯:每次拿到这类源码,先不改任何代码,按原样编译跑通一遍,确认「作者的环境能跑」;然后再逐步替换成自己的数据库密码、端口、路径,每改一处就重启验证一次。这样出问题时能快速定位是哪一步引入的。从那以后我每次拆新项目都强制走一遍「原样跑通 → 单点替换 → 端到端验证」的流程,省了很多来回排查的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询