简介这是一份基于C/C实现的网络联机五子棋小游戏完整源码项目分为QT客户端与Linux服务端两部分。客户端界面采用QT框架构建网络模块基于Windows平台socket开发服务端运行于Linux使用Linux socket实现通信整体支持公网联机对局适合学习QT界面编程、socket通信及C/S架构的读者。压缩包共28个文件、约559KB包含6个cpp源文件、4个h头文件、3个ui界面文件、1个pro工程文件及makefile另有9个png、1个jpg及1个ico图片资源用于菜单、按钮等界面美化目录分为客户端与服务端结构清晰便于直接编译与二次开发。目前已有1399人学习下载。通过该源码可直观掌握联机五子棋从界面搭建、棋盘绘制到数据收发、对局状态同步的完整实现思路可作为相关课程设计或毕业设计的基础参考。1. 网络联机五子棋小游戏源码C里真正值钱的部分是什么一套网络联机五子棋小游戏源码C看着像新手练手项目但对多数人来说真正卡住你的不是下棋规则也不是画棋盘而是两台电脑之间那几行 socket 代码。你写好了 15×15 的棋盘能落子也能判胜负但一接局域网就露馅对方连不上、落子不显示、走几步整个程序卡死。这份源码应该覆盖的最小闭环是一端等待连接一端主动连入双方轮流落子任意一方检测到五连就广播结果并结束对局。适合刚学完 C 基础、想拿 socket 编程做第一个实战项目的人也适合需要课程设计或毕业设计的同学拿来扩展成带界面的成品。2. 拆开C联机五子棋源码的三层结构socket通信、状态机、线程分工拿到这类源码别急着读 main 函数先看网络数据是从哪个线程进来、棋盘状态被谁修改、对局流程靠什么切换。我拆网络联机五子棋的习惯是把代码分成三层看最底层是 socket 收发包中间层是消息队列和状态机最上层才是棋盘逻辑和界面输出。只要把这三层之间的边界切清楚联机五子棋这个 c 小游戏代码的全部逻辑就藏不住了。2.1 为什么消息要选 TCP 而不是 UDPWinsock 与 POSIX socket 怎么取舍五子棋每回合传输的数据量只有几十字节照理说 UDP 也能塞得下。但联机对局里消息的顺序就是棋局的顺序后手必须先收到上一手才能接收下一手。UDP 不保证有序极端情况下会出现对方刚落第 5 手你这边先收到第 6 手判负逻辑当场错乱。所以这套源码里网络联机这一层几乎不用考虑直接选 TCP。Windows 下的 C 网络编程默认走 Winsock头文件用 winsock2.h链接库是 ws2_32。如果你想把同一份源码移植到 Linux改动集中在三处socket 头文件换成 sys/socket.h关闭 socket 的 closesocket 换 close初始化 WSAStartup 那一段直接删掉。我一般建议读者先在本机 Windows 上把 Winsock 跑通再考虑跨平台否则同时调试两套网络 API 会把自己绕晕。2.2 网络线程与逻辑线程消息队列是最稳的粘合方式很多翻车现场长这样网络线程 recv 到数据后顺手就把棋盘数组改了然后界面线程再读棋盘拿去画。Windows 下两个线程同时读写一个二维数组表面看偶尔能跑实际数据竞争严重轻则棋子显示错位重则直接崩溃。我一般的做法是加一个消息队列网络线程只负责接收原始消息并封装成结构体丢进队列游戏逻辑线程从队列里取消息再落子。这样网络线程从不直接接触棋盘锁的粒度也小很多。核心代码示意如下#include queue #include mutex #include condition_variable // 网络线程 - 逻辑线程的消息体 struct NetMsg { int type; // 0对方连接成功 1对方落子 2对方认输 3对方断开 int row; // type 为 1 时有效 int col; // type 为 1 时有效 int player; // 1黑棋 2白棋 }; std::queueNetMsg g_msgQueue; std::mutex g_mtx; std::condition_variable g_cv; // 网络线程调用收完包之后投递消息 void PostMessage(const NetMsg msg) { std::lock_guardstd::mutex lock(g_mtx); g_msgQueue.push(msg); g_cv.notify_one(); } // 逻辑线程调用取出一个待处理消息没有就返回 false bool PollMessage(NetMsg out) { std::lock_guardstd::mutex lock(g_mtx); if (g_msgQueue.empty()) return false; out g_msgQueue.front(); g_msgQueue.pop(); return true; }逻辑说明PostMessage 是唯一写入队列的入口由网络线程调用PollMessage 由主循环调用。所有网络数据都先落在这个队列里主循环每帧检查一次处理完一条再处理下一条。参数说明NetMsg 里的 player 字段很重要它是解决“双方各自判断棋盘”的一个前提条件。type 为 0 的消息用于通知逻辑层连接已建立可以进入等待落子状态type 为 3 的消息用于对局中途对方异常断开时触发清理逻辑而不是让程序卡死在 recv 调用里。消息队列的容量在这里不需要刻意设上限因为五子棋一整局最多 225 手队列深度按消息条数算很难被撑爆。但要注意队列里同时只会存在同一方的连续落子消息因为网络线程收到的每一条消息都会立刻被 push没有攒批机制。2.3 用状态机管住对局流程等待、连接、对局、结束联机五子棋有一个很容易踩的坑双方还没有建立连接时落子消息已经发出去了。socket 层的 send 确实能调用成功但对面根本没有进程在收这手棋就丢了。引入一个简单的状态机可以把这个边界管得死死的。我在源码里通常会定义四个状态enum GameState { STATE_WAIT_CONN, // 服务端等待客户端接入 STATE_CONNECTED, // 已建立连接尚未落子 STATE_PLAYING, // 对局中 STATE_GAME_OVER // 本局结束 };状态迁移规则只有四条服务端 accept 成功后从 WAIT_CONN 进入 CONNECTED收到对方第一条落子消息或本方落下第一子时从 CONNECTED 进入 PLAYING任意一方检测到五连或一方认输从 PLAYING 进入 GAME_OVERGAME_OVER 状态下只有收到“再来一局”消息才能重新回 CONNECTED。之所以把 CONNECTED 单独拎出来是因为先手方的初始化时机需要依赖它。比如服务端默认执黑那么服务端在 accept 完成后就可以立即落子客户端执白则必须先等到对方第一条消息到达才能在自己这边激活落子按钮。没有这个状态你很可能会写出“谁先 connect 谁先走”之类的混乱轮转逻辑对局起来完全不可控。2.4 一局棋的完整消息流从 accept 到 game over把两个端合起来看一局棋的消息流是可以走通的服务端创建 socketbind 到 8888 端口listen 后阻塞在 accept客户端 connect 成功后服务端 accept 返回双方各自把状态置为 CONNECTED。服务端执黑下 7,7 天元发送 MOVE:7,7 给客户端客户端收到后依次执行落子到本地棋盘、切换轮次、判断本局是否可继续。随后客户端落子 8,8发送给服务端。直到某一方在自己落子之后调用五连判定得到 true则发送 WIN:row,col 消息对方收到后也在本地棋盘做一次验证双方进入 GAME_OVER。这套消息流里没有任何一方是“绝对权威”双方各自维护一份棋盘拷贝并信任对方发来的落子坐标。对于一个联机五子棋小游戏来说这种设计已经足够而且实现最简单。如果你要做更严谨的版本可以让服务端做唯一裁决客户端所有落子先发给服务端服务端判定合法后再广播但那样代码量会多出一截先跑通再升级不迟。3. 把五子棋规则写成可复测的C函数棋盘、落子、四方向五连判定网络联机部分的框架搭好之后下一步是把五子棋规则做成不依赖网络的纯 C 函数。这一步做得好后面联机调试时你会省很多事因为所有胜负判断都可以在本地先用离线数据验证一遍不用开着两个窗口反复试。3.1 用二维数组表示棋盘0 空白、1 黑棋、2 白棋五子棋棋盘用 int board[15][15] 就够了不需要链表也不需要 bitboard。角标从 0 到 14board[row][col] 存 0 表示空格1 表示黑子2 表示白子。这样做的原因是判定函数需要频繁随机访问棋盘上的任意位置二维数组的寻址开销最低后续控制台输出也用同一个数组驱动。constexpr int BOARD_SIZE 15; int board[BOARD_SIZE][BOARD_SIZE] {0}; void PrintBoard(const int b[BOARD_SIZE][BOARD_SIZE]) { // 打印列坐标提示行方便联机时对齐坐标 std::cout ; for (int c 0; c BOARD_SIZE; c) { std::cout (c 10 ? : ) c ; } std::cout \n; for (int r 0; r BOARD_SIZE; r) { std::cout (r 10 ? : ) r ; for (int c 0; c BOARD_SIZE; c) { char ch .; if (b[r][c] 1) ch X; else if (b[r][c] 2) ch O; std::cout ch ; } std::cout \n; } }逻辑说明PrintBoard 只负责读取不修改棋盘所以这里传的是 const int 指针。控制台版用 X 和 O 代表黑白子. 代表空位。第一次联机调这类代码时建议坚持用控制台输出棋盘等通信完全正常再考虑图形界面否则你会分不清是网络问题还是绘制问题。参数说明列宽统一保持三字符保证棋盘在命令行里是方正的不会因为超过 9 导致错位。3.2 落子函数坐标合法性、空格校验、轮到谁落子看似简单但联机版有一个容易漏掉的细节收到对方消息后落子时不能只在本地棋盘上直接写值还要校验这个坐标当前是否是空的。因为网络消息可能重复例如对方客户端超时重发同一条落子消息时本地棋盘对应位置可能已经被写过了。bool PlaceStone(int b[BOARD_SIZE][BOARD_SIZE], int row, int col, int player) { // 坐标越界直接拒绝 if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) { return false; } // 该位置已有棋子拒绝覆盖 if (b[row][col] ! 0) { return false; } // 玩家编号必须合法1 黑2 白 if (player ! 1 player ! 2) { return false; } b[row][col] player; return true; }逻辑说明这个函数的返回值很重要网络层的落子处理必须检查返回值。如果 PlaceStone 返回 false说明消息里的坐标非法不能继续向对方发送确认否则双方棋盘从这一步开始永久错位。参数说明player 用 1 和 2 而不是 0 和 1是为了和棋盘上 0 代表空位的约定保持一致避免把“空位”和“黑棋”搞混。轮次控制不在这里做由调用方维护一个 int currentTurn 变量落子成功后切换 currentTurn 3 - currentTurn。3.3 四方向五连判定方向表、双端扫描、边界检查五子棋胜负判定是所有逻辑里的核心也是最容易写出 bug 的地方。常见错误是只检测水平方向或者只往右数五个格子忘了斜着连成五个也算赢。我推荐以刚落下的最后一子为起点向四个方向双向扫描水平、垂直、主对角线、副对角线。每个方向从落子点往两端数连续同色棋子的数量加起来大于等于 5 就判胜。bool CheckWin(const int b[BOARD_SIZE][BOARD_SIZE], int row, int col, int player) { // 四个方向的方向向量水平、垂直、主对角线、副对角线 const int dirs[4][2] { {0, 1}, // 水平左右 {1, 0}, // 垂直上下 {1, 1}, // 主对角线右下、左上 {1, -1} // 副对角线左下、右上 }; for (int d 0; d 4; d) { int dr dirs[d][0]; int dc dirs[d][1]; int count 1; // 当前落子本身算一个 // 向正方向延伸 for (int step 1;; step) { int nr row dr * step; int nc col dc * step; if (nr 0 || nr BOARD_SIZE || nc 0 || nc BOARD_SIZE) break; if (b[nr][nc] ! player) break; count; } // 向反方向延伸 for (int step 1;; step) { int nr row - dr * step; int nc col - dc * step; if (nr 0 || nr BOARD_SIZE || nc 0 || nc BOARD_SIZE) break; if (b[nr][nc] ! player) break; count; } if (count 5) return true; } return false; }逻辑说明count 的初始值是 1因为刚落下的这手棋已经占了位置从它本身开始计数。两个 for 循环分别沿正方向和反方向延伸一旦越界或遇到不同色棋子就停止。四个方向只要有一个方向计数值大于等于 5就判定当前玩家获胜。参数说明dirs 数组是四个方向基向量dc 为 1 时表示列号增大为 -1 时表示列号减小。这个方向表用 int 二维数组表示比枚举更直观后续如果想要支持六子棋等变体只需要改 BOARD_SIZE 和把 count 5 改成 count 6其余不动。3.4 用离线棋谱验证判定函数先不过网络直接跑单测我在实盘联网之前一定会先用一段临时代码测 CheckWin。测法很简单手动往棋盘里摆几组已经连成五子的坐标然后调用 CheckWin看返回值是否符合预期。这样做能在半小时内排除掉 80% 的规则逻辑问题。void TestCheckWin() { // 水平五连测试第 7 行从第 3 列到第 7 列 for (int c 3; c 7; c) board[7][c] 1; bool horizontal CheckWin(board, 7, 5, 1); std::cout 水平五连判定: horizontal \n; // 清空棋盘准备垂直五连测试 memset(board, 0, sizeof(board)); for (int r 2; r 6; r) board[r][9] 2; bool vertical CheckWin(board, 4, 9, 2); std::cout 垂直五连判定: vertical \n; // 清空棋盘准备反方向五连测试 memset(board, 0, sizeof(board)); for (int i 0; i 5; i) board[8 - i][2 i] 1; bool diag CheckWin(board, 6, 4, 1); std::cout 副对角线五连判定: diag \n; }测试要点每一组测试之间必须重新 memset 清空棋盘否则上一组的数据会干扰下一组。这里故意把最后一副棋谱做成从左下到右上走向的副对角线用来覆盖 dirs 数组里 dc 为 -1 的分支。参数说明CheckWin 的第三个参数 row、col 一定要传最后一次落子的坐标而不是五连的中间位置。虽然本例里传中间格也能通过但在真实对局中五连的判定点是对方刚落下的那一子传错位置会导致明明赢了却不判胜。4. 从握手到落子复盘服务端/客户端的消息协议与联机流程网络层和规则层都就绪后接下来就是把它们黏起来的消息协议。协议设计得简单清晰联机排障时你只要看日志里的一行字符串就能定位问题。4.1 消息协议一行文本一条命令用换行做分隔我给这类 c 小游戏代码选协议时很少用二进制结构体直接怼 socket而是用文本协议。每条消息以 \n 结束字段之间用冒号或逗号分隔。调试工具里直接肉眼可读收到畸形消息能被立刻发现。下表是源码中需要支持的最小消息集合消息类型方向格式示例落子双向MOVE:行,列\nMOVE:7,7\n胜利单向WIN:行,列\nWIN:7,7\n认输单向GIVEUP\nGIVEUP\n重开双向RESET\nRESET\n连接确认服务端到客户端HELLO\nHELLO\n协议里不需要双方名称、不需要房间号因为这些属于后续扩展项。最小协议只表达五件事我落到哪、我赢了、我不下了、再来一局、连接成功。用 \n 做消息边界有一个附带好处即使一次 recv 收到了两条消息只要按换行拆开逐条处理即可这对应了 TCP 粘包问题的标准解法后面避坑章节还会展开。4.2 服务端最小实现绑定端口、监听、接受连接服务端代码负责创建监听 socket绑定地址然后阻塞等待一个客户端接入。这里用阻塞模式写最小版本不建议新手一上来就搞 select 和 epoll先把单连接对局跑通更重要。#include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) void RunServer() { // 初始化 Winsock WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); // 创建 TCP socket SOCKET srv socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (srv INVALID_SOCKET) { std::cerr socket 创建失败\n; return; } // 绑定 8888 端口监听所有网卡 sockaddr_in addr {}; addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(8888); if (bind(srv, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { std::cerr bind 失败端口可能被占用\n; closesocket(srv); return; } listen(srv, 1); // 只允许一个客户端排队 std::cout 等待客户端加入端口 8888...\n; SOCKET cli accept(srv, nullptr, nullptr); if (cli INVALID_SOCKET) { std::cerr accept 失败\n; closesocket(srv); return; } std::cout 客户端已连接\n; // 进入对局处理循环closesocket 在对局结束后统一处理 }逻辑说明WSAStartup 是 Winsock 的初始化入口Windows 下 socket 相关比赛前必须先调用。INADDR_ANY 表示监听本机所有网卡地址这样局域网内任何一台机器都能通过这台机器的 IP 连入。参数说明端口 8888 是示例值实际使用时可以换成 1024 到 65535 之间任意端口。listen 的第二个参数 1 表示待连接队列长度因为五子棋是单人对局排队长度为 1 就够。4.3 客户端最小实现填好对方 IP发起连接客户端代码比服务端短一些核心就是创建一个 socket然后用对方 IP 和端口号去 connect。注意这里需要用户手动输入服务端的局域网 IPVSCode 里用 C 写网络程序的同学最容易在这一步漏掉 ws2_32 的链接配置。void RunClient(const char* serverIp) { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); SOCKET cli socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in peer {}; peer.sin_family AF_INET; peer.sin_port htons(8888); // 把 192.168.1.100 形式的字符串转成网络字节序地址 peer.sin_addr.s_addr inet_addr(serverIp); if (connect(cli, (sockaddr*)peer, sizeof(peer)) SOCKET_ERROR) { std::cerr 连接失败: WSAGetLastError() \n; closesocket(cli); return; } std::cout 连接成功白棋等待对方落子...\n; // 进入对局处理循环 }逻辑说明inet_addr 把点分十进制的 IP 字符串转换成网络字节序这是最容易出错的一行。微软在较新 SDK 中更推荐用 inet_pton 替代但 inet_addr 在旧教程和现有源码里出现频率更高。参数说明serverIp 在联机调试时先传 127.0.0.1 本机回环验证通过后再传对方实际局域网地址。connect 返回 SOCKET_ERROR 时WSAGetLastError 能给出错误码比如 10061 表示目标机器主动拒绝10060 表示超时这两种情况定位方向完全不同。4.4 先手确定与轮转控制消息驱动不靠时间猜谁执黑谁先走这个决策不能在两端各猜各的。我习惯这样约定服务端 accept 成功之后执黑客户端连接成功之后执白。服务端先手首手直接落子然后把 MOVE 消息发给客户端客户端只有收到第一条 MOVE 之后才允许落子。轮转控制的核心是一个 bool myTurn 变量本地落子后置 false收到对方 MOVE 后置 true。这个变量只在主循环线程里读写没有多线程竞争问题。对局中每收到一条 MOVE先调用 PlaceStone再检查 CheckWin。如果赢了发送 WIN 消息并进入 GAME_OVER。对方收到 WIN 后也在本地棋盘执行一次相同的落子和判定作为兜底尽量避免一方判胜一方没判胜的状态错位。5. 联机五子棋高频踩坑记录防火墙、粘包、端口占用与掉线重连网络联机小游戏源码跑不起来九成以上不是算法问题而是网络环境的边界条件没处理好。我把调试这类程序时遇到的高频问题整理成下面几条每条按现象、原因、解决三步写清楚。5.1 recv 一次收到两条消息或半条消息现象双方明明各下一手棋盘上却一次性出现了两个棋子或者有一手棋完全没显示。再仔细看控制台日志服务端打印的报文是 MOVE:7,7MOVE:8,8挤在同一行。原因TCP 是流式协议它不管上层消息怎么切分发送端两次 send 的数据可能被接收端一次 recv 收走也可能一次 send 被拆成两次 recv 接收。这就是典型的粘包和半包。初学 socket 的人最容易误以为每次 recv 返回的数据就是一条完整消息。解决给每条消息加明确的终止符 \n在 recv 侧维护一个累积缓冲区。每次 recv 后把新数据追加进去然后循环按 \n 查找完整行取出处理处理不了的残留在缓冲区等下一轮。代码如下char recvBuf[4096]; std::string leftover; int n recv(cli, recvBuf, sizeof(recvBuf) - 1, 0); if (n 0) { recvBuf[n] \0; leftover recvBuf; size_t pos; while ((pos leftover.find(\n)) ! std::string::npos) { std::string line leftover.substr(0, pos); leftover.erase(0, pos 1); HandleLine(line); // 解析一条完整的 MOVE 或 WIN 消息 } }操作提示所有 recv 分支都必须走这个累积流程不能因为想偷懒就直接把 recvBuf 当整条消息。缓冲区大小 4096 对五子棋完全够用但累计字符串 leftover 会在一整局过程中不断增长收缩不用担心225 手消息总量也就几千字节。5.2 服务端 bind 直接报错端口被上一轮进程占用现象服务器程序一运行就打印 bind 失败然后退出。检查代码发现端口号是 8888换一个端口又能跑。原因上一次运行的服务端进程没有正常退出socket 还处于 TIME_WAIT 状态导致端口暂时被占住。Windows 下频繁启停调试程序时这个现象几乎必现。多次 CtrlC 终止程序也会留下未释放的句柄。解决bind 之前设置允许地址重用int yes 1; setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, (const char*)yes, sizeof(yes));如果设置之后仍然失败用命令netstat -ano | findstr 8888查看占用端口进程的 PID再到任务管理器里确认是不是上一轮调试进程还活着。这个坑不掉价就算有多年经验的 C 工程师也会在改完代码重启服务端时撞上同一堵墙。5.3 本机用 127.0.0.1 能连局域网对面电脑连不上现象客户端写成 127.0.0.1 时一切正常换成对方机器的局域网 IP 后卡在 connect 界面长达十秒无响应最后超时。原因Windows 防火墙默认拦截入站的 TCP 连接。服务端监听的是 INADDR_ANY确实能收到包但防火墙在 accept 之前直接丢弃了 SYN 报文。另一个常见原因写上服务端机器和客户端机器不在同一网段或者路由器开启了 AP 隔离。解决先在 Windows 防火墙的高级设置里添加入站规则允许 TCP 端口 8888 通过适用范围选“所有配置文件”。每次改完端口号都要同步改规则否则会出现“代码明明没问题下一台设备就是连不上”的玄学现场。快速验证网络是否通的方法在客户端机器上打开 cmd输入telnet 对方IP 8888如果连接被拒绝或超时基本可以确定是网络层问题而不是 C 代码问题。5.4 对面程序一关闭自己这边直接卡死现象对局中一方直接关掉窗口另一方程序瞬间无响应连棋盘都不刷新了过几秒被系统提示“未响应”。原因recv 是阻塞调用对方断开后 recv 返回 0这部分本身没问题。常见的是进程收到该信号后继续跑完整段逻辑以至于在内部死循环或者陷入错误的状态没有对 recv 返回值做分支处理。更麻烦的是网络线程如果抛异常锁未释放主循环取消息的互斥锁就被永久占用整个程序僵死。解决两处代码都要写严谨。首先recv 返回值要分类判断int n recv(cli, recvBuf, sizeof(recvBuf) - 1, 0); if (n 0) { // 对方正常关闭发送方断开进行清理 printf(对方已断开连接\n); closesocket(cli); PostMessage({MSG_DISCONNECT, 0, 0, 0}); return; } if (n SOCKET_ERROR) { // 检查错误码普通断线按同上流程处理 int err WSAGetLastError(); printf(recv 错误err%d\n, err); return; }其次在主循环的 PollMessage 分支里收到 MSG_DISCONNECT 后要立即退出循环并清理资源不要把处理逻辑继续往落子分支里带。网络线程尤其是要包一层 catch(...)任何异常都要被捕获否则线程栈里的锁永远不会释放。5.5 双击 exe 报缺少运行库或者 Visual Studio 链接报 LNK2019现象直接把编译好的 exe 拷到另一台电脑运行弹出“找不到 MSVCP140.dll”或“无法定位程序输入点于 msvcp140.dll”。在 VSCode 里编译时socket 相关函数报 LNK2019 未解析的外部符号。原因前者是目标机器缺少 Microsoft Visual C Redistributable 运行库程序是用动态方式链接 C 运行库的后者是编译环境没有把 ws2_32.lib 传给链接器。这是两件经常被混淆的事。解决编译期检查 VSCode 的 c_cpp_properties.json 里是否配置了ws2_32.lib。直接用 MSVC 的命令行可以这样cl main.cpp /link ws2_32.lib运行期打包时用静态链接 /MT 编译选项把运行库编进 exe或者让目标机器安装对应架构的 Visual C Redistributable。注意区分 x86 和 x64装错架构同样会报入口点错误。课堂上程序能跑不等于成品可用这一点在课程设计答辩现场翻车的案例不在少数。6. 让黑匣子变白盒本机回环验证与SDL界面的升级路径联机五子棋源码升级到带图形界面之前先建立一套回环验证流程。这套流程能让网络问题在 5 分钟内暴露出来不用每次开着两台电脑猜原因。验证梯度分四步第一步服务端监听 127.0.0.1客户端也连接 127.0.0.1用同机两个进程验证协议正确性。第二步服务端监听 INADDR_ANY客户端连接本机局域网 IP验证网卡绑定有没有问题。第三步在第二台电脑上运行客户端连服务端的局域网 IP验证防火墙和路由。第四步对局中随机关闭客户端验证断线清理分支是否生效。每次改动协议或网络参数后我习惯先回到第一步重跑全部流程确认没把基础功能改坏再往后面梯度走。图形界面升级我推荐走 SDL2 而不是 Win32 APISDL2 是跨平台的事件循环清晰贴着 C 的使用习惯。实现思路是把主循环改成 SDL 事件驱动的形式SDL_PollEvent 处理键盘鼠标同时非阻塞地调用 PollMessage 处理网络队列。两者都空了就 SDL_Delay 休眠 10 毫秒避免 CPU 空转。如果要把网络线程的消息直接接到 SDL 的窗口线程里不要在回调函数里修改棋盘仍然投递到消息队列由主循环统一处理。界面升级的正确顺序是先保持控制台逻辑完整不动用 SDL 替换 PrintBoard 输出部分棋盘数组和 CheckWin 一个字符都不用改。跑通一局之后再加悔棋功能因为悔棋需要回滚双方棋盘协议层要新增 ROLLBACK 消息并约定只能在对方落子前使用。这个功能我建议留到最后它牵涉到状态同步的边界问题比画一个窗口难得多。我的习惯是每次改完网络相关代码固定跑一遍“双端启动、黑白各落三手、主动断开”的脚本用例确认没有卡死、没有越界、没有残留线程再继续写下一段功能。C 写网络程序很多问题都是偶发的不靠固定用例去压你会被后续的玄学问题拖垮。希望这些习惯对你有帮助。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?