[IO复用] Windows IOCP的简单实现

前言

之前简单学了一下IOCP,这里结合晚上的示例,写一个简单的IOCP测试代码。

代码

服务端代码

代码结构

这里通过主线程进行主要代码管理、各种SOCKET和IOCP对象的创建、WSARECV的投递、退出IOCP等待等操作。
子线程就负责从GetQueuedCompletionStatus()中获取完成的信息。

主要参考以下文章中的模型:
IO完成端口(IOCP) - 木文的文章 - 知乎
用户态和内核态的相对关系:
在这里插入图片描述
主线程和子线程的工作流程:
在这里插入图片描述
图片来源:
IO完成端口(IOCP) - 木文的文章 - 知乎

代码

#include <WinSock2.h>
#include <iostream>
#include <thread>
#include <mutex>
#pragma comment(lib, "ws2_32.lib")

std::mutex mtx;
inline void OPT(const char * head, const char* msg)
{
	std::lock_guard<std::mutex> lkg(mtx);
	std::cout << head << " - " << msg << std::endl;
}

HANDLE hIOCP = nullptr;
struct IOContext {
	OVERLAPPED overlapped{};
	WSABUF wsaBuf{ 1024, buffer };
	CHAR buffer[1024]{};
	SOCKET socket = INVALID_SOCKET;
	DWORD nBytes = 0;
};

子线程

void _func()
{

	OPT("child thread","IN");
	IOContext* ioContext = nullptr;
	DWORD lpNumberOfBytesTransferred = 0;
	void* lpCompletionKey = nullptr;

	DWORD dwFlags = 0;
	DWORD nBytes = 1024;

	while (true) {
		OPT("child thread", "before GetQueuedCompletionStatus()");
		//等待GetQueuedCompletionStatus()返回完成的ioContext。
		//这里的阻塞选项选择的INFINITE,就是一直等待。
		BOOL bRt = GetQueuedCompletionStatus(
			hIOCP,
			&lpNumberOfBytesTransferred,
			(PULONG_PTR)&lpCompletionKey,
			(LPOVERLAPPED*)&ioContext,
			INFINITE);
		OPT("child thread", "after GetQueuedCompletionStatus()");
		if (!bRt)
		{ //根据MSDN的说法, 返回值非0,就是执行失败
			OPT("child thread", "GetQueuedCompletionStatus() failed");
			continue;
		}
		// 收到 PostQueuedCompletionStatus 发出的退出指令
		if (lpNumberOfBytesTransferred == -1)
		{
			OPT("child thread", "recv quit cmd");
			break;
		}

		if (lpNumberOfBytesTransferred == 0)
		{ // 通常认为这样相当于recv() == 0, 是断开连接的信号
			OPT("child thread", "recv msg size is 0");
			{
				int res = recv(ioContext->socket, ioContext->buffer, 1024, 0);
				if (res == 0)
				{
					OPT("child thread", "conn disconnected");
				}
				else
				{
					OPT("child thread", "continue");
					
				}
				continue;
			}
			
		}
		// 读到,或者写入的字节总数
		ioContext->nBytes = lpNumberOfBytesTransferred;
		// 处理对应的事件
		{
			// 输出读取到的内容
			std::cout << ">>>Recv:" << ioContext->buffer << std::endl;
			// 继续提交Recv请求
			OPT("child thread", "Post RECV again");
			int nRt = WSARecv(
				ioContext->socket,
				&ioContext->wsaBuf,
				1,
				&nBytes,
				&dwFlags,
				&(ioContext->overlapped),
				nullptr);
			auto e = WSAGetLastError();
			if (SOCKET_ERROR == nRt && e != WSAGetLastError()) {
				// 读取发生错误,如果发生了
				closesocket(ioContext->socket);
				delete ioContext;
				ioContext = nullptr;
			}
		}
	}
}

主线程

int main()
{
	WSAData wsadata;
	WSAStartup(MAKEWORD(2,2), &wsadata);

	//创建IOCP 
	OPT("master thread", "create IOCP");
	hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 5);
	if (INVALID_HANDLE_VALUE != hIOCP) {

		//创建工作线程
		OPT("master thread", "create thread");
		std::thread th(_func);
		
		//创建listen的SOCKET,注意需要WSA_FLAG_OVERLAPPED声明是重叠IO可以进行异步操作
		OPT("master thread", "create Overlapped socket");
		//SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		SOCKET listen_sock = WSASocket(AF_INET, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED);

		if (listen_sock != INVALID_SOCKET)
		{
			OPT("master thread", "bind Overlapped socket");
			sockaddr_in local_addr;
			local_addr.sin_family = AF_INET;
			local_addr.sin_port = htons(9100);
			local_addr.sin_addr.S_un.S_addr = ADDR_ANY;
			if (bind(listen_sock, (sockaddr*)&local_addr, sizeof(local_addr)) == 0)
			{
				OPT("master thread", "listen Overlapped socket");
				if (listen(listen_sock, 5) == 0)
				{
					sockaddr_in conn_addr;
					int sock_addr_len = sizeof(conn_addr);
					SOCKET conn_sock = accept(listen_sock, (sockaddr*)&conn_addr, &sock_addr_len);

					//将要绑定到IOCP的套接字设置为非阻塞的					
					unsigned long ul = 1;
					if (SOCKET_ERROR != ioctlsocket(conn_sock, FIONBIO, &ul)) {					

						//将要监控的SOCKET绑定到IOCP 
						OPT("master thread", "bind accept socket with Overlapped socket");
						if (nullptr != CreateIoCompletionPort((HANDLE)conn_sock, hIOCP, 0, 0))
						{
							DWORD nBytes = 1024;
							DWORD dwFlags = 0;
							auto ioContext = new IOContext;
							ioContext->socket = conn_sock;
							// 提交一次读取事件
							OPT("master thread", "post recv request to Overlapped");
							auto rt = WSARecv(ioContext->socket, &ioContext->wsaBuf, 1, &nBytes, &dwFlags, &ioContext->overlapped, nullptr);
							auto err = WSAGetLastError();
							if (SOCKET_ERROR == rt && ERROR_IO_PENDING != err) {
								// 发生不为 ERROR_IO_PENDING 的错误
								//如果发生ERROR_IO_PENDING 错误,认为这是正常的,系统在进行异步处理中
								closesocket(ioContext->socket);
								delete ioContext;
							}
							else
							{
								getchar();
								OPT("master thread", "post exit request to Overlapped");
								void* lpCompletionKey = nullptr;
								PostQueuedCompletionStatus(hIOCP, -1, (ULONG_PTR)lpCompletionKey, nullptr);
							}
						}
					}

				}
				else {//略}
			}
			else{//略}
		}
		else{//略}
		OPT("master thread", "thread join");
		th.join();
	}
	WSACleanup();
	return 0;
}

客户端模拟

使用NetAssist工具,模拟TCP客户端,进行连接,并发送一条消息"this is a test msg",然后断开连接。
在这里插入图片描述

服务端log输出如下。
在这里插入图片描述
可以看到,子线程执行以后,就卡在了GetQueuedCompletionStatus().
直到主线程投递了一个WSARECV,然后客户端发送了msg,子线程才从GetQueuedCompletionStatus()返回。

备注

重叠端口

上面的代码中,listen scoket是重叠端口,accept socket不是,但是依旧可以对accept socket完成异步操作。accept scoket没有声明为重叠端口, 为什么还可以和IOCP绑定呢?

那么如果使用上述函数但是传入一个非阻塞的SOCKET会怎么样呢,这些函数只看是否传入OVERLAPPED结构而不管SOCKET是否是阻塞的,一律按重叠IO的方式来运行。这也就是说,要使用重叠I/O方式来操作SOCKET,那么不一定非要一开初就创建一个重叠I/O方式的SOCKET对象
WinSock 重叠IO模型

如何获取客户端断开连接

  1. 返回0字节的判断
if (lpNumberOfBytesTransferred == 0)
...

通常如果获取到的msg是0,认为客户端断开了连接。
2. 必须投递recv请求否则没反应、
这里要注意的是,如果我们向IOCP绑定了一个accept socket,然后投递了一个WSARECV,然后通过 GetQueuedCompletionStatus()获取到了recv buffer。
此时就要再次WSARECV或者WSASEND,否则就再也无法通过 GetQueuedCompletionStatus()获取这个accept socekt的事件结果了。
3. 客户端异常断开连接
还有一种 GetQueuedCompletionStatus()返回结果对应客户端异常断开连接

 // 可能是客户端异常退出了(64)
            else if (ERROR_NETNAME_DELETED == dwErr)
            {
                iocp->OnConnectionError(sockContext, dwErr);

                // 回收socket
                iocp->DoClose(sockContext);
                continue;
            }

代码来源:WinSock IOCP 模型总结 - 知乎

非阻塞IO

WSARECV在配合重叠IO使用时,是非阻塞IO,是会立刻返回的。

参考

Windows 下 IOCP 的简单使用
WinSock 重叠IO模型
IO完成端口(IOCP) - 木文的文章 - 知乎
WinSock IOCP 模型总结 - 知乎

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值