IOCP服务器模型

知识铺垫

1.网卡与socket API

       真正与网络相关的操作,都是网卡在做,而API就是网卡的驱动,比如recv,就是程序从网卡中读取数据,send,就是将一块数据通过总线传给网卡,然后由网卡发到网络中。

2.线程的调度

       既然程序本身并不直接与网络交互,那么,怎么知道网络另外那头有数据发送过来了呢?在Windows NT3.5版本之前,就是不断的用每一个socket作为参数去recv,若返回值>0,就去处理业务,但是当客户端socket多了以后怎么办呢?一个线程处理不过来就开多个线程吧。这应该是早期的服务器模型了。但是这样做的效果不好,因为开启的线程是有任务在执行的,尽管执行的任务不复杂,但是的确在消耗CPU资源。例如下面这两段代码:

循环1

while(true)
{
	//就是一个死循环,什么也不做;
}

循环2

while(true)
{
	getchar();//等待用户输入一个字符;
	/*
		用户输入完后开始执行一堆操作;
	*/
}

       都是死循环,循环1就在不停的消耗资源,循环2在等待用户输入,处于挂起状态,不占用CPU资源。直到用户输入字符后,才开始占用CPU执行下面的代码。

Windows IOCP

       官方解释就自行百度吧,针对上面提到的两个问题,IOCP都很好的解决了,因为涉及内核和硬件的范畴,你不用管它是怎么做到的,它的确是做到了。

       程序不需要再死循环的去询问每一个socket“你现在有数据吗?”,而是内核主动告诉程序,“这个socket接收到数据了,你看怎么办”,或,“刚才给这个socket发送的数据已经发送完成了,你看怎么办”。

       你开启的线程再空闲时期也不会占用CPU资源了,因为此时的线程与上面提到的循环2情况一样,是处于被挂起状态,在等待着内核通知它,“有个socket的操作完成了,你别睡了,起来看看怎么办”。如此一来,线程被高效的调度了,也就不需要开那么多的线程了,理论上讲,开始CPU物理核心*2的线程数,是最佳状态。

       官方解释IOCP叫“完成端口”,我觉得,本质就是异步的事件驱动机制

Linux EPOLL

       在Linux平台下也有一种优化方案,与windows IOCP扮演的角色一样,它叫EPOLL,机制都是异步的事件驱动,但内部原理不太一样,有兴趣的可以研读一下。

IOCP的关键API

创建完成端口

HANDLE CreateIoCompletionPort(
       HANDLE FileHandle,
       HANDLE ExistingCompletionPort,
       ULONG_PTR CompletionKey,
       DWORD NumberOfConcurrentThreads
);

       创建一个IOCP完成端口句柄,你可以这样理解,CreateIoCompletionPort返回一个IOCP完成端口的指针(但它不是指针,只是代表这个IOCP完成端口而已,后面的操作都需要针对这个代表进行操作)。

接收数据

int WSAAPI WSARecv (
       SOCKET s,
       LPWSABUF lpBuffers,
       DWORD dwBufferCount,
       LPDWORD lpNumberOfBytesRecvd,
       LPINT lpFlags,
       LPWSAOVERLAPPED lpOverlapped,
       LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

       参数较多并且比较复杂,暂时只要知道接收数据时使用这个函数,但这个函数并不直接返回接收到的结果,可以理解成向系统提交了一次数据接收任务。

发送数据

int WSASend (
       SOCKET s,
       LPWSABUF lpBuffers
       DWORD dwBufferCount,
       LPDWORD lpNumberOfBytesSent,
       DWORD dwFlags,
       LPWSAOVERLAPPED lpOverlapped,
       LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

       参数较多并且比较复杂,暂时只要知道发送数据时使用这个函数,但这个函数并不立即发送,可以理解成向系统提交了一次数据发送任务。

获取已完成的任务

BOOL GetQueuedCompletionStatus(
       HANDLE CompletionPort,
       LPDWORD lpNumberOfBytes,
       PULONG_PTR lpCompletionKey,
       LPOVERLAPPED* lpOverlapped,
       DWORD dwMilliseconds
);

       这个函数是在工作线程中被调用的,每个线程执行到这个函数时,如果没有已完成的任务,就会挂起在这个地方(也可以设置成不挂起或挂起指定时间),当有任务完成时(发送或接受完成),线程自动被唤醒,继续向下执行。

直接上IOCP的代码吧

先看两个结构体WNSOperation(异步输入输出信息的结构体)和ClientStruct(客户端链接对象)


//用于异步输入输出信息的结构体
struct WNSOperation
{
	 OVERLAPPED		ol;			//IOCP结构体,必须放在第一个;
	 WSABUF			wbuf;		//缓冲区;
	 DWORD			dwBytes;	//上一次操作的字节数;
	 DWORD			dwFlags;	//标志位;
	 int			signal;		//标志位;
	 char			OpCode;		//操作标识,用来判断这个异步操作是读取('r')还是写入('w');
};

//客户端链接结构体;
class ClientStruct
{
public:
	SOCKET					socket;				//SOCKET对象;
	WNSOperation			pReadOperation;		//异步读取对象;
	bool					bSend;				//是否正在进行发送;
private:
	HANDLE					sendQueueLock;		//数据发送队列lock;
	queue<WNSOperation*>	sendQueue;			//数据发送队列;
public:

	ClientStruct(int ReadBufferSize)
	{
		bSend = false;
		pReadOperation.wbuf.buf = new char[ReadBufferSize];
		pReadOperation.OpCode = 'r';
	}

	~ClientStruct()
	{
		//先取出发送队列中未完成的BUFFER释放掉;
		WNSOperation* res = NULL;
		WaitForSingleObject(sendQueueLock,INFINITE);
		while(sendQueue.empty() == false)
		{
			res = sendQueue.front();
			sendQueue.pop();
			delete res;
		}
		ReleaseMutex(sendQueueLock);
		
		//释放接收BUFFER;
		if(pReadOperation.wbuf.buf != NULL)
		{
			delete[] pReadOperation.wbuf.buf;
			pReadOperation.wbuf.buf = NULL;
		}

		closesocket(socket);
	}
	
	//添加发送BUFFER到发送队列中;
	void AddSendBuffer(WNSOperation* w)
	{
		WaitForSingleObject(sendQueueLock,INFINITE);
		sendQueue.push(w);
		ReleaseMutex(sendQueueLock);
	}

	//从发送队列中获取一个BUFFER,如果当前客户端正在进行发送任务,则返回NULL;
	WNSOperation* GetSendBuffer()
	{
		WNSOperation* res = NULL;
		WaitForSingleObject(sendQueueLock,INFINITE);
		if(bSend == true)
		{
			//如果bSend == true,则表示这个客户端正在执行发送任务;
		}
		else 
		{
			if(sendQueue.empty() == false)
			{
				res = sendQueue.front();
				sendQueue.pop();
				bSend = true;
			}
			else 
			{
				bSend = false;
				printf("发送任务已结束\n");
			}
		}
		ReleaseMutex(sendQueueLock);
		return res;
	}

	//等IOCP发送完成后,需要调用这个函数将bSend设置到false
	void ResetSendSignal()
	{
		WaitForSingleObject(sendQueueLock,INFINITE);
		bSend = false;
		ReleaseMutex(sendQueueLock);
	}
};

#include "stdafx.h"
#include 
#include 
#include 
using namespace std;
#pragma  comment(lib, "ws2_32.lib")


//工作线程数;
#define WORK_THREAD_COUNT 4
//客户端接收缓冲区大小;
#define READ_BUFFER_SIZE 8000

SOCKET socket_listen;//监听socket;

HANDLE Handle_Iocp;//iocp句柄;

HANDLE workThread[WORK_THREAD_COUNT];//工作线程句柄;

HANDLE acceptThread;//监听线程句柄;

//创建一个发送BUFFER;
WNSOperation* CreateSendBuff(int bufLen)
{
	WNSOperation* sendBuffer = new WNSOperation();
	memset(&sendBuffer->ol, 0, sizeof(OVERLAPPED));
	sendBuffer->OpCode = 'w';
	sendBuffer->dwBytes = 0;
	sendBuffer->dwFlags = 0;
	sendBuffer->wbuf.buf = new char[bufLen];
	return sendBuffer;
}

//使一个ClientStruct开始接收数据;
bool ReadData(ClientStruct* pClientStruct, int offset, int count, int signal)
{
	memset(&pClientStruct->pReadOperation.ol, 0, sizeof(OVERLAPPED)); 
	pClientStruct->pReadOperation.wbuf.buf = pClientStruct->pReadOperation.wbuf.buf + offset;
	pClientStruct->pReadOperation.wbuf.len = count;
	pClientStruct->pReadOperation.dwFlags = 0;
	pClientStruct->pReadOperation.dwBytes = 0;
	pClientStruct->pReadOperation.signal = signal;
	int ret = WSARecv(pClientStruct->socket, &pClientStruct->pReadOperation.wbuf, 1, &pClientStruct->pReadOperation.dwBytes, &pClientStruct->pReadOperation.dwFlags, &pClientStruct->pReadOperation.ol, NULL);  
	if (ret == -1)
	{
		ret = WSAGetLastError();
		if(ret == 997)
			return true;
		printf("ReadData >>> %d\n", ret);
		return false;
	}
	return true;  
}

//使一个ClientStruct开始发送数据;
bool SendData(ClientStruct* pClientStruct)
{
	WNSOperation* obj = pClientStruct->GetSendBuffer();
	if(obj == NULL)
		return true;
	int ret = WSASend(pClientStruct->socket, &obj->wbuf, 1, &obj->dwBytes, obj->dwFlags, &obj->ol, NULL);  
	if (ret == -1)
	{
		ret = WSAGetLastError();
		if(ret == 997)
			return true;
		printf("SendData >>> %d\n", ret);
		return false;
	}
	return true;
}

//监听线程;
DWORD __stdcall AcceptThreadFunction(LPVOID Param)
{
	sockaddr addr;
	int clientlen = sizeof(struct sockaddr);
	printf("监听线程已启动\n");
	while(true)  
	{
		ClientStruct* newClient = new ClientStruct(READ_BUFFER_SIZE);
		if ((newClient->socket = accept(socket_listen,(struct sockaddr *)&addr, &clientlen)) == INVALID_SOCKET)  
		{
			printf("accept失败");
			continue;
		}
		
		if (CreateIoCompletionPort((HANDLE)newClient->socket, Handle_Iocp, (ULONG_PTR)newClient, 0) == NULL) 
		{
			printf("CreateIoCompletionPort失败");
			continue;
		}
		ReadData(newClient, 0, READ_BUFFER_SIZE, 0);
	}
	printf("监听线程已退出\n");
	return 0;
}

//工作者线程
DWORD __stdcall WorkThreadFunction(LPVOID Param)
{
	ClientStruct* pClientStruct;
	OVERLAPPED* pOverlap;
	WNSOperation* pIOCPOperation;
	DWORD berByte;
	printf("工作线程已启动\n");
	while(true)
	{
		//线程执行到这里时会被挂起,等待一个异步操作完成;
		if(GetQueuedCompletionStatus(Handle_Iocp, &berByte, (LPDWORD)&pClientStruct, (LPOVERLAPPED *)&pOverlap, INFINITE) == ERROR_SUCCESS)  
		{
			printf("GetQueuedCompletionStatus >>> %d\n", WSAGetLastError());
			continue;  
		}

		if (berByte <= 0)
		{
			delete pClientStruct;
			continue;
		}

		if (pClientStruct == NULL)
		{
			printf("pClient==NULL\n");
			continue;
		}

		pIOCPOperation = (WNSOperation*)pOverlap;	//这是一个WNSOperation对象,通过OpCode判断是读取操作还是发送操作;
		switch(pIOCPOperation->OpCode)
		{
		case 'r':
			{
				//我在buf最后插入一个'\0'字符打印一下;
				pClientStruct->pReadOperation.wbuf.buf[berByte] = '\0';	
				printf("%s\n", pClientStruct->pReadOperation.wbuf.buf);
			
				//调用CreateSendBuff创建一个用于发送的WNSOperation;
				WNSOperation* sendBuffer1 = CreateSendBuff(berByte);
				//拷贝接收到的数据在发送回去;
				memcpy(sendBuffer1->wbuf.buf, pClientStruct->pReadOperation.wbuf.buf, berByte);
				sendBuffer1->wbuf.len = berByte;
				//sendBuffer1加入到这个客户端的发送队列中;
				pClientStruct->AddSendBuffer(sendBuffer1);
				
				{
					//为了测试发送队列,我在追加一组数据;
					WNSOperation* sendBuffer2 = CreateSendBuff(10);
					memcpy(sendBuffer2->wbuf.buf, "1234567890", 10);
					sendBuffer2->wbuf.len = 7;//最终发送的数据长度是在这里设定的;
					pClientStruct->AddSendBuffer(sendBuffer2);
				}
				
				//使这个客户端进入发送状态;
				SendData(pClientStruct);

				//同时进入读取状态;
				ReadData(pClientStruct, 0, READ_BUFFER_SIZE, 0);
			}
			break;
		case 'w':
			//发送完成后,这个WNSOperation对象就可以被删除了;
			delete[] pIOCPOperation;
			pIOCPOperation = NULL;
			//重置客户端发送状态;
			pClientStruct->ResetSendSignal();
			//再次进入发送状态,如果发送队列中有未发送的BUF,则继续发送;
			SendData(pClientStruct);
			break;
		default:
			continue;
		}
	}
	printf("工作线程已退出\n");
	return 0;
};

int _tmain(int argc, _TCHAR* argv[])
{
	WSADATA wsd;  
	if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
	{
		printf("初始化失败");
		getchar();
		return 0;
	}

	socket_listen = socket(AF_INET, SOCK_STREAM, 0);
	if (socket_listen == INVALID_SOCKET)  
	{
		printf("初始化socket失败");
		getchar();
		return 0;
	}

	sockaddr_in addr;  
	addr.sin_family = AF_INET;
	addr.sin_port = htons(9090);  
	addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  
	
	if (bind(socket_listen, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR)
	{
		printf("bind失败");
		getchar();
		return 0;
	}
	
	if (listen(socket_listen, 0) == SOCKET_ERROR)
	{
		printf("listen失败");
		getchar();
		return 0;
	}
	if ((Handle_Iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0)) == NULL)
	{
		printf("创建完成端口失败");
		getchar();
		return 0;
	}
	
	//启动监听线程;
	acceptThread = CreateThread(NULL, 0,AcceptThreadFunction, NULL, 0, NULL);

	//启动工作线程;
	for(int i=0;i<WORK_THREAD_COUNT;i++)
	{
		workThread[i] = CreateThread(NULL, 0,WorkThreadFunction, NULL, 0, NULL);
	}

	//主线程歇了吧
	while(true)
	{
		getchar();
	}
	return 0;
}

你可能感兴趣的:(后台,C++,IOCP)