网络编程是现代软件开发中不可或缺的一部分,尤其在构建实时通信应用时更是如此。
本文将使用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腾讯技术创作特训营最新征文,快来和我瓜分大奖!