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;
}

运行效果:

posted @ 2022-10-27 13:11  hugeYlh  阅读(31)  评论(0编辑  收藏  举报  来源