windows网络编程 --网络聊天室(2)
IOCP模型
IOCP :输入输出完成端口。
是支持多个同时发生的异步I/O操作的应用程序编程接口,IOCP特别适合C/S模式网络服务器端模型。
因为,让每一个socket有一个线程负责同步(阻塞)数据处理,one-thread-per-client的缺点是:一是如果连入的客户多了,就需要同样多的线程;二是不同的socket的数据处理都要线程切换的代价。
最佳线程数量=cpu内核数量*2
一个IOCP对象,在操作系统中可关联着多个Socket和(或)文件控制端。 IOCP对象内部有一个先进先出(FIFO)队列,用于存放IOCP所关联的输入输出端的服务请求完成消息。请求输入输出服务的进程不接收IO服务完成通知,而是检查IOCP的消息队列以确定IO请求的状态。
- 队列有消息: (线程池中的)多个线程负责从IOCP消息队列中取走完成通知并执行数据处理;
- 如果队列中没有消息,那么线程阻塞挂起在该队列。这些线程从而实现了负载均衡。
IOCP是一个内核对象,但是他是一个不需要安全属性的Windows内核对象
常用IOCP函数
使用IOCP模型,首先必须创建(或者关联)一个IOCP对象(与文件句柄):
HANDLE CreateIoCompletionPort( [in] HANDLE FileHandle,//要关联的文件句柄 [in, optional] HANDLE ExistingCompletionPort,//完成端口句柄 [in] ULONG_PTR CompletionKey,//完成键 [in] DWORD NumberOfConcurrentThreads//允许的最大线程数 );
CreateIoCompletionPort 函数创建 IOCP完成端口并将其与指定的文件句柄相关联,或者创建一个新的未被关联的IOCP对象。
- 参数一:接受一个文件句柄或者为NULL。 文件句柄:将其与一个已存在的IOCP对象相关联; NULL:创建一个新的IOCP对象。
- 参数二:接受一个现有IOCP对象或 NULL 的句柄。现有IOCP对象: 将其与一个文件句柄相关联;NULL:创建一个新的IOCP对象。
- 参数三:指定文件句柄的每个 I/O 完成数据包中包含的每句柄用户定义完成密钥。如果是关联已存在文件句柄,则此参数为此文件句柄,否则为空。
- 参数四:一个IOCP允许处理的最大线程数,NULL为系统默认。
获取IOCP的消息队列:
BOOL GetQueuedCompletionStatus( [in] HANDLE CompletionPort,//IOCP对象句柄 LPDWORD lpNumberOfBytesTransferred,//IO操作传输的字节数 [out] PULONG_PTR lpCompletionKey,//发送消息方 [out] LPOVERLAPPED *lpOverlapped,//接受消息,并且存储消息的结构体指针 [in] DWORD dwMilliseconds//接受等待时间 );
GetQueuedCompletionStatus 函数尝试从指定的 I/O 完成端口取消 I/O 完成数据包的排队。
如果没有排队的完成数据包,该函数将等待与完成端口关联的挂起 I/O 操作完成。
说白了就是 接收从指定的发送方发送的消息,并且取消排队。
- 参数一:正在操作的IOCP对象。
- 参数二 :准备从发送方接收的数据的字节数量,接收成功返回接收的字节数,失败返回NULL,表示读取失败。
- 参数三:指定的消息发送方,即客户端。
- 参数四:接收消息的一个结构体,消息信息都存储在这个结构体中。
- 参数五:等待的时间,相当于WaitForSingleoObect的函数等待。
基于IOCP的网络聊天室
服务器端
#include <WinSock2.h> #include <Windows.h> #include <iostream> #include <ws2tcpip.h> #include <vector> #pragma comment(lib,"ws2_32.lib") std::vector<SOCKET> Vec_Socket; typedef struct My_Overlapped { OVERLAPPED wsaoverlapped; WSABUF wsabuf; }My_Overlapped,*PMy_Overlapped; DWORD WINAPI DoMessage( LPVOID lpParameter ) { HANDLE SocketHandle = (HANDLE)lpParameter; BOOL Result = FALSE; PMy_Overlapped myOverlapped = nullptr; DWORD NumOfReadISze = 0; ULONG_PTR ComplixKey = 0; DWORD Flags = 0; //指向接收与 I / O 操作已完成的文件句柄关联的完成键值的变量的指针 ULONG_PTR client{ 0 }; while (true) { //等待消息的读入 Result = GetQueuedCompletionStatus( SocketHandle, &NumOfReadISze, &client, //从每一个客户句柄里读取信息 (LPOVERLAPPED*)&myOverlapped, INFINITE ); if (Result && NumOfReadISze > 0) { for (UINT i = 0; i < Vec_Socket.size(); i++) { //发送消息 if (Vec_Socket[i] != client) {//句柄不是你自己,往其他人客户端发送消息 send(Vec_Socket[i], myOverlapped->wsabuf.buf, myOverlapped->wsabuf.len, NULL); } } //清空缓冲区 memset(myOverlapped->wsabuf.buf, 0, 0x100); memset(&myOverlapped->wsaoverlapped, 0, sizeof(OVERLAPPED)); WSARecv( client, &myOverlapped->wsabuf, 1, &NumOfReadISze, &Flags, (LPWSAOVERLAPPED)myOverlapped, NULL ); } else { //卸载客户端 for (UINT i = 0; i < Vec_Socket.size(); i++) { if (Vec_Socket[i] == client) { closesocket(client); printf("客户端%u断开了连接\n", Vec_Socket[i]); Vec_Socket.erase(Vec_Socket.begin() + i); } } } } return 1; } int main() { // 创建IOCP对象 HANDLE IOCP = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, NULL, NULL ); if (!IOCP) { printf("IOCP创建失败!\n"); return 0; } //获取CPU的内核处理器数量 SYSTEM_INFO system_info{ 0 }; GetNativeSystemInfo(&system_info); for (UINT i = 0; i < system_info.dwNumberOfProcessors*2; i++) { CreateThread( NULL, NULL, DoMessage, (LPVOID)IOCP, //IOCP传入 NULL, NULL ); } //1. 初始化网络环境 WSADATA WsaData{ 0 }; WSAStartup( MAKEWORD(2, 2), //套接字 &WsaData ); //2. 创建socket套接字 //创建绑定到特定传输服务提供者的套接字。 SOCKET serve = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); //3. 绑定端口号IP地址 sockaddr_in serverAddr{ 0 }; serverAddr.sin_family = AF_INET; //地址族 serverAddr.sin_port = htons(666); //端口号 //inet_pton:标准文本呈现形式中将 IPv4 或 IPv6 Internet 网络地址转换为其数字二进制形式 inet_pton(AF_INET, "172.20.230.126", &serverAddr.sin_addr); //bind:绑定函数将本地地址与套接字相关联。 bind(serve, (sockaddr*)&serverAddr, sizeof(serverAddr)); //4. 监听 //侦听函数将套接字置于侦听传入连接的状态。 listen(serve, SOMAXCONN); sockaddr_in ClientAddr{ 0 }; int SockSize = sizeof(ClientAddr); //5. 接收会话 while (1) { //连接客户端与服务器,返回客户端句柄 SOCKET client = accept( serve, //标识已使用监听函数处于侦听状态的套接字 (sockaddr*)&ClientAddr, &SockSize ); //保存所有的客户端句柄于一个容器中 Vec_Socket.push_back(client); //将SOCKET关联到IOCP CreateIoCompletionPort( (HANDLE)client, IOCP, client, NULL ); PMy_Overlapped MyOverlapped = new My_Overlapped{ 0 }; MyOverlapped->wsabuf.buf = new char[0x100] {0}; MyOverlapped->wsabuf.len = 0x100; //缓冲区的长度 DWORD RealSize = 0; DWORD Flags = 0; //接收消息 WSARecv( client, &MyOverlapped->wsabuf, 1, &RealSize, &Flags, (LPWSAOVERLAPPED)MyOverlapped, NULL ); printf("服务器%u连接到了客户端\n", client); } closesocket(serve); system("pause"); return 0; }
客户端
#include <WinSock2.h> #include <Windows.h> #include <WS2tcpip.h> #include <iostream> #pragma comment(lib,"ws2_32.lib") DWORD WINAPI GetMessageAAA( LPVOID lpParameter ) { SOCKET Socket = (SOCKET)lpParameter; CHAR DstBuff[0x100]{ 0 }; while (recv(Socket, DstBuff, 0x100, NULL) > 0) { printf("接收:%s\n", DstBuff); } return 1; } int main() { //1. 初始化网络连接 WSADATA WsaData{ 0 }; WSAStartup(MAKEWORD(2, 2), &WsaData); //2. 创建SOCKET套接字 SOCKET serve = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //3. 绑定IP地址 sockaddr_in serveAddr{ 0 }; serveAddr.sin_family = AF_INET; //地址族 serveAddr.sin_port = htons(666); //端口号 inet_pton(AF_INET, "172.20.230.126", &serveAddr.sin_addr); //连接服务器 //connect函数建立与指定套接字的连接。 int result = connect(serve, (sockaddr*)&serveAddr, sizeof(serveAddr)); if (result != 0) //未发生错误,返回零 { printf("连接失败!\n"); system("pause"); return 0; } //创建线程 HANDLE Thread = CreateThread( NULL, NULL, GetMessageAAA, (LPVOID)serve, //传入当前的客户端句柄 NULL, NULL ); CHAR lpBuff[0x100]{ 0 }; while (scanf("%s", lpBuff) && strcmp(lpBuff, "exit")) { send(serve, lpBuff, strlen(lpBuff)+1, NULL); } system("pause"); closesocket(serve); return 0; }
运行效果:
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209676.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)