Windows下C++/C简单的多线程网络编程SOCKET聊天服务端实现

2024-07-08 22:21:05 浏览数 (2)

网络编程是现代软件开发中不可或缺的一部分,尤其在构建实时通信应用时更是如此。

本文将使用C 和Winsock库构建一个基本的多线程聊天服务器


代码步骤

1.头文件

代码语言:cpp复制
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <ws2tcpip.h> // 包含inet_ntop定义
#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <thread>
#include <vector>
#include <sstream>
#include <mutex>
#pragma comment(lib, "ws2_32.lib")

这里包含了Winsock所需的头文件,以及标准I/O流、线程、向量和互斥锁的头文件。

#pragma comment(lib, "ws2_32.lib")指令告诉编译器链接Winsock库。

2. 用户UserInfo类

代码语言:cpp复制
class UserInfo
{
public:
    bool isLogin = false; // 是否登录
    std::string userName; // 用户名
    SOCKET clientSock;    // 客户端套接字
    UserInfo(bool isLogin, std::string userName, SOCKET clientSock) :isLogin(isLogin), userName(userName), clientSock(clientSock) {}
};

UserInfo类用于存储每个客户端的信息,包括登录状态、用户名和客户端套接字。

3. 全局变量定义

代码语言:cpp复制
std::vector<UserInfo*> g_clients; // 用于存储客户端用户信息
std::mutex clientsMutex;          // 保护g_clients的互斥锁

g_clients是一个存储UserInfo指针的向量,用于全局管理所有客户端的信息。clientsMutex互斥锁用于保证线程安全。

4. 相关函数定义

代码语言:cpp复制
//  判定用户是否处在登陆状态函数
//  INPUIT: const std::string&  userName  用户姓名
//  RETURN: bool  true:在线 false:不在线
bool isUserLoggedIn(const std::string& userName)
{
	for (const auto& user : g_clients)
	{
		if (user->userName == userName && user->isLogin)
			return true;
	}
	return false;
}

//  广播信息函数
//  INPUIT: SOCKET selfSock 客户端的Sock描述符, const char* msg  广播信息
void SendMsg(SOCKET selfSock, const char* msg)
{
	int msglen = strlen(msg);
	for (int i = 0; i < g_clients.size(); i  )
	{
		if (g_clients[i]->clientSock == selfSock)continue;
		send(g_clients[i]->clientSock, msg, msglen, 0);
	}
}

//  分割字符串函数
//  INPUIT: string_view s 待分割字符串, char delimiter 分割符号
//  RETURN: std::vector<std::string>  存储分割的字符串的数组
std::vector<std::string> splitString( std::string_view s, char delimiter)
{
	std::vector<std::string> result;
	std::string path;
	for (size_t i = 0; i < s.size(); i  ) // 更改int为size_t以匹配size()的返回类型
	{
		if (s[i] != delimiter)
		{
			path.push_back(s[i]);
		}
		else if (!path.empty()) // 确保path非空时才push_back
		{
			result.push_back(path);
			path.clear();
		}
	}
	if (!path.empty()) // 处理字符串以分隔符结尾的情况
	{
		result.push_back(path);
	}
	return result;
}

//  客户端后的线程处理函数
//  INPUIT: SOCKET clientSocket 客户端的Sock描述符, const char* clientIp 客户端IP, int clientPort 客户端端口
void HandleClientConnection(SOCKET clientSocket, const char* clientIp, int clientPort)
{
	std::cout << "开始处理客户端: " << clientIp << ":" << clientPort << std::endl;
	//UserInfo* newUser = new UserInfo(false, "undefined", clientSocket);
	// 用于存储当前处理的用户信息指针,初始为nullptr
	UserInfo* currentUser = nullptr;
	while (true)
	{
		char szData[1024] = {};
		int ret = recv(clientSocket, szData, sizeof(szData), 0);
		if (ret > 0)
		{
			std::cout << "收到数据: [" << szData << "]" << std::endl;
			std::vector<std::string> splits = splitString(szData, '|');

			if (splits[0] == "Login")
			{
				// 验证用户是否已登录
				if (isUserLoggedIn(splits[1]))
				{
					char loginFailedMsg[64];
					snprintf(loginFailedMsg, sizeof(loginFailedMsg), "Error|%s|LoginFailed", splits[1].c_str());
					send(clientSocket, loginFailedMsg, sizeof(loginFailedMsg), 0);
					continue;
				}

				// 用户未登录,创建并登录
				currentUser = new UserInfo(true, splits[1], clientSocket);
				{
					std::lock_guard<std::mutex> lock(clientsMutex);
					g_clients.emplace_back(currentUser);
				}

				char UserLoginOK[64];
				snprintf(UserLoginOK, sizeof(UserLoginOK), "Login|%s|OK", splits[1].c_str());
				SendMsg(0, UserLoginOK);
				std::cout << "用户[" << splits[1] << "]登录" << std::endl;

				if (g_clients.size() == 2)
				{
					SendMsg(0, "GameStart");
				}
			}
			else if (splits[0] == "其它命令")
			{
				//其它命令相关
			}
			else
			{
				//不是任何命令则认为是聊天信息,可以加入聊天标志,方便客户端放到聊天窗口显示
				std::string chatMsg;
				if (currentUser)
				{
					chatMsg = currentUser->userName   ":"   szData; // 当用户已登录时,使用用户名
				}
				else
				{
					chatMsg = "undefined:"   std::string(szData); // 用户未登录或无法识别时,使用"undefined"
				}
				SendMsg(clientSocket, chatMsg.c_str());
			}
		}
		else if (ret == 0)
		{
			std::cout << "客户端: " << clientIp << ":" << clientPort << " 断开连接" << std::endl;
			break;
		}
		else
		{
			std::cerr << "接收客户端数据失败" << std::endl;
			break;
		}
	}
	// 如果currentUser已分配(即用户曾登录),需要释放资源
	if (currentUser != nullptr)
	{
		std::lock_guard<std::mutex> lock(clientsMutex);
		for (auto it = g_clients.begin(); it != g_clients.end();   it)
		{
			// 找到并移除对应的UserInfo对象
			if (*it == currentUser)
			{
				delete currentUser;
				g_clients.erase(it);
				break;
			}
		}
	}
	closesocket(clientSocket);
	std::cout << "结束处理客户端: " << clientIp << ":" << clientPort << std::endl;
}

isUserLoggedIn: 检查用户是否已经登录。

SendMsg: 广播消息给所有客户端,除了指定的客户端。

splitString: 将字符串按照特定分隔符拆分为字符串向量。

HandleClientConnection: 主要处理客户端连接的函数,包含登录逻辑和消息处理。

5. 主函数main

代码语言:cpp复制
int main()
{
	// 0. 初始化网络环境
	WSADATA wsaData = {};
	WSAStartup(MAKEWORD(2, 2), &wsaData);
	SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
	if (sockServer == INVALID_SOCKET)
	{
		std::cerr << "创建服务端句柄失败" << std::endl;
		WSACleanup();
		return -1;
	}
	printf("1. 创建服务端成功n");
	SOCKADDR_IN addr = { 0 };
	addr.sin_family = AF_INET;
	addr.sin_port = htons(9870);
	addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	if (bind(sockServer, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
	{
		std::cerr << "绑定端口号失败" << std::endl;
		closesocket(sockServer);
		WSACleanup();
		return -1;
	}
	if (listen(sockServer, SOMAXCONN) == SOCKET_ERROR)
	{
		std::cerr << "监听端口号失败" << std::endl;
		closesocket(sockServer);
		WSACleanup();
		return -1;
	}
	std::cout << "服务器正在监听..." << std::endl;

	while (true)
	{
		SOCKET sockClient = accept(sockServer, NULL, NULL);
		if (sockClient != INVALID_SOCKET)
		{
			SOCKADDR_IN clientAddr;
			int nAddrLen = sizeof(SOCKADDR_IN);
			getpeername(sockClient, (SOCKADDR*)&clientAddr, &nAddrLen);

			char cliIp[INET_ADDRSTRLEN];
			inet_ntop(AF_INET, &clientAddr.sin_addr, cliIp, sizeof(cliIp));
			unsigned short cliPort = ntohs(clientAddr.sin_port);
			std::cout << "新客户端连接: " << cliIp << ":" << cliPort << std::endl;


			std::thread t([=]() {
				HandleClientConnection(sockClient, cliIp, cliPort);
				});
			t.detach();

		}
		else
		{
			std::cerr << "接收客户端连接失败" << std::endl;
		}
	}

	closesocket(sockServer);
	WSACleanup();

	return 0;
}

初始化Winsock环境。

创建服务器套接字并绑定到本地地址和端口。

开始监听连接。

在无限循环中接受客户端连接,并为每个连接创建一个新线程执行HandleClientConnection。

全部代码

代码语言:cpp复制
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <ws2tcpip.h> // 包含inet_ntop定义
#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <thread>
#include <vector>
#include <sstream>
#include <mutex>

#pragma comment(lib, "ws2_32.lib")

class UserInfo
{
public:
	bool isLogin = false; //是否登陆
	std::string userName; //客户端的用户名
	SOCKET clientSock;   //客户端的socket

	UserInfo(bool isLogin, std::string userName, SOCKET clientSock) :isLogin(isLogin), userName(userName), clientSock(clientSock)
	{

	}
};

std::vector<UserInfo*> g_clients; //用于服务端存储客户端信息
std::mutex clientsMutex; // 用于保护g_clients的互斥锁



//  判定用户是否处在登陆状态函数
//  INPUIT: const std::string&  userName  用户姓名
//  RETURN: bool  true:在线 false:不在线
bool isUserLoggedIn(const std::string& userName)
{
	for (const auto& user : g_clients)
	{
		if (user->userName == userName && user->isLogin)
			return true;
	}
	return false;
}

//  广播信息函数
//  INPUIT: SOCKET selfSock 客户端的Sock描述符, const char* msg  广播信息
void SendMsg(SOCKET selfSock, const char* msg)
{
	int msglen = strlen(msg);
	for (int i = 0; i < g_clients.size(); i  )
	{
		if (g_clients[i]->clientSock == selfSock)continue;
		send(g_clients[i]->clientSock, msg, msglen, 0);
	}
}

//  分割字符串函数
//  INPUIT: string_view s 待分割字符串, char delimiter 分割符号
//  RETURN: std::vector<std::string>  存储分割的字符串的数组
std::vector<std::string> splitString( std::string_view s, char delimiter)
{
	std::vector<std::string> result;
	std::string path;
	for (size_t i = 0; i < s.size(); i  ) // 更改int为size_t以匹配size()的返回类型
	{
		if (s[i] != delimiter)
		{
			path.push_back(s[i]);
		}
		else if (!path.empty()) // 确保path非空时才push_back
		{
			result.push_back(path);
			path.clear();
		}
	}
	if (!path.empty()) // 处理字符串以分隔符结尾的情况
	{
		result.push_back(path);
	}
	return result;
}

//  客户端后的线程处理函数
//  INPUIT: SOCKET clientSocket 客户端的Sock描述符, const char* clientIp 客户端IP, int clientPort 客户端端口
void HandleClientConnection(SOCKET clientSocket, const char* clientIp, int clientPort)
{
	std::cout << "开始处理客户端: " << clientIp << ":" << clientPort << std::endl;
	//UserInfo* newUser = new UserInfo(false, "undefined", clientSocket);
	// 用于存储当前处理的用户信息指针,初始为nullptr
	UserInfo* currentUser = nullptr;
	while (true)
	{
		char szData[1024] = {};
		int ret = recv(clientSocket, szData, sizeof(szData), 0);
		if (ret > 0)
		{
			std::cout << "收到数据: [" << szData << "]" << std::endl;
			std::vector<std::string> splits = splitString(szData, '|');

			if (splits[0] == "Login")
			{
				// 验证用户是否已登录
				if (isUserLoggedIn(splits[1]))
				{
					char loginFailedMsg[64];
					snprintf(loginFailedMsg, sizeof(loginFailedMsg), "Error|%s|LoginFailed", splits[1].c_str());
					send(clientSocket, loginFailedMsg, sizeof(loginFailedMsg), 0);
					continue;
				}

				// 用户未登录,创建并登录
				currentUser = new UserInfo(true, splits[1], clientSocket);
				{
					std::lock_guard<std::mutex> lock(clientsMutex);
					g_clients.emplace_back(currentUser);
				}

				char UserLoginOK[64];
				snprintf(UserLoginOK, sizeof(UserLoginOK), "Login|%s|OK", splits[1].c_str());
				SendMsg(0, UserLoginOK);
				std::cout << "用户[" << splits[1] << "]登录" << std::endl;

				if (g_clients.size() == 2)
				{
					SendMsg(0, "GameStart");
				}
			}
			else if (splits[0] == "其它命令")
			{
				//其它命令相关
			}
			else
			{
				//不是任何命令则认为是聊天信息,可以加入聊天标志,方便客户端放到聊天窗口显示
				std::string chatMsg;
				if (currentUser)
				{
					chatMsg = currentUser->userName   ":"   szData; // 当用户已登录时,使用用户名
				}
				else
				{
					chatMsg = "undefined:"   std::string(szData); // 用户未登录或无法识别时,使用"undefined"
				}
				SendMsg(clientSocket, chatMsg.c_str());
			}
		}
		else if (ret == 0)
		{
			std::cout << "客户端: " << clientIp << ":" << clientPort << " 断开连接" << std::endl;
			break;
		}
		else
		{
			std::cerr << "接收客户端数据失败" << std::endl;
			break;
		}
	}
	// 如果currentUser已分配(即用户曾登录),需要释放资源
	if (currentUser != nullptr)
	{
		std::lock_guard<std::mutex> lock(clientsMutex);
		for (auto it = g_clients.begin(); it != g_clients.end();   it)
		{
			// 找到并移除对应的UserInfo对象
			if (*it == currentUser)
			{
				delete currentUser;
				g_clients.erase(it);
				break;
			}
		}
	}
	closesocket(clientSocket);
	std::cout << "结束处理客户端: " << clientIp << ":" << clientPort << std::endl;
}
int main()
{
	// 0. 初始化网络环境
	WSADATA wsaData = {};
	WSAStartup(MAKEWORD(2, 2), &wsaData);
	SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
	if (sockServer == INVALID_SOCKET)
	{
		std::cerr << "创建服务端句柄失败" << std::endl;
		WSACleanup();
		return -1;
	}
	printf("1. 创建服务端成功n");
	SOCKADDR_IN addr = { 0 };
	addr.sin_family = AF_INET;
	addr.sin_port = htons(9870);
	addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	if (bind(sockServer, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
	{
		std::cerr << "绑定端口号失败" << std::endl;
		closesocket(sockServer);
		WSACleanup();
		return -1;
	}
	if (listen(sockServer, SOMAXCONN) == SOCKET_ERROR)
	{
		std::cerr << "监听端口号失败" << std::endl;
		closesocket(sockServer);
		WSACleanup();
		return -1;
	}
	std::cout << "服务器正在监听..." << std::endl;

	while (true)
	{
		SOCKET sockClient = accept(sockServer, NULL, NULL);
		if (sockClient != INVALID_SOCKET)
		{
			SOCKADDR_IN clientAddr;
			int nAddrLen = sizeof(SOCKADDR_IN);
			getpeername(sockClient, (SOCKADDR*)&clientAddr, &nAddrLen);

			char cliIp[INET_ADDRSTRLEN];
			inet_ntop(AF_INET, &clientAddr.sin_addr, cliIp, sizeof(cliIp));
			unsigned short cliPort = ntohs(clientAddr.sin_port);
			std::cout << "新客户端连接: " << cliIp << ":" << cliPort << std::endl;


			std::thread t([=]() {
				HandleClientConnection(sockClient, cliIp, cliPort);
				});
			t.detach();

		}
		else
		{
			std::cerr << "接收客户端连接失败" << std::endl;
		}
	}

	closesocket(sockServer);
	WSACleanup();

	return 0;
}

使用总结

客户端通过发送Login|昵称来登陆

登陆之后视为进入聊天室并拥有昵称

其它功能可以通过相应的指令来实现

---

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞