Teternity

欢迎大家指出代码的不足,共同学习!

扫雷(联网对战版)

一、简介

单机版扫雷总会有些枯燥,不妨试试联网对战版扫雷!

开发环境:VS2019 + EasyX_20210224。

游戏玩法:左键按下翻开方块,翻开雷判输,或翻开最后一个方块判赢;逃跑或超时会结束本轮连接,正常结束会自动重新开局,对手不变。

目的:一方面提供联网对战版扫雷供大家娱乐,另外也是重构曾经写过的扫雷单机版,最后也为了练习 Windows 网络编程。

二、网络

关于 Windows 网络编程书籍,推荐朱晨冰老师的《Visual C++ 2017 网络编程实战》,出版日期 2020 年。

个人感觉这本书讲解比较细致,内容也不会太老旧,有计网基础会更容易理解学习。

程序采用阻塞套接字编写,因此服务器免不了使用多线程,多线程部分由 C++11 提供。

想要让程序不限于局域网通信又不想购买服务器,这里推荐花生壳软件,操作简单,TCP 部分免费使用,相关内容自行搜索。

三、程序文件

1、服务器文件:

包含两个文件:WinsockTcp.h 和 main.cpp。

WinsockTcp.h 主要封装了 TCP 相关操作,可创建 TCP 服务器和客户端;main.cpp 为服务器代码。

2、客户端代码:

包含文件:WinsockTcp.h,Button.h,Datas.h,HomeScene.h,RunningScene.h,main.cpp。

WinsockTcp.h 同服务器的 WinsockTcp.h;

Button.h 封装简单按钮;

Datas.h 包含一些数据和绘图图像;

HomeScene.h 为开始场景,比较简单;

RunningScene.h 核心代码,涉及客户端网络连接和程序控制;

main.cpp 程序入口,主要控制程序走向。

3、如何测试运行该游戏:

a)客户端 IP 地址为笔者服务器地址,可直接运行客户端进行联网匹配。

b)局域网或本地测试:

运行服务器 -> 修改客户端源码 IP 地址为本地 IP -> 运行客户端,可运行多个实例。

客户端连接服务器成功后 30s 左右未匹配时将匹配失败,可重新尝试匹配。

c)更新:修改服务器对于同一个 IP 地址的客户端,最多接受两个实例进行连接。

四、其他

界面截图:

源码包:点此下载源码包

客户端(可直接运行客户端进行匹配):点此下载客户端程序

五、服务器更新

2021-01-29 针对服务器的更新。

在 Windows 下,套接字有两种 I/O 模式:阻塞模式和非阻塞模式,前面服务器在阻塞模式下借助多线程实现。而对于非阻塞模式,微软提出五种 I/O 模型:选择模型(select 模型)、异步选择模型(WSAAsyncSelect 模型)、事件选择模型(WSAEventSelect 模型)、重叠 I/O 模型(Overlapped I/O 模型)、完成端口模型。不同的模型,程序架构是不同的,相对而言,难度依次递增。

完成端口会充分利用 Windows 内核来进行 I/O 的调度,是用于 C/S 通信模式中性能最好的网络通信模型,没有之一,甚至连和它性能接近的通信模型模型都没有。因此完成端口被广泛的应用于各个高性能服务器程序上。这里使用完成端口模型更新扫雷服务器,功能不变,客户端也不需要改变。

代码如下:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <iostream>
using std::cin;
using std::cout;
using std::endl;

#include <WinSock2.h>
#pragma comment (lib, "ws2_32")

#include <thread>
#include <future>
#include <mutex>
#include <queue>
#include <unordered_map>
#include <easyx.h>

// 参数设定
const int nRow = 30;				// 横向格子数
const int nCol = 20;				// 纵向格子数
const int munMine = 108;			// 雷数目

// 定义格子值
// '0' 表示空白,'1' - '8' 表示方块周围雷数目,值 1 表示雷
const char MINE = 1;

// 定义传送信息结构体
struct Info
{
	int code;			// 状态码,1 代表正常进行,2 代表正常结束,3 代表断开
	unsigned int uMsg;	// 鼠标消息 uMsg
	int x;				// 鼠标位置 x
	int y;				// 鼠标位置 y;
};

// IP Map
std::unordered_map<ULONG, int> ipMap;
std::mutex ipMapMutex;

// 存放套接字的队列
std::queue<std::pair<SOCKET, std::pair<ULONG, clock_t>>> sQue;
// 套接字队列互斥锁
std::mutex sqMut;

// 重叠结构
OVERLAPPED overlap;

// 接收缓冲区
constexpr int MSGSIZE = sizeof(Info);

// 每个套接字需要的数据结构
struct PER_IO_DATA
{
	SOCKET s;					// 客户端套接字
	SOCKET s2;					// 对家套接字
	ULONG ip1, ip2;				// 两个客户端地址
	char msgbuf[MSGSIZE];		// 消息缓冲区
	WSABUF buffer;				// WSABUF
};

// 生成地图并向两方发送
bool SendDatas(SOCKET s1, SOCKET s2, ULONG ip1, ULONG ip2);

// 从队列获取并匹配玩家线程
void ConsumerThread(HANDLE);

// 工作线程
void WorkerThread(HANDLE);

// 打印当前时间
void PrintCurrentTime()
{
	tm localtm;

	const time_t t = time(nullptr);
	localtime_s(&localtm, &t);

	cout << localtm.tm_year + 1900 << "-" << localtm.tm_mon + 1 << "-" << localtm.tm_mday;
	cout << "  " << localtm.tm_hour << ":" << localtm.tm_min << ":" << localtm.tm_sec << endl;
}

// 主函数
int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return -1;

	// 创建完成端口
	HANDLE hcp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	if (hcp == NULL)
	{
		cout << "CreateIoCompletionPort failed with error: " << GetLastError() << endl;
		WSACleanup();
		return -1;
	}

	// 创建工作线程
	int nCPU = std::thread::hardware_concurrency() * 2 - 1;
	if (nCPU < 1) nCPU = 1;
	for (int i = 0; i < nCPU; ++i) std::thread(WorkerThread, hcp).detach();

	// 创建从队列获取并匹配玩家线程
	std::thread(ConsumerThread, hcp).detach();

	// 服务器本地地址
	sockaddr_in addrServer{};
	addrServer.sin_family = AF_INET;
	addrServer.sin_port = htons(9999);
	addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

	// 创建套接字
	SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sockListen == INVALID_SOCKET) return -1;

	// 绑定
	if (bind(sockListen, (sockaddr*)&addrServer, sizeof(addrServer)) != 0) return -1;

	// 监听
	if (listen(sockListen, 5) != 0) return -1;

	cout << "----------服务器已经启动----------" << endl;

	// 客户端地址
	sockaddr_in addr;
	int addrLen = sizeof(addr);

	// 循环接受连接请求
	while (true)
	{
		SOCKET s = accept(sockListen, (sockaddr*)&addr, &addrLen);

		ULONG ipaddr = ntohl(addr.sin_addr.S_un.S_addr);
		ipMapMutex.lock();
		++ipMap[ipaddr];
		if (ipMap[ipaddr] > 2)
		{
			--ipMap[ipaddr];
			ipMapMutex.unlock();
			closesocket(s);
			cout << "x. ";
		}
		else
		{
			ipMapMutex.unlock();
			std::lock_guard<std::mutex> lg(sqMut);
			sQue.push({ s,{ ipaddr,clock()} });
		}

		cout << "客户端接入: " << inet_ntoa(addr.sin_addr) << " - " << ntohs(addr.sin_port) << "   ";
		PrintCurrentTime();
	}

	closesocket(sockListen);
	WSACleanup();
	return 0;
}

// 生成地图并向两方发送
bool SendDatas(SOCKET s1, SOCKET s2, ULONG ip1, ULONG ip2)
{
	// 生成地图
	char* Map = new char[nRow * nCol];
	memset(Map, 0, nRow * nCol);
	// 生成雷区
	for (int i = 0; i < munMine; ++i) Map[i] = MINE;
	// 打乱雷区
	std::random_shuffle(Map, Map + nRow * nCol);
	// 遍历确认每个格子值
	for (int j = 0; j < nCol; ++j)
	{
		for (int i = 0; i < nRow; ++i)
		{
			if (Map[j * nRow + i] != MINE)
			{
				char nMine = 0;
				for (int y = -1; y <= 1; ++y)
				{
					for (int x = -1; x <= 1; ++x)
					{
						if (x == 0 && y == 0) continue;
						if ((i + x) >= 0 && (i + x) < nRow && (j + y) >= 0 && (j + y) < nCol && Map[(j + y) * nRow + (i + x)] == MINE) ++nMine;
					}
				}
				Map[j * nRow + i] = '0' + nMine;
			}
		}
	}

	// 关闭套接字并清理 ipMap
	auto closeSockets = [&]
	{
		ipMapMutex.lock();
		--ipMap[ip1];
		--ipMap[ip2];
		ipMapMutex.unlock();
		closesocket(s1);
		closesocket(s2);
	};

	// 向两方发送地图数据
	int ret1 = send(s1, Map, nRow * nCol, 0);
	int ret2 = send(s2, Map, nRow * nCol, 0);
	if (ret1 == SOCKET_ERROR || ret2 == SOCKET_ERROR)
	{
		closeSockets();
		return false;
	}

	// 发送先手信息,'1' 先手
	char ch1 = '1', ch2 = '0';
	ret1 = send(s1, &ch1, 1, 0);
	ret2 = send(s2, &ch2, 1, 0);
	if (ret1 == SOCKET_ERROR || ret2 == SOCKET_ERROR)
	{
		closeSockets();
		return false;
	}

	return true;
}

// 从队列获取并匹配玩家线程
void ConsumerThread(HANDLE hcp)
{
	while (true)
	{
		sqMut.lock();
		// 队列中长时间只有一个成员时关闭该连接
		if (sQue.size() == 1 && clock() - sQue.front().second.second > 30 * 1000)
		{
			sqMut.unlock();
			closesocket(sQue.front().first);

			ipMapMutex.lock();
			--ipMap[sQue.front().second.first];
			ipMapMutex.unlock();

			std::lock_guard<std::mutex> lg(sqMut);
			sQue.pop();
		}
		// 队列成员大于一时匹配
		else if (sQue.size() > 1)
		{
			std::pair<SOCKET, std::pair<ULONG, clock_t>> s1 = sQue.front();
			sQue.pop();
			std::pair<SOCKET, std::pair<ULONG, clock_t>> s2 = sQue.front();
			sQue.pop();
			sqMut.unlock();

			std::async(
				[&] {
					// 向两方发送数据
					if (!SendDatas(s1.first, s2.first, s1.second.first, s2.second.first)) return;

					// 对 s1 创建 PER_IO_DATA 并关联到完成端口对象
					PER_IO_DATA* pData = new PER_IO_DATA{};
					pData->s = s1.first;
					pData->s2 = s2.first;
					pData->ip1 = s1.second.first;
					pData->ip2 = s2.second.first;
					pData->buffer.len = MSGSIZE;
					pData->buffer.buf = pData->msgbuf;
					CreateIoCompletionPort((HANDLE)pData->s, hcp, (ULONG_PTR)pData, 0);

					// 对 s2 创建 PER_IO_DATA 并关联到完成端口对象
					PER_IO_DATA* pData2 = new PER_IO_DATA{};
					pData2->s = s2.first;
					pData2->s2 = s1.first;
					pData2->ip1 = s2.second.first;
					pData2->ip2 = s1.second.first;
					pData2->buffer.len = MSGSIZE;
					pData2->buffer.buf = pData2->msgbuf;
					CreateIoCompletionPort((HANDLE)pData2->s, hcp, (ULONG_PTR)pData2, 0);

					auto closeSockets = [](PER_IO_DATA* pData)
					{
						ipMapMutex.lock();
						--ipMap[pData->ip1];
						--ipMap[pData->ip2];
						ipMapMutex.unlock();
						closesocket(pData->s);
						closesocket(pData->s2);
					};

					// 针对 s1 投递 WSARecv 请求
					DWORD nRecvd = 0, flags = 0;
					memset(&overlap, 0, sizeof(overlap));
					int ret = WSARecv(pData->s, &pData->buffer, 1, &nRecvd, &flags, &overlap, nullptr);
					if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
					{
						closeSockets(pData);
						delete pData;
						return;
					}

					// 针对 s2 投递 WSARecv 请求
					nRecvd = 0, flags = 0;
					memset(&overlap, 0, sizeof(overlap));
					ret = WSARecv(pData2->s, &pData2->buffer, 1, &nRecvd, &flags, &overlap, nullptr);
					if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
					{
						closeSockets(pData2);
						delete pData2;
						return;
					}
				});
		}
		else sqMut.unlock();
	}
}

// 工作线程
void WorkerThread(HANDLE hcp)
{
	DWORD nTrans;
	PER_IO_DATA* pData;
	OVERLAPPED* poverlap;

	auto closeSockets = [&]
	{
		closesocket(pData->s);
		int ret = closesocket(pData->s2);
		if (ret == SOCKET_ERROR) return;
		ipMapMutex.lock();
		--ipMap[pData->ip1];
		--ipMap[pData->ip2];
		ipMapMutex.unlock();
	};

	while (true)
	{
		bool bRet = GetQueuedCompletionStatus(hcp, &nTrans, (PULONG_PTR)&pData, &poverlap, WSA_INFINITE);
		Info* info = (Info*)pData->msgbuf;

		if (!bRet || nTrans == 0 || ntohl(info->code) == 3)
		{
			info->code = htonl(3);
			send(pData->s2, pData->msgbuf, pData->buffer.len, 0);
			closeSockets();
			delete pData;
		}
		else
		{
			if (ntohl(info->code) == 1)
			{
				if (send(pData->s2, pData->msgbuf, pData->buffer.len, 0) == SOCKET_ERROR)
				{
					closeSockets();
					delete pData;
				}
				else
				{
					DWORD nRecvd = 0, flags = 0;
					memset(&overlap, 0, sizeof(overlap));
					int ret = WSARecv(pData->s, &pData->buffer, 1, &nRecvd, &flags, &overlap, nullptr);
					if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
					{
						closeSockets();
						delete pData;
					}
				}
			}
			else
			{
				std::async(
					[&] {
						if (SendDatas(pData->s, pData->s2, pData->ip1, pData->ip2))
						{
							DWORD nRecvd = 0, flags = 0;
							memset(&overlap, 0, sizeof(overlap));
							int ret = WSARecv(pData->s, &pData->buffer, 1, &nRecvd, &flags, &overlap, nullptr);
							if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
							{
								closeSockets();
								delete pData;
							}
						}
					});
			}
		}
	}
}
分享到

评论 (3) -

  • 如果是内网环境的话可以直接用客户端创建房间然后扫内网IP找房间加入吧

添加评论