☰
VC++联机五子棋实战:Winsock网络通信与MFC棋盘同步
2026/9/28 17:20:49 网站建设 项目流程

简介:这是一份面向C++与VC++初学者、课程设计及毕业设计学生的五子棋游戏开发资料,围绕在线联机对战与人机博弈两个核心场景展开,帮助读者理解Windows窗口编程、鼠标交互与简单AI搜索算法的落地方式。压缩包共26个文件,约1.7MB,以cpp源文件与h头文件为主体,辅以o编译中间文件、cbp工程配置、layout界面布局、docx设计报告及可直接运行的exe程序,源码与可执行文件并存,便于对照调试。功能上支持双人鼠标交替落子、人机对战中人类执黑先行、AI自动应对,并在连成五子后判定胜负、返回菜单。已有336人学习下载,读者可从中获取完整工程结构、棋型评估与搜索模块的拆分思路、设计报告参考以及编译运行排错经验,适合作为C++综合练习与游戏开发入门案例。

1. 从单机棋盘到联机对局:VC++ 五子棋项目到底在解决什么问题

很多人第一次看到“基于VC++的在线联机五子棋游戏设计与实现”这个标题,脑子里浮现的是 MFC 画个棋盘、鼠标点两下、判断五子连珠就完事。真动手才发现,单机五子棋和联机五子棋之间隔着一整套网络通信、状态同步和断线处理的工程问题。单机版你只需要一个二维数组、一个胜负判断函数、一个重绘逻辑;联机版你要面对的是两台机器上的棋盘状态如何保持一致、谁先手、落子消息怎么发、对方掉线了怎么办、网络延迟导致的操作顺序错乱怎么兜底。这些才是这个项目真正的技术含量所在。

这篇文章面向的是想用 VC++ 做一套能跑起来的联机五子棋的开发者——不管你是课程设计需要,还是想拿一个完整项目练手 Windows 网络编程和图形界面。我会把技术选型、通信协议设计、棋盘渲染、胜负判定、联机同步、调试排错这条链路拆开讲清楚,每一步给出可复现的做法和参数。你不需要事先精通 Winsock 或 MFC,但需要有一点 C++ 基础和 Visual Studio 的使用经验。读完你应该能自己搭出一套局域网内可对战的双人五子棋,并且知道哪些地方容易翻车、怎么提前规避。

2. 技术选型与工程骨架:VC++ 做联机五子棋用什么搭

2.1 界面框架选 MFC 还是 Win32 裸写

VC++ 做 Windows 桌面游戏界面,常见的选择有三个:MFC、Win32 API 裸写、以及 Qt。Qt 严格来说不算纯 VC++ 技术栈,而且部署时要带一堆 DLL,对于“VC++ 项目”这个定位来说不太对味。Win32 裸写窗口和消息循环代码量大,新手容易在 WndProc 里绕晕。MFC 是折中方案——它封装了窗口创建、消息映射、GDI 绘图,同时仍然是纯 VC++ 体系,Visual Studio 直接支持,编译出来不依赖额外运行时(静态链接的话)。

我一般会选 MFC 的对话框程序(Dialog-based)而不是单文档/多文档。原因很直接:五子棋界面就是一个固定大小的棋盘加几个按钮和状态栏,不需要文档/视图架构那套东西。对话框程序建起来快,控件拖拽方便,消息映射清晰。在 VS2017 或更高版本里新建项目时选“MFC 应用程序”,应用程序类型选“基于对话框”,其余默认即可。

有一点要注意:VS2017 之后的版本默认可能没装 MFC 组件。如果你新建项目时找不到 MFC 模板,打开 Visual Studio Installer,修改当前版本,在“单个组件”里勾选“适用于最新 v143 生成工具的 C++ MFC”。这个坑很常见,尤其是用社区版的同学。

2.2 网络通信选 TCP 还是 UDP

五子棋是回合制游戏,每一步落子都必须可靠到达对方,不能丢。丢一步棋,整个棋局就废了。所以传输层选 TCP,不选 UDP。TCP 保证有序、可靠、面向连接,正好匹配回合制对局的通信需求。UDP 的低延迟优势在五子棋场景里毫无意义——你落一子的延迟从 1ms 变成 50ms,玩家根本感知不到,但丢一个包就是灾难。

Winsock 2.2 是 Windows 下 TCP 编程的标准接口,VC++ 直接可用。通信模型上,两人对战用最简单的 C/S 架构:一方做服务端(创建监听 socket,等待连接),另一方做客户端(主动连接服务端)。服务端负责维护权威棋盘状态,客户端落子后发请求给服务端,服务端校验合法性后广播给双方。这种“服务端权威”模式比“对等协商”模式简单得多,也更容易处理冲突。

如果你想让任意一方都能先手,可以在连接建立后由服务端随机或协商决定执黑方。但棋盘状态的权威源始终在服务端,客户端只负责发送操作请求和渲染服务端同步过来的状态。

2.3 工程目录结构与依赖清单

一个干净的工程结构能让后续调试省很多事。我通常这样组织:

GomokuOnline/ ├── GomokuOnline.sln ├── GomokuOnline/ │ ├── GomokuOnline.h // 主头文件,包含全局定义 │ ├── GomokuOnline.cpp // 应用入口 │ ├── GomokuDlg.h / .cpp // 主对话框,承载棋盘和控件 │ ├── BoardLogic.h / .cpp // 棋盘逻辑:落子、胜负判断 │ ├── Network.h / .cpp // 网络模块:Winsock 封装 │ ├── Protocol.h // 通信协议定义 │ └── res/ // 资源文件

依赖方面,只需要系统自带的 ws2_32.lib。在项目属性 → 链接器 → 输入 → 附加依赖项里加上 ws2_32.lib,或者在代码里用#pragma comment(lib, "ws2_32.lib")自动链接。不需要任何第三方库。

提示:如果你用的是 VS2017 且编译时报“无法打开包括文件 winsock2.h”,检查 Windows SDK 是否安装完整。通常安装 VS 时勾选“Windows 10 SDK”即可。

3. 棋盘逻辑与胜负判定:从二维数组到五子连珠的完整实现

3.1 棋盘数据结构与落子合法性校验

五子棋棋盘标准是 15×15 交叉点。用二维数组int board[15][15]表示,0 表示空位,1 表示黑子,2 表示白子。这个数组放在服务端作为权威状态,客户端也维护一份用于渲染,但客户端的数组只读——所有修改都来自服务端同步。

落子合法性校验看起来简单,但有几个容易漏掉的点。第一,目标位置必须为空;第二,必须轮到当前玩家;第三,游戏未结束。前两条是基本要求,第三条很多人忘了——如果一方已经五连,另一方还能继续落子,逻辑就乱了。

// BoardLogic.h #pragma once #include <vector> const int BOARD_SIZE = 15; const int EMPTY = 0; const int BLACK = 1; const int WHITE = 2; class BoardLogic { public: BoardLogic(); // 尝试落子,返回是否成功 bool PlaceStone(int row, int col, int player); // 判断指定位置落子后是否形成五连 bool CheckWin(int row, int col, int player) const; // 获取棋盘状态 int GetCell(int row, int col) const; // 重置棋盘 void Reset(); // 当前是否已有胜者 bool HasWinner() const { return m_winner != EMPTY; } int GetWinner() const { return m_winner; } private: int m_board[BOARD_SIZE][BOARD_SIZE]; int m_winner; };
// BoardLogic.cpp #include "pch.h" #include "BoardLogic.h" BoardLogic::BoardLogic() { Reset(); } void BoardLogic::Reset() { memset(m_board, 0, sizeof(m_board)); m_winner = EMPTY; } bool BoardLogic::PlaceStone(int row, int col, int player) { // 边界检查 if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return false; // 位置必须为空 if (m_board[row][col] != EMPTY) return false; // 游戏已结束则不允许继续落子 if (m_winner != EMPTY) return false; m_board[row][col] = player; if (CheckWin(row, col, player)) { m_winner = player; } return true; } int BoardLogic::GetCell(int row, int col) const { if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) return -1; return m_board[row][col]; }

PlaceStone的参数含义:row和col是 0 起始的棋盘坐标,player是 BLACK 或 WHITE。返回值表示落子是否被接受。注意这里没有做“轮到谁”的判断——那个逻辑放在上层对局管理里,因为棋盘逻辑本身不应该关心回合顺序。

3.2 五连判定的四个方向扫描

胜负判定是五子棋的核心算法。判断逻辑是:在刚落子的位置,沿四个方向(水平、垂直、主对角线、副对角线)分别统计同色连续棋子数,任意方向达到或超过 5 个即获胜。

bool BoardLogic::CheckWin(int row, int col, int player) const { // 四个方向:水平、垂直、主对角线、副对角线 const int dx[] = { 0, 1, 1, 1 }; const int dy[] = { 1, 0, 1, -1 }; for (int dir = 0; dir < 4; ++dir) { int count = 1; // 包含当前落子 // 正方向延伸 for (int step = 1; step < 5; ++step) { int nr = row + dx[dir] * step; int nc = col + dy[dir] * step; if (nr < 0 || nr >= BOARD_SIZE || nc < 0 || nc >= BOARD_SIZE) break; if (m_board[nr][nc] != player) break; ++count; } // 反方向延伸 for (int step = 1; step < 5; ++step) { int nr = row - dx[dir] * step; int nc = col - dy[dir] * step; if (nr < 0 || nr >= BOARD_SIZE || nc < 0 || nc >= BOARD_SIZE) break; if (m_board[nr][nc] != player) break; ++count; } if (count >= 5) return true; } return false; }

方向数组dx和dy的组合含义:(0,1)是水平向右,(1,0)是垂直向下,(1,1)是主对角线(左上到右下),(1,-1)是副对角线(右上到左下)。每个方向从落子点向两侧各延伸最多 4 步,加上自身共最多 9 个位置,统计连续同色数量。只要某个方向达到 5 就返回胜利。

这里有个细节:循环上界写的是step < 5,意味着每个方向最多检查 4 步。加上当前子,一个方向最多覆盖 9 个位置。对于标准五子棋(恰好五连获胜,长连不算额外奖励),这个范围足够。如果你要做“长连禁手”之类的规则,需要额外判断 count 是否恰好等于 5。

3.3 棋盘渲染:GDI 绘制与双缓冲防闪烁

MFC 对话框上画棋盘,用 GDI 就够了。核心是在OnPaint里绘制网格线、星位、棋子。但直接画会闪烁——每次落子重绘整个棋盘,屏幕会抖。解决办法是双缓冲:先在内存 DC 上画好整幅图,再一次性 BitBlt 到屏幕 DC。

void CGomokuDlg::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(&rect); // 创建内存 DC 和兼容位图 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height()); memDC.SelectObject(&bmp); // 背景填充 memDC.FillSolidRect(&rect, RGB(220, 179, 92)); // 绘制棋盘网格 DrawBoard(memDC); // 绘制所有棋子 DrawStones(memDC); // 一次性输出到屏幕 dc.BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY); }

DrawBoard里计算网格间距:假设棋盘区域从(MARGIN, MARGIN)开始,每个格子边长CELL_SIZE,则第 i 条竖线 x 坐标是MARGIN + i * CELL_SIZE,横线类似。星位(天元、四隅)在(3,3)、(3,11)、(7,7)、(11,3)、(11,11)处画小圆点。

DrawStones遍历board数组,对每个非空位置画圆。黑子用FillSolidRect或Ellipse填充黑色,白子填充白色加灰色边框。棋子半径约为CELL_SIZE * 0.4。

双缓冲的关键是CreateCompatibleDC和CreateCompatibleBitmap必须在每次 OnPaint 时创建并在结束时释放,否则会内存泄漏。如果你觉得频繁创建开销大,可以在对话框初始化时创建一次,缓存起来,在OnSize时重建。

4. 联机通信协议与状态同步:Winsock 实战

4.1 自定义应用层协议的消息格式

TCP 是字节流,没有消息边界。你发两次数据,对方可能一次收到,也可能分三次收到。所以必须自定义应用层协议来划分消息边界。最简单的做法是“长度前缀 + 消息体”:每条消息前 4 个字节是消息体长度(网络字节序),后面跟消息体。

消息体内部再用一个字节表示消息类型,后面跟具体数据。我定义以下几种消息类型:

消息类型值方向数据内容
MSG_JOIN1客户端→服务端玩家昵称
MSG_JOIN_ACK2服务端→客户端分配的颜色(1或2)
MSG_MOVE3客户端→服务端row(1B) + col(1B)
MSG_MOVE_BROADCAST4服务端→双方row(1B) + col(1B) + player(1B)
MSG_GAME_OVER5服务端→双方winner(1B)
MSG_RESTART_REQ6客户端→服务端无
MSG_RESTART_ACK7服务端→双方无
MSG_CHAT8双向文本内容
// Protocol.h #pragma once #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") const int MSG_JOIN = 1; const int MSG_JOIN_ACK = 2; const int MSG_MOVE = 3; const int MSG_MOVE_BROADCAST = 4; const int MSG_GAME_OVER = 5; const int MSG_RESTART_REQ = 6; const int MSG_RESTART_ACK = 7; // 发送带长度前缀的消息 inline bool SendMessage(SOCKET sock, const char* data, int len) { // 先发 4 字节长度(网络字节序) int netLen = htonl(len); if (send(sock, (char*)&netLen, 4, 0) != 4) return false; // 再发消息体 int sent = 0; while (sent < len) { int ret = send(sock, data + sent, len - sent, 0); if (ret <= 0) return false; sent += ret; } return true; } // 接收一条完整消息,返回消息体长度,失败返回 -1 inline int RecvMessage(SOCKET sock, char* buffer, int bufSize) { int netLen = 0; int received = 0; // 先收 4 字节长度 while (received < 4) { int ret = recv(sock, ((char*)&netLen) + received, 4 - received, 0); if (ret <= 0) return -1; received += ret; } int msgLen = ntohl(netLen); if (msgLen <= 0 || msgLen > bufSize) return -1; // 再收消息体 received = 0; while (received < msgLen) { int ret = recv(sock, buffer + received, msgLen - received, 0); if (ret <= 0) return -1; received += ret; } return msgLen; }

SendMessage和RecvMessage是网络模块的基础设施。注意send和recv都不保证一次传输完所有字节,所以必须循环直到发完或收完。htonl和ntohl处理大小端问题——Windows 是小端,网络字节序是大端,不转换的话跨平台通信会出错。

4.2 服务端监听与客户端连接流程

服务端流程:初始化 Winsock → 创建 socket → bind 到端口 → listen → accept 等待客户端 → 连接建立后进入消息循环。客户端流程:初始化 Winsock → 创建 socket → connect 到服务端地址 → 进入消息循环。

// 服务端初始化 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡 serverAddr.sin_port = htons(9527); // 端口号 bind(listenSock, (sockaddr*)&serverAddr, sizeof(serverAddr)); listen(listenSock, 2); // 最多两个客户端排队 // 等待客户端连接 SOCKET clientSock = accept(listenSock, nullptr, nullptr);

端口号选 9527 只是个人习惯,你可以用 1024 以上的任意未占用端口。INADDR_ANY表示监听本机所有网络接口,局域网内其他机器可以通过你的 IP 连接。如果只想本机测试,用inet_addr("127.0.0.1")。

客户端连接:

SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 服务端 IP serverAddr.sin_port = htons(9527); if (connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { // 连接失败,提示用户检查 IP 和端口 AfxMessageBox(_T("连接服务端失败,请检查 IP 地址和端口")); return; }

4.3 落子同步与回合控制的服务端权威模式

服务端权威模式的核心原则:客户端不直接修改本地棋盘,所有落子必须发给服务端,服务端校验通过后再广播给双方,双方收到广播后才更新本地棋盘并重绘。

服务端维护一个BoardLogic实例和当前回合变量currentPlayer。收到MSG_MOVE后:

  1. 检查发送方是否是当前回合玩家
  2. 调用board.PlaceStone(row, col, player)校验并落子
  3. 如果成功,构造MSG_MOVE_BROADCAST发给双方
  4. 如果board.HasWinner(),再发MSG_GAME_OVER
  5. 切换currentPlayer
// 服务端处理落子请求 void HandleMove(SOCKET fromSock, int row, int col) { int player = GetPlayerBySocket(fromSock); if (player != currentPlayer) { // 不是你的回合,忽略 return; } if (!board.PlaceStone(row, col, player)) { // 非法落子,忽略 return; } // 广播落子消息 char buf[3] = { (char)row, (char)col, (char)player }; BroadcastMessage(MSG_MOVE_BROADCAST, buf, 3); if (board.HasWinner()) { char winBuf[1] = { (char)board.GetWinner() }; BroadcastMessage(MSG_GAME_OVER, winBuf, 1); } else { currentPlayer = (currentPlayer == BLACK) ? WHITE : BLACK; } }

客户端收到MSG_MOVE_BROADCAST后,更新本地棋盘数组并调用Invalidate()触发重绘。注意客户端本地也要调用PlaceStone来更新数组,但不需要再做胜负判断——胜负结果由服务端的MSG_GAME_OVER通知。

这种模式的好处是:即使客户端因为网络延迟导致操作顺序错乱,服务端也会拒绝非法请求,最终状态始终以服务端为准。坏处是每次落子都要等一个网络往返,局域网内延迟通常小于 5ms,体验上无感知。

5. 联机五子棋的避坑与排查:那些让你调一晚上的问题

5.1 连接成功但收不到消息:粘包与半包

现象:客户端 connect 成功,服务端 accept 也返回了,但双方就是收不到对方发的消息,或者收到乱码。

原因:TCP 粘包。发送方连续发了多条消息,接收方一次 recv 把多条消息的数据全收到了,但代码只按一条消息解析,剩下的数据丢了。或者接收方 recv 的缓冲区太小,一条消息被截断。

解决:严格按“长度前缀 + 消息体”的协议来收发。每次 recv 先确保收到完整的 4 字节长度,再根据长度收消息体。不要假设一次 recv 就是一条完整消息。上面的RecvMessage函数已经处理了这个问题,关键是循环 recv 直到收满指定字节数。

5.2 界面卡死:在主线程里阻塞 recv

现象:程序运行后界面点不动,像死机一样,但网络数据其实在收发。

原因:在 UI 线程里直接调用阻塞的recv,导致消息循环被卡住,Windows 消息无法处理,界面自然不响应。

解决:把网络接收放在独立线程里。用AfxBeginThread或std::thread创建一个接收线程,循环调用RecvMessage,收到消息后通过PostMessage把数据投递到 UI 线程处理。不要在工作线程里直接操作 UI 控件。

// 接收线程函数 UINT RecvThreadProc(LPVOID pParam) { CGomokuDlg* pDlg = (CGomokuDlg*)pParam; char buffer[1024]; while (pDlg->m_bConnected) { int len = RecvMessage(pDlg->m_sock, buffer, sizeof(buffer)); if (len <= 0) { pDlg->m_bConnected = FALSE; pDlg->PostMessage(WM_CONNECTION_LOST, 0, 0); break; } // 把收到的数据拷贝到堆上,通过 PostMessage 传给 UI 线程 char* pData = new char[len]; memcpy(pData, buffer, len); pDlg->PostMessage(WM_NETWORK_DATA, (WPARAM)pData, (LPARAM)len); } return 0; }

UI 线程在OnNetworkData消息处理函数里解析数据、更新棋盘、重绘。注意pData要在处理完后delete[],否则内存泄漏。

5.3 编译报错“无法解析的外部符号 __imp__WSAStartup”

现象:代码里调用了WSAStartup、socket、send、recv等函数,编译时提示无法解析的外部符号。

原因:没有链接 ws2_32.lib。

解决:在项目属性 → 链接器 → 输入 → 附加依赖项里添加ws2_32.lib,或者在任意一个 .cpp 文件里加#pragma comment(lib, "ws2_32.lib")。另外确保包含的是winsock2.h而不是旧的winsock.h,两者有冲突。如果同时包含了windows.h和winsock2.h,要把winsock2.h放在前面,或者定义WIN32_LEAN_AND_MEAN宏。

5.4 对方落子后本地棋盘不更新

现象:服务端广播了落子消息,客户端也收到了,但棋盘上没显示新棋子。

原因:客户端更新了棋盘数组但没触发重绘,或者重绘时用的还是旧数据。

解决:收到MSG_MOVE_BROADCAST后,先更新本地board数组,然后调用Invalidate()或InvalidateRect()触发OnPaint。如果用了双缓冲,确认OnPaint里读取的是更新后的数组。另一个常见问题是消息处理函数里更新了数组,但OnPaint里画的是另一个副本——检查是不是有两个棋盘数组实例。

5.5 断线后程序崩溃或卡死

现象:一方关闭程序或网络断开,另一方程序无响应或弹出一堆错误。

原因:recv返回 0 或负数时没有正确处理,继续在无效 socket 上操作。或者接收线程退出时没有通知 UI 线程。

解决:RecvMessage返回 -1 时,立即标记连接断开,关闭 socket,通过PostMessage通知 UI 线程显示“对方已断开”。UI 线程收到后禁用落子操作,显示重连或退出选项。不要在 socket 已关闭的情况下继续调用send或recv。

6. 从能跑到好用:联机五子棋的进阶技巧与验证方法

6.1 用本地回环测试联机逻辑

在找两台机器测试之前,先在本机用回环地址验证通信逻辑。服务端 bind 到127.0.0.1,客户端也连127.0.0.1,开两个程序实例(一个做服务端,一个做客户端)。这样可以排除局域网防火墙、IP 配置等干扰因素,专注调试协议和逻辑。

验证步骤:启动服务端 → 启动客户端 → 客户端应显示“已连接,等待分配颜色” → 服务端分配颜色后双方棋盘初始化 → 黑方落子 → 双方棋盘同步显示 → 白方落子 → 直到一方五连 → 双方显示胜负结果。每一步都用调试输出或日志确认消息收发正常。

6.2 加一个简单的日志系统方便排查

联机程序出问题时,光看界面很难定位是发送端没发、还是接收端没收、还是解析错了。在关键路径加日志输出,能省大量时间。

void Log(const CString& msg) { CString logFile = _T("gomoku_log.txt"); CStdioFile file; if (file.Open(logFile, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite)) { file.SeekToEnd(); CString line; line.Format(_T("[%s] %s\n"), CTime::GetCurrentTime().Format(_T("%H:%M:%S")), msg); file.WriteString(line); file.Close(); } }

在SendMessage和RecvMessage调用前后各加一条日志,记录消息类型和长度。出问题时打开日志文件,一眼就能看出消息是在哪一步断掉的。

6.3 用 Wireshark 抓包验证协议格式

如果日志显示消息发出去了但对方没收到,或者收到的数据不对,可以用 Wireshark 抓本地回环包(选 Loopback 接口)或局域网包。过滤条件设tcp.port == 9527,看 TCP 流里实际传输的字节。对照你的协议定义,检查长度前缀是否正确、消息类型字节是否匹配、数据内容是否符合预期。

这一步能发现很多“代码看起来对但实际发出去不对”的问题,比如htonl忘了加导致长度字段字节序反了,或者send的缓冲区指针偏移算错了。

6.4 重连与观战:两个值得做的扩展方向

基础版本跑通后,可以考虑两个扩展。一是断线重连:客户端断开后保留对局状态,重新连接时发送MSG_REJOIN带上之前的玩家标识,服务端恢复该玩家的棋盘视图。二是观战模式:服务端接受第三种连接类型,只接收广播不发送落子请求,用于旁观对局。

这两个扩展都需要在协议里增加消息类型,并在服务端维护更复杂的状态机。建议先把基础双人对战跑稳定,再逐步加功能。我自己的习惯是每加一个功能就用回环测试跑一遍完整对局,确认没有回归问题再继续。

做联机程序最深的教训是:不要相信“应该没问题”。网络编程里每一个假设都可能被现实打脸——你以为一次 send 能发完,你以为对方一定按顺序收到,你以为断线会立刻被发现。每一个“以为”都要用代码去验证、去兜底。把边界情况处理好了,程序才真正能用。希望帮到你。

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

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

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

立即咨询