前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Windows下C++/C简单的多线程网络编程SOCKET聊天服务端实现

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

原创
作者头像
晨星成焰
发布2024-07-08 22:21:05
1900
发布2024-07-08 22:21:05
举报
文章被收录于专栏:网络编程

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

本文将使用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腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代码步骤
    • 1.头文件
      • 2. 用户UserInfo类
        • 3. 全局变量定义
          • 4. 相关函数定义
            • 5. 主函数main
            • 全部代码
            • 使用总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档