Windows 下 C++ 多线程 TCP 服务器 Demo —— 支持并发处理多个客户端
大家好今天给大家分享一个在 Windows 环境下用 C 实现的多线程 TCP 服务器 Demo。在之前的单线程版本中服务端一次只能处理一个客户端处理完才能接受下一个连接这在实际应用中显然不够用。本次我们在原有基础上引入了多线程技术让服务器能够同时为多个客户端提供服务真正实现并发通信。代码同样从零上手即可跑通非常适合入门网络编程和多线程编程的小伙伴参考。一、为什么需要多线程TCP 服务器通常需要同时处理多个客户端的请求。如果使用单线程模型主线程在 accept 一个连接后必须完成该客户端的所有收发操作才能返回去接受下一个连接这会导致其他客户端长时间等待。通过多线程我们可以为每一个新接入的客户端创建一个独立的工作线程主线程只负责快速接受连接并分发任务从而极大提升服务器的并发处理能力。本 Demo 还引入了原子操作std::atomic来安全地统计当前在线客户端数量并设置了最大并发连接数5 个超过则拒绝新连接。二、开发环境说明操作系统Windows开发工具VSCode编程语言C使用 C11 标准构建工具CMake编译器MinGW 64 位 g 8.1.0 或更高版本需支持 C11 及thread库三、新增关键函数与概念说明本次多线程实现用到了一些 C 标准库的新特性简要介绍如下概念 / 函数说明std::threadC11 提供的线程类用于创建和管理线程。std::thread::detach()将线程与主线程分离使其在后台独立运行主线程不再等待它结束。std::thread::detach()将线程与主线程分离使其在后台独立运行主线程不再等待它结束。std::this_thread::get_id()获取当前线程的唯一 ID用于日志输出方便调试。std::atomic原子整型保证多线程环境下对变量的增减操作是线程安全的无需额外加锁。std::atomic::operator / operator–原子地自增或自减。SOMAXCONNWindows 中 listen 的 backlog 参数表示监听队列的最大长度一般取系统允许的最大值。此外Winsock 的 API 依然沿用之前的版本如socket、bind、listen、accept、send、recv、closesocket等用法不变。四、代码分步解析4.1多线程服务端server.cpp首先引入必要的头文件。除了 Winsock 和标准输入输出外我们新增了thread、atomic用于多线程支持#includethread#includeiostream#includecstring#includeatomic#includewinsock2.h接着定义最大并发客户端数量这里设为 5并声明一个原子计数器clientCount用于记录当前已连接的客户端数#defineMAX_CLIENTS5std::atomicintclientCount(0);// 当前客户端数量线程安全客户端处理函数handleClient这个函数将作为线程的入口函数每个客户端连接都会创建一个独立线程来执行它。参数是客户端套接字和客户端地址结构体。voidhandleClient(SOCKET clientSock,SOCKADDR_IN clientaddr){charbuf[1024];// 打印客户端信息并显示当前线程 IDstd::cout[Thread std::this_thread::get_id()] 客户端端口:ntohs(clientaddr.sin_port) 已连接std::endl;while(true){// 接收数据阻塞intrecvRetrecv(clientSock,buf,sizeof(buf),0);if(recvRet0){if(recvRet0)std::cout[Thread std::this_thread::get_id()] 客户端主动断开连接std::endl;elsestd::cout[Thread std::this_thread::get_id()] 接收失败std::endl;break;}std::cout[Thread std::this_thread::get_id()] 接收来自客户端的信息: bufstd::endl;// 回显数据给客户端if(send(clientSock,buf,strlen(buf),0)0){std::cout[Thread std::this_thread::get_id()] 发送失败std::endl;break;}}// 关闭客户端套接字并减少计数closesocket(clientSock);clientCount--;// 原子递减std::cout[Thread std::this_thread::get_id()] 客户端断开当前在线数: clientCountstd::endl;}重点说明每个线程都会打印自己的thread::id方便我们观察哪个线程在处理哪个客户端。使用std::atomic确保clientCount的增减是原子操作不会出现数据竞争。主函数mainWSADATA wsa;WSAStartup(MAKEWORD(2,0),wsa);SOCKET serversocsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(serversocINVALID_SOCKET){/* 错误处理 */}SOCKADDR_IN serveraddr;serveraddr.sin_familyAF_INET;serveraddr.sin_porthtons(9102);serveraddr.sin_addr.S_un.S_addrinet_addr(192.168.31.86);if(bind(serversoc,(SOCKADDR*)serveraddr,sizeof(SOCKADDR_IN))!0){/* 错误处理 */}if(listen(serversoc,SOMAXCONN)!0){/* 错误处理 */}std::cout服务器启动最大并发连接数: MAX_CLIENTSstd::endl;接下来进入主循环不断调用 accept 接受新连接while(true){SOCKADDR_IN clientaddr;intlensizeof(clientaddr);SOCKET clientsocaccept(serversoc,(SOCKADDR*)clientaddr,len);if(clientsocINVALID_SOCKET){std::coutaccept 失败退出主循环std::endl;break;}// 判断是否达到最大连接数if(clientCount.load()MAX_CLIENTS){clientCount;// 原子增加// 创建工作线程detach 使其独立运行std::thread(handleClient,clientsoc,clientaddr).detach();}else{// 拒绝连接并发送提示constchar*msg服务器当前繁忙请稍后再试。\n;send(clientsoc,msg,strlen(msg),0);closesocket(clientsoc);std::cout拒绝连接已达到最大并发数std::endl;}}重要细节clientCount.load()原子地读取当前值判断是否还可以接受新连接。若未达上限则clientCount原子自增然后创建std::thread对象并立即调用detach()让线程在后台运行。若已达上限则向客户端发送“繁忙”消息后关闭套接字不创建线程。最后主循环退出时通常不会发生除非accept 失败释放服务器套接字并清理Winsockclosesocket(serversoc);WSACleanup();return0;4.2 客户端client.cpp支持循环交互客户端代码也做了增强现在支持用户多次输入并接收服务器回显输入quit可主动断开连接。大部分内容与之前相同这里只贴出循环部分while(true){// 输入消息发送给服务端std::cout请输入发送给服务器的字符输入 quit 退出std::endl;std::cinbuf;// 检查是否要主动断开if(strcmp(buf,quit)0){std::cout主动断开连接std::endl;break;}// 发送消息if(send(clientsocket,buf,strlen(buf),0)0){std::cout发送失败std::endl;break;}// 接收服务器回复阻塞intrecvRetrecv(clientsocket,buf,sizeof(buf)-1,0);if(recvRet0){std::cout接收失败或服务器断开连接std::endl;break;}std::cout服务器回复bufstd::endl;}4.3 CMake 构建配置CMakeLists.txtCMake 配置需要指定 C 标准为 C11因为使用了thread和atomic同时链接ws2_32库。如果你使用的是 MinGWstd::thread可能需要链接pthread库某些 MinGW 版本默认依赖因此我们也在服务端链接了pthreadcmake_minimum_required(VERSION3.10)set(CMAKE_CXX_COMPILERD:/mingw64/x86_64-16.1.0-release-win32-seh-ucrt-rt_v14-rev1/mingw64/bin/g.exe)project(TcpDemo)set(CMAKE_CXX_STANDARD11)set(CMAKE_CXX_STANDARD_REQUIRED ON)add_executable(server server.cpp)if(WIN32)target_link_libraries(server ws2_32 pthread)# pthread 可能被 std::thread 依赖endif()add_executable(client client.cpp)if(WIN32)target_link_libraries(client ws2_32)endif()五、直接使用 g 命令行编译的方法如果你不想使用 CMake也可以直接在命令行下分别编译服务端和客户端。记得启用 C11 标准并链接必要的库编译服务端多线程g-stdc11 server.cpp-oserver.exe-lws2_32-lpthread编译客户端g-stdc11 client.cpp-oclient.exe-lws2_32六、完整代码如下server.cpp#includethread#includeiostream#includecstring#includeatomic#includewinsock2.h#defineMAX_CLIENTS5std::atomicintclientCount(0);//当前客户端数量线程安全voidhandleClient(SOCKET clientSock,SOCKADDR_IN clientaddr){charbuf[1024];//打印客户端信息打印服务器端线程IDstd::cout[Thread std::this_thread::get_id()] 客户端端口:ntohs(clientaddr.sin_port) 已连接std::endl;while(true){//接收数据、阻塞等待intrecvRetrecv(clientSock,buf,sizeof(buf),0);if(recvRet0){if(recvRet0){std::cout[Thread std::this_thread::get_id()] 客户端主动断开连接std::endl;}else{std::cout[Thread std::this_thread::get_id()] 接收失败std::endl;}break;}std::cout[Thread std::this_thread::get_id()] 接收来自客户端的信息: bufstd::endl;//发送数据(回显)if(send(clientSock,buf,strlen(buf),0)0){std::cout[Thread std::this_thread::get_id()] 发送失败std::endl;break;}}// 关闭客户端套接字并减少计数closesocket(clientSock);clientCount--;std::cout[Thread std::this_thread::get_id()] 客户端断开当前在线数: clientCountstd::endl;}intmain(){WSADATA wsa;WSAStartup(MAKEWORD(2,0),wsa);//创建套接字SOCKET serversocsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(serversocINVALID_SOCKET){std::cout套接字socket创建失败std::endl;WSACleanup();return-1;}SOCKADDR_IN serveraddr;//定义服务器自身的 IP 端口信息serveraddr.sin_familyAF_INET;serveraddr.sin_porthtons(9102);serveraddr.sin_addr.S_un.S_addrinet_addr(192.168.31.86);//绑定套接字if(bind(serversoc,(SOCKADDR*)serveraddr,sizeof(SOCKADDR_IN))!0){std::cout套接字socket绑定失败std::endl;closesocket(serversoc);// 绑定失败释放服务端socketWSACleanup();return-1;}std::cout开始监听...std::endl;if(listen(serversoc,SOMAXCONN)!0){std::cout监听失败std::endl;closesocket(serversoc);WSACleanup();return-1;}std::cout服务器启动最大并发连接数: MAX_CLIENTSstd::endl;// 主循环接受连接while(true){SOCKADDR_IN clientaddr;intlensizeof(clientaddr);SOCKET clientsocaccept(serversoc,(SOCKADDR*)clientaddr,len);if(clientsocINVALID_SOCKET){std::coutaccept 失败退出主循环std::endl;break;}// 判断是否达到最大连接数if(clientCountMAX_CLIENTS){clientCount;// 增加计数// 创建工作线程detach 使其独立运行std::thread(handleClient,clientsoc,clientaddr).detach();}else{// 拒绝连接constchar*msg服务器当前繁忙请稍后再试。\n;send(clientsoc,msg,strlen(msg),0);closesocket(clientsoc);std::cout拒绝连接已达到最大并发数std::endl;}}// 主循环退出时释放服务器套接字(实际不会执行到这里)closesocket(serversoc);WSACleanup();return0;}client.cpp#includewinsock2.h#includeiostream#includecstring// strlenintmain(){SOCKET clientsocket;SOCKADDR_IN serveraddr;charbuf[1024];WSADATA wsa;WSAStartup(MAKEWORD(2,0),wsa);// 创建客户端套接字if((clientsocketsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP))0){std::cout套接字socket创建失败std::endl;WSACleanup();return-1;}// 配置要连接的服务端地址serveraddr.sin_familyAF_INET;serveraddr.sin_porthtons(9102);serveraddr.sin_addr.S_un.S_addrinet_addr(192.168.31.86);std::cout请求连接服务端...std::endl;if(connect(clientsocket,(SOCKADDR*)serveraddr,sizeof(SOCKADDR_IN))!0){std::cout连接失败std::endl;closesocket(clientsocket);// 连接失败释放套接字WSACleanup();return-1;}std::cout连接服务端成功std::endl;while(true){// 输入消息发送给服务端std::cout请输入发送给服务器的字符输入 quit 退出std::endl;std::cinbuf;// 检查是否要主动断开if(strcmp(buf,quit)0){std::cout主动断开连接std::endl;break;// 退出循环随后关闭套接字}// 发送消息if(send(clientsocket,buf,strlen(buf),0)0){std::cout发送失败std::endl;break;}// 接收服务器回复阻塞intrecvRetrecv(clientsocket,buf,sizeof(buf)-1,0);if(recvRet0){std::cout接收失败或服务器断开连接std::endl;break;}std::cout服务器回复bufstd::endl;}// 程序正常退出释放资源closesocket(clientsocket);WSACleanup();std::cout客户端正常退出std::endl;return0;}七、运行效果启动服务器server.exe启动客户端1client.exe启动客户端1后服务器端响应启动客户端2client.exe及 服务器端响应客服端1发送字符串abcd客户端2发送字符串12345客户端相继主动断开希望这篇博客能帮助你快速上手 Windows 下的多线程网络编程。

相关新闻