简介这是一套面向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_mapuint64_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_guardstd::mutex lk(mtx_); map_[s-userId] s; } SessionPtr get(uint64_t uid) { std::lock_guardstd::mutex lk(mtx_); return map_[uid]; } void remove(SessionPtr s) { std::lock_guardstd::mutex lk(mtx_); map_.erase(s-userId); } private: std::unordered_mapuint64_t, SessionPtr map_; std::mutex mtx_; // 多线程下必须加锁 };逻辑说明add在登录成功或连接建立后调用get在需要给指定用户推送消息时调用。参数说明userId来自数据库自增主键roomId为 0 表示空闲。注意这里的std::mutex不能省WebSocket 服务端通常是多线程的不加锁会出现数据竞争表现为「偶尔找不到对手」或「消息发错人」。2.3 matcher 与 room匹配队列和房间状态机matcher.hpp负责把等待中的玩家两两配对room.hpp负责一局游戏的状态。匹配器一般用一个std::queueSessionPtr或std::list存等待者新玩家进来先入队队列长度达到 2 就弹出两人创建房间。房间创建后要做的几件事分配执黑执白、初始化棋盘数组、把房间 ID 写回两个 session、给双方推送「匹配成功」消息。下面是一个匹配逻辑的简化版// matcher.hpp 匹配核心示意 void Matcher::push(SessionPtr s) { std::lock_guardstd::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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明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 标准版本如果代码用了 C11 以上特性Makefile里要有-stdc11或更高。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 表示白。注意边界判断不能少否则数组越界会直接崩溃。常见坑是只检查了正方向没检查反方向导致「中间落子连成五连却不判胜」。最后说一个我自己的习惯每次拿到这类源码先不改任何代码按原样编译跑通一遍确认「作者的环境能跑」然后再逐步替换成自己的数据库密码、端口、路径每改一处就重启验证一次。这样出问题时能快速定位是哪一步引入的。从那以后我每次拆新项目都强制走一遍「原样跑通 → 单点替换 → 端到端验证」的流程省了很多来回排查的时间。希望帮到你。本文还有配套的精品资源点击获取