news 2026/9/28 17:20:35

VC++联机五子棋实战:C/S通信架构与Socket编程详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC++联机五子棋实战:C/S通信架构与Socket编程详解

简介:这份资源是面向C++与VC++初学者、课程设计及毕业设计学生的五子棋游戏完整项目,重点解决图形界面开发、鼠标交互与简单AI算法落地的问题。项目基于VC++实现,包含双人对战与人机对战两种模式:双人模式下黑白双方鼠标交替落子,人机模式中人类执黑先行、AI自动应对,均能判定率先连成五子的一方并返回菜单。压缩包共26个文件,约1.7MB,以cpp源文件、h头文件、o目标文件为主,另含docx设计报告、cbp工程文件、layout布局文件、exe可执行程序及README说明,覆盖从源码到可运行程序的完整链路。已有336人学习下载。读者可借此理解棋型判断、落子搜索与界面刷新等核心逻辑,参考设计报告梳理项目结构,并直接运行exe验证效果,适合作为C++综合练习与二次开发的基础模板。

1. 从单机到联机:VC++ 五子棋的通信骨架怎么搭

很多人做五子棋练手,卡在单机 AI 就停了。棋盘绘制、胜负判断、简单搜索树,这些在 VC++ 里用 GDI 加一个二维数组就能跑起来,但一旦要联机,问题立刻从“算法”变成“通信”。标题里的“在线联机”四个字,才是这个项目真正的分水岭。它要解决的不是五子棋规则本身,而是两个 VC++ 客户端如何通过一个服务端交换落子坐标、同步棋盘状态、处理先后手和断线重连。适合谁?适合已经能用 VC++ 写窗口程序、想补上网络编程这一环的开发者。你不需要先成为五子棋高手,但需要理解 socket、阻塞与非阻塞、消息边界这三个基础概念。这一篇就按“先立通信模型,再落代码,最后排坑”的顺序,把 VC++ 联机五子棋从设计到跑通讲清楚。

2. 联机五子棋的通信模型:C/S 还是 P2P,VC++ 下怎么选

2.1 为什么 C/S 是 VC++ 联机五子棋的默认答案

在 VC++ 环境里做联机五子棋,常见做法是 C/S 架构:一个服务端进程负责维护房间、转发落子消息、判定胜负,两个客户端只负责界面和收发。P2P 看起来省服务器,但 NAT 穿透、双方状态一致性、掉线判定都会把复杂度拉高,对练手项目不划算。C/S 的代价是你需要多写一个服务端,但换来的是逻辑集中、调试路径清晰。

服务端不需要图形界面,用 Win32 控制台或 MFC 对话框程序都行。核心是WSAStartup初始化、socket创建、bind端口、listen等待连接。客户端用connect连上后,双方约定一套极简协议:每条消息固定长度或带长度前缀,避免 TCP 粘包。五子棋的消息类型很少,落子、悔棋请求、认输、心跳,四五个枚举就够。

选 C/S 的另一个理由是 VC++ 的调试工具链。你可以在服务端打日志,看到每个连接的 IP、端口、收到的字节流,客户端只关心“我收到一条落子消息,更新棋盘”。这种职责分离让新手能分阶段验证:先让两个控制台程序互发字符串,再把字符串换成结构体,最后接上 MFC 棋盘。

2.2 消息协议设计:用结构体还是 JSON

VC++ 里传结构体很自然,但跨进程、跨机器时要注意字节对齐和大小端。更稳妥的做法是定义固定长度的二进制包,或者用长度前缀加 JSON 字符串。对于五子棋,我一般会定义一个包头:

#pragma pack(push, 1) struct GomokuHeader { int32_t msgType; // 1=落子 2=悔棋 3=认输 4=心跳 5=开始 int32_t payloadLen; // 后续负载长度 }; #pragma pack(pop)

#pragma pack(push,1)取消结构体填充,保证两端sizeof一致。msgType用int32_t而不是int,避免不同编译器下int长度差异。payloadLen告诉接收方后面还有多少字节,解决 TCP 粘包。

落子负载可以再定义一个结构:

#pragma pack(push, 1) struct MovePayload { int32_t row; int32_t col; int32_t player; // 1=黑 2=白 }; #pragma pack(pop)

发送时先发GomokuHeader,再发MovePayload。接收方先recv固定 8 字节包头,解析出payloadLen,再循环recv直到收满负载。这个“先头后体”的模式是 VC++ 网络编程里最稳的写法,比直接recv一个结构体可靠得多。

提示:如果服务端和客户端都在本机测试,可以用127.0.0.1和高端口号如 9527,避免和系统服务冲突。

2.3 服务端最小实现:两个客户端如何被撮合

服务端启动后进入accept循环。第一个客户端连上时,把它放入等待队列;第二个客户端连上时,配对成功,服务端给双方各发一条“开始”消息,并指定黑方。之后任何一方发来落子,服务端先校验坐标是否合法、是否轮到该玩家,再转发给另一方。

// 伪代码:服务端主循环 SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(9527); addr.sin_addr.s_addr = INADDR_ANY; bind(listenSock, (sockaddr*)&addr, sizeof(addr)); listen(listenSock, 2); SOCKET clients[2] = {0, 0}; int count = 0; while (count < 2) { clients[count] = accept(listenSock, nullptr, nullptr); count++; } // 通知双方开始,clients[0] 执黑 sendStart(clients[0], 1); sendStart(clients[1], 2);

这段代码只处理两个客户端,适合练手。如果要支持多房间,需要把clients数组换成std::map<int, Room>,每个房间两个 socket。但第一次跑通,两个客户端足够验证通信链路。

参数说明:htons(9527)把主机字节序端口转网络字节序;INADDR_ANY表示监听所有网卡;listen的第二个参数是等待队列长度,设 2 即可。accept返回的新 socket 才是和客户端通信用的,listenSock继续等待下一个连接。

2.4 客户端接入:MFC 对话框里怎么塞进 socket

客户端用 MFC 对话框程序,主线程负责界面,socket 收消息如果也放主线程会阻塞界面。常见做法是开一个工作线程专门recv,收到完整消息后PostMessage给主窗口,主窗口再更新棋盘。这样界面不卡,逻辑也清晰。

// 客户端接收线程 UINT RecvThread(LPVOID pParam) { SOCKET sock = (SOCKET)pParam; while (true) { GomokuHeader header; int ret = recv(sock, (char*)&header, sizeof(header), 0); if (ret <= 0) break; std::vector<char> payload(header.payloadLen); int received = 0; while (received < header.payloadLen) { ret = recv(sock, payload.data() + received, header.payloadLen - received, 0); if (ret <= 0) break; received += ret; } // 把消息投递给主窗口 ::PostMessage(g_hMainWnd, WM_GOMOKU_MSG, header.msgType, (LPARAM)new std::vector<char>(payload)); } return 0; }

PostMessage的wParam传消息类型,lParam传堆上的 payload 指针,主窗口处理完记得delete。这里用new是为了跨线程传递数据,避免栈变量失效。如果嫌麻烦,也可以定义一个固定大小的环形缓冲区,但堆分配在练手项目里更直观。

3. 棋盘同步与胜负判定:VC++ 里怎么保证两边看到的一样

3.1 棋盘数据结构:二维数组还是位棋盘

五子棋棋盘 15×15,用int board[15][15]足够。0 表示空,1 黑,2 白。服务端和客户端各维护一份,服务端是权威,客户端收到落子消息后更新本地数组并重绘。位棋盘用两个uint32_t表示一行,省内存但代码可读性差,练手项目不推荐。

重绘用 GDI 的Rectangle和Ellipse,或者直接贴棋子图片。MFC 的OnPaint里根据board数组画网格和棋子。每次收到落子消息,更新数组后调用Invalidate()触发重绘。

3.2 胜负判定的四个方向与边界处理

判定逻辑放在服务端,客户端也可以做本地预判但以服务端为准。对最后一颗落子,检查横、竖、左斜、右斜四个方向,每个方向往两边数连续同色棋子,达到 5 就赢。

bool CheckWin(int board[15][15], int row, int col, int player) { int dirs[4][2] = {{0,1},{1,0},{1,1},{1,-1}}; for (auto& d : dirs) { int count = 1; for (int step = 1; step < 5; step++) { int r = row + d[0]*step, c = col + d[1]*step; if (r<0||r>=15||c<0||c>=15||board[r][c]!=player) break; count++; } for (int step = 1; step < 5; step++) { int r = row - d[0]*step, c = col - d[1]*step; if (r<0||r>=15||c<0||c>=15||board[r][c]!=player) break; count++; } if (count >= 5) return true; } return false; }

边界检查r<0||r>=15||c<0||c>=15必须放在访问board[r][c]之前,否则数组越界。dirs里{1,-1}是右斜向上,配合反向搜索覆盖整个斜线。这个函数在服务端每次落子后调用,返回 true 就广播胜利消息。

3.3 落子合法性校验:别让客户端说了算

客户端可以点任何位置,但服务端必须校验:坐标在 0~14 之间、该位置为空、当前轮到该玩家。三个条件缺一不可。常见翻车是只校验了坐标范围,没校验轮次,导致一方连下两手。服务端维护currentPlayer,每次合法落子后切换。

bool IsValidMove(int board[15][15], int row, int col, int player, int current) { if (row < 0 || row >= 15 || col < 0 || col >= 15) return false; if (board[row][col] != 0) return false; if (player != current) return false; return true; }

校验通过后,服务端更新自己的board,切换currentPlayer,再把落子消息转发给两个客户端。客户端收到后也更新本地board,但不再做轮次判断,直接信任服务端。

3.4 断线处理:心跳与重连的最小方案

TCP 连接断开时recv返回 0 或负数。服务端检测到一方断开,给另一方发“对手已离线”,然后关闭房间。客户端可以定时发心跳包,服务端收到后回复,超过一定时间没收到就判定掉线。练手项目里心跳间隔设 5 秒,超时 15 秒。

// 客户端心跳线程片段 while (connected) { Sleep(5000); GomokuHeader hb{4, 0}; send(sock, (char*)&hb, sizeof(hb), 0); }

服务端收到msgType=4就更新该连接的lastActive时间。主循环里检查所有连接,超过 15 秒没活动的关闭 socket。这个机制不复杂,但能避免“一方关窗口另一方干等”的尴尬。

4. 避坑与排查:VC++ 联机五子棋最容易翻车的五个点

4.1 现象:客户端一连接就断,服务端 accept 返回无效 socket

原因通常是WSAStartup没调用或版本不对。VC++ 里用 Winsock 必须先WSAStartup(MAKEWORD(2,2), &wsaData),否则所有 socket 函数返回INVALID_SOCKET。解决:在程序入口处调用,并检查返回值。

4.2 现象:落子消息偶尔丢失或粘在一起

TCP 是字节流,没有消息边界。如果发送方连续send两次,接收方可能一次recv收到两条消息的拼接。解决:严格按“先收 8 字节包头,再收 payloadLen 字节负载”的循环收,不要假设一次recv就是一条完整消息。

4.3 现象:界面卡死,点按钮没反应

把recv放在主线程会导致阻塞。解决:接收放工作线程,用PostMessage通知主线程更新 UI。注意PostMessage的参数不能传栈上变量的地址,要用new或全局缓冲区。

4.4 现象:服务端和客户端结构体大小不一致

不同编译器或不同#pragma pack设置下,struct可能有不同填充。解决:所有网络传输的结构体都用#pragma pack(push,1)包裹,并且用固定宽度类型如int32_t。

4.5 现象:胜负判定在边界处误判

斜线方向搜索时,如果没有先检查坐标范围就访问数组,会读到越界内存,导致随机胜负。解决:把边界检查放在board[r][c]之前,用短路求值r<0||r>=15||...||board[r][c]!=player确保安全。

5. 进阶技巧:用 VC++ 访问 HTTP 服务端 API 做战绩上传

联机五子棋跑通后,一个自然的扩展是把每局结果上传到 HTTP 服务端,做战绩统计。VC++ 访问 HTTP 接口常见做法是用 WinINet 或 WinHTTP。WinHTTP 更现代,适合访问 RESTful API。

#include <winhttp.h> #pragma comment(lib, "winhttp.lib") bool UploadResult(const std::wstring& url, const std::string& jsonBody) { HINTERNET hSession = WinHttpOpen(L"GomokuClient/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) return false; URL_COMPONENTS uc{}; uc.dwStructSize = sizeof(uc); wchar_t host[256]{}, path[1024]{}; uc.lpszHostName = host; uc.dwHostNameLength = 255; uc.lpszUrlPath = path; uc.dwUrlPathLength = 1023; WinHttpCrackUrl(url.c_str(), 0, 0, &uc); HINTERNET hConnect = WinHttpConnect(hSession, host, uc.nPort, 0); HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", path, nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); WinHttpSendRequest(hRequest, L"Content-Type: application/json\r\n", -1, (LPVOID)jsonBody.c_str(), (DWORD)jsonBody.size(), (DWORD)jsonBody.size(), 0); WinHttpReceiveResponse(hRequest, nullptr); WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return true; }

WinHttpOpen初始化会话,WinHttpCrackUrl拆出主机和路径,WinHttpConnect建连接,WinHttpOpenRequest建请求,WinHttpSendRequest发 POST。Content-Type: application/json告诉服务端 body 是 JSON。jsonBody可以拼一个简单字符串如{"winner":1,"moves":42}。注意WinHttpSendRequest的第四个参数是额外请求头,第五个是 body 指针,第六个是 body 长度。

参数说明:WINHTTP_ACCESS_TYPE_DEFAULT_PROXY使用系统代理设置;WINHTTP_FLAG_SECURE用于 HTTPS;WinHttpReceiveResponse后可以再WinHttpQueryHeaders读状态码,但练手项目里发出去就算成功。

这个技巧的价值在于把单机联机五子棋和现代 Web 后端串起来。你可以用任何语言写一个接收 JSON 的 HTTP 接口,存到数据库,前端再展示排行榜。VC++ 客户端不需要引入庞大框架,WinHTTP 是系统自带,编译时链接winhttp.lib即可。

我自己的习惯是:每次跑通一个联机功能,先别急着加特效,而是把日志打全。服务端记录每个连接的 IP、端口、收发的消息类型和长度;客户端记录每次recv的返回值和解析结果。这样出问题时,翻日志比猜快得多。联机五子棋的难点从来不是五子棋,而是“我以为发出去了”和“我以为收到了”之间的那段黑匣子。把这段黑匣子照亮,后面加悔棋、加聊天、加观战都是顺水推舟。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 17:19:32

IR2103自举电路设计与故障排查实战指南

1. 为什么IR2103的自举电路总在H桥里“掉链子”——从格力变频板维修现场说起去年冬天帮朋友修一台格力老款变频空调&#xff0c;故障现象很典型&#xff1a;压缩机启动瞬间有“咔哒”声&#xff0c;但转不起来&#xff1b;用示波器一测上桥臂MOS管栅极波形&#xff0c;发现PWM…

作者头像 李华
网站建设 2026/9/28 17:19:31

手把手构建命令行工具箱:CLI-Anything 设计与实践

1. 项目全景&#xff1a;每个“手工操作”都值得一个命令1.1 从一次“复制粘贴”开始先说我做这个项目的动机。常年待在终端里干活的人大概都有这种经历&#xff1a;明明只是个“把一批文件改名”、“把目录里的图片压缩一遍”、“生成一个新模块的模板代码”之类的小事&#x…

作者头像 李华
网站建设 2026/9/28 17:19:10

伦理量子信息学:九元原子如何把伦理约束变成量子态结构

第一次看到“伦理量子信息学&#xff1a;九元原子的量子信息实现”这个题目时&#xff0c;我的第一反应和大家一样&#xff1a;这是不是把化学元素周期表又改写了一遍&#xff1f;“九元原子”听起来像是发现了一种新的元素。实际接触之后才发现&#xff0c;这完全是另一回事—…

作者头像 李华
网站建设 2026/9/28 17:18:53

多智能体编排利器harness-sdk:模型聚合、路由与故障转移实践

最近好几个做AI应用的朋友都在问我同一个问题&#xff1a;harness-sdk到底是干什么的&#xff1f;为什么GitHub上那个叫DeepSeek Harness的项目这么多人讨论&#xff1f;还有人直接把热搜词里的“harness和agent区别”甩给我。正好我最近用这个SDK做了几个多智能体编排的实验&a…

作者头像 李华
网站建设 2026/9/28 17:17:42

AD9122高速DAC实战:SPI配置与LVDS接口设计调试

第一次在项目上用到AD9122时&#xff0c;我的第一反应是“这芯片太挑接口了”。16位双通道、最高1200MSPS的更新率&#xff0c;如果老老实实按并行CMOS方式喂数据&#xff0c;接口速率得上Gbps级别&#xff0c;一般的FPGA和PCB布线根本招架不住。所以LVDS数据接口与SPI配置寄存…

作者头像 李华
网站建设 2026/9/28 17:17:34

CLI-Anything:面向开发者的Agent-Native命令行智能体框架

1. CLI-Anything 是什么&#xff1a;一个被严重低估的命令行智能体基础设施CLI-Anything 不是一个玩具脚本&#xff0c;也不是某个大厂临时起意的 Demo 工具。它是一套面向开发者日常真实工作流设计的、可嵌入、可扩展、可自定义的命令行智能体&#xff08;CLI Agent&#xff0…

作者头像 李华