IOCP实现高并发以及与传统socke编程的对比

Posted revercc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IOCP实现高并发以及与传统socke编程的对比相关的知识,希望对你有一定的参考价值。

前言

传统socket编程中服务端一般为每一个客户端开启一个线程(一对一)。这样虽然可以使程序的结构简单明了并且方便对数据处理,但是这些都是建立在创建多个线程的基础上,也就是以牺牲线程为代价。一旦有大量数量了客户端连接服务端,我们的服务端需要开启很多线程这显然是不能被我们所接受的。那么为了解决这个问题就必须采用一种方法令有限的线程去处理所有的客户端连接,windows的IOCP完成端口就可以帮助我们完成这个操作。

IOCP实现高并发思路

技术图片

相关函数

  • WSARecv/WSASend
    WSARecv/WSASend与函数recv/send函数相对应,前者为异步函数,后者为同步函数。以WSARecv( )函数为例,此函数相当于往任务队列中投递一个接受包的任务,然后立刻返回而此时包并未真正受到,等到客户端真正把数据包发送时,线程池中的线程会从任务队列中将此任务取出同时得到收到的数据。

  • CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);
    此函数为IOCP对象的创建于绑定函数,每当一个客户端与服务端连接时我们都要将IOCP对象与此客户端的socket绑定。在客户端绑定IOCP完成端口对象时注意第三个参数CompletionKey,此参数为完成键。因为我们客户端的数据包最后都到数量受限的线程池中处理,为了区分是哪个客户端的数据包需要为每个不同的客户端指定不同的完成键,从而标志不同的客户端(通常用客户端的套接字作为完成键)。

  • GetQueuedCompletionStatus()
    哪个线程调用GetQueuedCompletionStatus()函数,其就会被IOCP完成对象认为是线程池中的一个线程。此函数从IOCP任务队列中取出任务,如果最后一个参数指定为INFINITE,则只有当真正接收到来自客户端的数据包时才返回,否则一直等待。

  • PostQueuedCompletionStatus()
    此函数可以向线程池中的每一个函数传递一个数据包,在退出程序的时候可以通过此函数来向线程池中的每一个线程发送一个特定的数据包使其线程安全退出

在利用IOCP完成端口对象时遇到的一些问题

  • 因为WSARecv()在异步接受数据时会指定接受数据的内存,为了通过重叠结构传递这块内存,需要new[]出来这块内存放到堆中,所以记得delete[]。
    因为重叠结构是IOCP完成对象与线程池中线程交互进一步传递来自客户端的数据的,所以这块内存也需要放到堆中new出来最后也要delete。
        DWORD	dwSize;
	DWORD	dwFlag = 0;
	WSABUF	stBuffer;
	stBuffer.buf = new char[0x1000]();
	stBuffer.len = 0x1000;


	MYOVERLAPPED* lpMyOVERLAPPED = new MYOVERLAPPED;
	memset(lpMyOVERLAPPED, 0, sizeof(MYOVERLAPPED));
	lpMyOVERLAPPED->nTypr = TYPE_RECV;				//表示发送一个收包请求任务
	lpMyOVERLAPPED->pBuf  =	(BYTE *)stBuffer.buf;			//使接收到的数据通过重叠结构传递


	//异步接受消息(向任务队列投递一个接受请求)
	WSARecv(
		h,
		&stBuffer,					//缓冲区结构
		1,						//缓冲区数组数量
		&dwSize,
		&dwFlag,
		&(lpMyOVERLAPPED->ol),			        //标准重叠结构
		NULL);


  • 因为我们客户端发送消息的形式一般是先发送包头,在发送包尾。所以 GetQueuedCompletionStatus()在从任务队列中取数据也是先获得包头数据再获得包尾数据。然后将其拼接成完整的包后处理。
  • 我们在处理完一个包后需要往任务队列中再发送接收任务,等待下一次客户端的数据到达。这时我们需要在调用WSARecv()异步接受数据时需要重新指定新的内存给IOCP存放接受到的数据使用。所以需要重新new,至于new的大小取决于我们是接下来是接收包头还是接收包尾。接收包头就是new 包头大小,包尾就是new对应的包尾大小。(一个包分两次接收)。
    注意不要采用每次new固定的大小内存来接收,这样会使GetQueuedCompletionStatus()一次获取不完包中数据从而进行多次调用,而在如果在获取包的最后的数据不足new的大小的话,其会把下一个包的数据一起放进来给我们带来不必要的麻烦。
        DWORD	dwSize;	
	DWORD	dwFlag = 0;
	WSABUF	stBuffer;
	if (pClient->stWrap.dwLength == 0)				//如果刚收完包尾,继续收下一个包的包头
	{
		stBuffer.buf = new char[8]();
		stBuffer.len = 8;
	}
	else								//如果刚收完包头,则收对应大小的包尾
	{
		stBuffer.buf = new char[pClient->stWrap.dwLength]();
		stBuffer.len = pClient->stWrap.dwLength;
	}

	memset(pMyOVERLAPPED, 0, sizeof(MYOVERLAPPED));
	pMyOVERLAPPED->nTypr = TYPE_RECV;				//表示发送一个收包请求任务
	pMyOVERLAPPED->pBuf = (BYTE*)stBuffer.buf;		        //使接收到的数据通过重叠结构传递

	//异步接受消息(向任务队列投递一个接受请求)
	WSARecv(pClient->hSocketClient, &stBuffer, 1, &dwSize, &dwFlag, &(pMyOVERLAPPED->ol), NULL);






以上是关于IOCP实现高并发以及与传统socke编程的对比的主要内容,如果未能解决你的问题,请参考以下文章

[高并发]Java高并发编程系列开山篇--线程实现

Java与Netty实现高性能高并发

与传统的物理服务器对比,云服务器有哪些优势?

与传统的物理服务器对比,云服务器有哪些优势?

一种 Windows IOCP 整合 OpenSSL 实现方案

一种 Windows IOCP 整合 OpenSSL 实现方案