IOCP中相关函数的使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SwordArcher/article/details/83269291

IOCP中相关函数的使用

部分参考于:https//www.cnblogs.com/talenth/p/7068392.html

(1)创建或者绑定完成端口

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

参数1:打开文件句柄,套接字或INVALID_HANDLE_VALUE。句柄必须是支持重叠I / O的对象,如果提供了句柄,则必须打开它才能完成重叠的I / O.例如,在使用CreateFile函数获取句柄时,必须指定FILE_FLAG_OVERLAPPED标志。如果指定了INVALID_HANDLE_VALUE,则该函数会创建一个I / O完成端口,而不会将其与文件句柄相关联。在这种情况下,ExistingCompletionPort参数必须为NULL并且忽略CompletionKey参数

参数2:现有I / O完成端口的句柄或NULL。如果此参数指定现有I / O完成端口,则该函数将其与FileHandle参数指定的句柄相关联。如果成功,该函数返回现有I / Ø完成端口的句柄; 它不会创建新的I / O完成端口。如果此参数为NULL,则该函数将创建新的I / O完成端口,如果FileHandle参数有效,则将其与新的I / O完成端口关联。否则,不会发生文件句柄关联。如果成功,该函数将返回新I / O完成端口的句柄。

参数3:每个句柄用户定义的完成密钥,包括在指定文件句柄的每个I / O完成数据包中。有关更多信息

参数4:操作系统可以允许同时处理I / O完成端口的I / O完成数据包的最大线程数。如果ExistingCompletionPort参数不为NULL 则忽略此参数。如果此参数为零,则系统允许与系统中的处理器一样多的并发运行线程

返回值:

如果函数成功,则返回值是I / O完成端口的句柄:

  • 如果ExistingCompletionPort参数为NULL,则返回值为新句柄。

  • 如果ExistingCompletionPort参数是有效的I / O完成端口句柄,则返回值是相同的句柄。

  • 如果FileHandle参数是有效句柄,则该文件句柄现在与返回的I / O完成端口相关联。

如果函数失败,则返回值为NULL。要获取扩展错误信息,请调用GetLastError函数

注意:在打开句柄的实例与I / O完成端口关联后,它不能在ReadFileExWriteFileEx函数中使用,因为这些函数具有自己的异步I / O机制。最好不要使用句柄继承或对DuplicateHandle函数的调用来共享与I / O完成端口关联的文件句柄。使用此类重复句柄执行的操作会生成完成通知建议仔细考虑。

I / O完成端口句柄和与该特定I / O完成端口关联的每个文件句柄称为对I / O完成端口的引用。当没有更多引用时,将释放I / O完成端口。因此,必须正确关闭所有这些句柄以释放I / O完成端口及其关联的系统资源。满足这些条件后,通过调用CloseHandle函数关闭I / O完成端口句柄。

(2)获取函数指针(以获取AcceptEx函数指针为例

INT  WSAAPI   WSAIoctl
SOCKET  S,
DWORD   dwIoControlCode,
LPVOID    lpvInBuffer,
DWORD   cbInBuffer,
LPVOID   lpvOutBuffer,
DWORD   cbOutBuffer,
LPDWORD   lpcbBytesReturned,
LPWSAOVERLAPPED   lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE   lpCompletionRoutine
);  

参数1:标识套接字的描述符,一个有效的SOCKET即可,该套接字的类型不会影响获取的的AcceptEx函数指针
参数2:执行的操作控制代码,使用 SIO_GET_EXTENSION_FUNCTION_POINTER参数来获取指针函数
参数3:指向输入缓冲区的指针
参数4:输入缓冲区的大小(以字节为单位)
参数5:指向输出缓冲区的指针
参数6:输出缓冲区的大小(以字节为单位)
参数7:指向实际输出字节数的指针
参数8:指向 WSAOVERLAPPED结构的指针(对于非重叠套接字,将被忽略)

返回值:成功完成后,  WSAIoctl返回零。否则,返回值SOCKET_ERROR,并且可以通过调用 WSAGetLastError检索来特定错误的代码

例:

LPFN_ACCEPTEX   m_lpfnAcceptEx;  // AcceptEx函数指针  LPFN_ACCEPTEX需要添加MSWSock.h头文件
GUID GuidAcceptEx = WSAID_ACCEPTEX;   // GUID,这个是识别AcceptEx函数必须的 WSAID_ACCEPTEX需要添加MSWSock.h头文件
DWORD dwBytes = 0;

WSAIoctl(
	m_pListenContext->m_Socket,
	SIO_GET_EXTENSION_FUNCTION_POINTER,
	&GuidAcceptEx,
	sizeof(GuidAcceptEx),
	&m_lpfnAcceptEx,
	sizeof(m_lpfnAcceptEx),
	&dwBytes,
	NULL,
	NULL);

然后,我们就可以通过其中的指针m_lpfnAcceptEx调用的的的的的AcceptEx函数了

(3)接受一个新的连接,返回本地和远程地址,并且接收由所述客户端应用程序发送的数据的第一个块

BOOL AcceptEx
SOCKET   sListenSocket,
SOCKET   sAcceptSocket,
PVOID   lpOutputBuffer,
DWORD   dwReceiveDataLength,
DWORD   dwLocalAddressLength,

DWORD   dwRemoteAddressLength,

LPDWORD   lpdwBytesReceived,

LPOVERLAPPED   lpOverlapped ); 

参数1:标识已使用函数调用的套接字的描述符服务器。
参数2:。标识要接受传入连接的套接字的套接字,这个就是那个需要我们事先建好的,等有客户端连接进来直接把,是的的的的AcceptEx高性能的关键所在
参数3:指向缓冲区的指针,这个缓冲区包含了三个信息,该缓冲区接收在新连接上发送,服务器的本地地址以及客户端的。接收数据从偏移零开始写入缓冲区,而地址写入缓冲区的后半部分
参数4: lpOutputBuffer用于中将缓冲区开头和结尾的实际接收数据的字节数。此大小不应包括服务器本地地址的大小,也不应包括客户端的远程地址 ; 就需要将该参数设成为: sizeof(lpOutputBuffer) - 2 *(sizeof sockaddr_in +16),也就是说总长度减去两个地址空间的长度就是了它们被附加到输出缓冲区。如果 dwReceiveDataLength为零,则AcceptEx将不会待数据到来,而直接返回。相反,一旦连接到达,  AcceptEx就会完成,而不会等待任何数据
参数5:存放本地址地址信息的空间大小,此值必须至少比使用的传输协议的最大地址长度长16个字节
参数6:存放远程地址信息的空间大小,此值必须至少比使用的传输协议的最大地址长度长16个字节不能为零。
参数7:指向DWORD的指针,该DWORD。接收接收的字节数仅当操作同步完成时才设置此参数。如果它返回ERROR_IO_PENDING并在稍后完成,则永远不会设置此DWORD,您必须获取从完成通知机制读取的字节数(通常不用管)
参数8:本次重叠I / O所要到了的重叠结构,它不能为NULL

返回值:如果未发生错误,则  AcceptEx函数成功完成,并返回值TRUE。如果函数失败,则  AcceptEx返回FALSE

(4)监控完成端口

作用:会让工作者线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要处理

BOOL   WINAPI   GetQueuedCompletionStatus(  
 __ in        HANDLE           CompletionPort,//这个就是我们建立的那个唯一的完成端口 

 __out     LPDWORD          lpNumberOfBytes,//这个是操作完成后返回的字节数  
__out      PULONG_PTR lpCompletionKey       ,//这个是我们建立完成端口的时候绑定的那个自定义结构体参数  
__out      LPOVERLAPPED     * lpOverlapped,//这个是我们在连入Socket的时候一起建立的那个重叠结构  
__in        DWORD           dwMilliseconds //等待完成端口的超时时间,如果线程不需要做其它的事情,那就无限就行了  
 ); 

参数1:完成端口的句柄
参数2:指向变量的指针,该变量接收已完成的I / O操作期间传输的字节数
参数3:指向变量的指针,该变量接收与I / O操作已完成的文件句柄关联的完成键值。完成密钥是在调用CreateIoCompletionPort时指定的每文件密钥
参数4:指向变量的指针,该变量接收在启动完成的I / O操作时指定的OVERLAPPED结构的地址。即使您已将函数传递给与完成端口关联的文件句柄状态从句:有效值的  OVERLAPPED,结构,应用程序也可以阻止完成端口通知这是通过为OVERLAPPED结构的hEvent成员指定有效事件句柄并设置其低位来完成的设置了低位的有效事件句使I / O不会完成排队到完成端口

参数5: 。调用者愿意等待完成数据包出现在完成端口的毫秒数如果在指定时间内未出现完成包,则该函数超时,返回FALSE,并将*  lpOverlapped的设置为NULL。如果dwMillisecondsINFINITE,则该函永远不会超时。如果dwMilliseconds为零并且没有出队的I / O操作,则该函数将立即超时

返回值:如果成功则返回非零(TRUE)否则返回零(FALSE)可以调用  GetLastError获得错误信息


(5)CONTAINING_RECORD宏 

作用:根据结构体类型和结构体中成员变量地址和名称则可求出该变量所在结构体的指针

PCHAR   CONTAINING_RECORD( 

IN  PCHAR  Address, 
IN  TYPE  Type, 
IN  PCHAR  Field  

);

参数1:指向类型类型结构实例中某域(成员)指针的
参数2:需要得到基地址的结构实例的结构类型名
参数3:类型类型结构包含的域(成员)的名称

返回值:包含场域(成员)的结构体的基地址

(6)解析的AcceptEx缓冲区内容的函数(扩展的函数)

void GetAcceptExSockaddrs
PVOID   lpOutputBuffer,
DWORD   dwReceiveDataLength,
DWORD   dwLocalAddressLength,
DWORD   dwRemoteAddressLength,
sockaddr ** LocalSockaddr,
LPINT   LocalSockaddrLength,
sockaddr ** RemoteSockaddr,
LPINT  RemoteSockaddrLength ); 

参数1:指向缓冲区的指针,该缓冲区接收由的AcceptEx调用产生的连接上发送的第。的AcceptEx函数的相同 lpOutputBuffer参数
参数2:用于接收第一个数据的缓冲区中的字节数此值必须。的的AcceptEx函数的 dwReceiveDataLength参数
参数3:为本地地址信息保留的字节数。的的AcceptEx函数的 dwLocalAddressLength参数
参数4:为远程地址信息保留的字节数等于传递。的的AcceptEx函数的 dwRemoteAddressLength参数
参数5:指向的sockaddr的结构的指针,该结构接收连接的本地地址,必须指定此参数
参数6:本地地址的大小(以字节为单位)必须指定此参数。
参数7:指向的sockaddr的结构的指针,该结构接收连接的远程地址,指定必须此参数
参数8:远程地址的大小(以字节为单位)。必须指定此参数

例:

PER_IO_CONTEXT* pIoContext = 本次通信用的I/O Context  
  
SOCKADDR_IN* ClientAddr = NULL;  
SOCKADDR_IN* LocalAddr = NULL;    
int remoteLen = sizeof(SOCKADDR_IN), localLen = sizeof(SOCKADDR_IN);    
  
m_lpfnGetAcceptExSockAddrs(pIoContext->m_wsaBuf.buf, pIoContext->m_wsaBuf.len - ((sizeof(SOCKADDR_IN)+16)*2),  sizeof(SOCKADDR_IN)+16, sizeof(SOCKADDR_IN)+16, (LPSOCKADDR*)&LocalAddr, &localLen, (LPSOCKADDR*)&ClientAddr, &remoteLen);

关闭完成端口:  从前面的章节中,我们已经了解到,工人线程一旦进入了则GetQueuedCompletionStatus时()的阶段,就会进入睡眠状态,无限的等待完成端口中,如果完成端口上一直都没有已经完成/ O请求,那么这些线程将无法被唤醒,这也意味着线程没法正常退出,如果在线程睡眠的时候,简单粗暴的就把线程关闭掉的,那是会一个很可怕的事情,因为很多线程体内很多资源都来不及释放掉,无论是这些资源最后是否会被,我们作为一个C ++程序员来讲,都不应该允许这样的事情出现。

(7)将I / O完成数据包发布到I / O完成端口

作用:手动的添加一个完成端口I / O操作,这样处于睡眠等待的状态的线程就会有一个被唤醒,如果为我们每一个工人线程都调用一次PostQueuedCompletionStatus()的话,那么所有的线程也就会因此而被唤醒了

BOOL  WINAPI   PostQueuedCompletionStatus
_In_  HANDLE   CompletionPort
_In_  DWORD    dwNumberOfBytesTransferred,
_In_  ULONG_PTR   dwCompletionKey,
_In_opt_  LPOVERLAPPED   lpOverlapped
);

参数1: I / O完成端口的句柄
参数2:通过GetQueuedCompletionStatus函数的lpNumberOfBytesTransferred参数返回的值
参数3:通过GetQueuedCompletionStatus函数的lpCompletionKey参数返回的值
参数4:通过GetQueuedCompletionStatus函数的lpOverlapped参数返回的值

返回值:如果函数成功,则返回值为非零如果函数失败,则返回值为零要获取扩展错误信息,请调用.GetLastError 函数

我们为了能够实现通知线程退出的效果,可以自己定义一些约定,比如把这后面三个参数设置一个特殊的值,然后工人线程接收到完成通知之后,通过判断这3个参数中是否出现了特殊的值,来决定是否是应该退出线程了

for (int i = 0; i < m_nThreads; i++)  
{  
      PostQueuedCompletionStatus(m_hIOCompletionPort, -1, 0, NULL);  
}  

为每一个线程都发送一个完成端口数据包,有几个线程就发送几遍,根据相关参数是否一致,如果一致那么工人线程就会知道,这是应用程序再向工作者线程发送的退出指令,这样工人线程在内部就可以自己很“优雅”的退出了。因为完成端口同样也是一个手柄,所以也得用CloseHandle的的将这个句柄关闭

(8)WSABUF结构体

作用:该  WSABUF结构使得能够通过一些的Winsock的功能中使用的数据缓存器的创建或操纵

typedef struct   _WSABUF { ULONG   len; CHAR *   buf; } WSABUF,* LPWSABUF;

 

参数1:缓冲区的长度,字节以单位为
参数2:指向缓冲区的指针

注意:

我们只是发送了m_nThreads次,我们如何能确保每一个工作者线程正好就收到一个,然后所有的线程都正好退出呢?是的,我们没有办法保证,所以很有可能一个工人线程处理完一个完成请求之后,发生了某些事情,结果又再次去循环接收下一个完成请求了,这样就会造成有的工作者线程没有办法接收到我们发出的退出通知。

        所以,我们在退出的时候,一定要确保工人线程只调用一次则GetQueuedCompletionStatus时(),这就需要我们自己想办法了,各位请参考我在工作者线程中实现的代码,我搭配了一个退出的事件,在退出的时候SetEvent的一下,来确保工人线程每次就只会调用一轮GetQueuedCompletionStatus时(),这样就应该比较安全了。

        另外,在VISTA / Win7的系统中,我们还有一个更简单的方式,我们可以直接CloseHandle的关掉完成端口的句柄,这样所有在GetQueuedCompletionStatus时()的线程都会被唤醒,并且返回FALSE,这时调用GetLastError函数()获取错误码时,会返回ERROR_INVALID_HANDLE,这样每一个工作者线程就可以通过这种方式轻松简单的知道自己该退出了。当然,如果我们不能保证我们的应用程序只在VISTA / Win7的中,那还是老老实实的PostQueuedCompletionStatus()吧

完成端口使用中的注意事项

       1. Socket的通信缓冲区设置成多大合适?

        在86的体系中,内存页面是以4KB为单位来锁定的,也就是说,就算是你投递的的WSARecv()的时候只用了1KB大小的缓冲区,系统还是得给你分4KB的内存。为了避免这种浪费,最好是把发送和接收数据的缓冲区直接设置成4KB的倍数。

       2.关于完成端口通知的次序问题

        这个不用想也能知道,调用GetQueuedCompletionStatus()获取I / O完成端口请求的时候,肯定是用先入先出的方式来进行的。

        但是,咱们大家可能都想不到的是,唤醒那些调用了则GetQueuedCompletionStatus时()的线程是以后入先出的方式来进行的。

        比如有4个线程在等待,如果出现了一个已经完成的I / O项,那么是最后一个调用GetQueuedCompletionStatus时()的线程会被唤醒。平常这个次序倒是不重要,但是在对数据包顺序有要求的时候,比如传送大块数据的时候,是需要注意下这个先后次序的。

        - 微软之所以这么做,那当然是有道理的,这样如果反复只有一个I / O操作而不是多个操作完成的话,内核就只需要唤醒同一个线程就可以了,而不需要轮着唤醒多个线程,节约了资源,而且可以把其他长时间睡眠的线程换出内存,提到资源利用率。

       3.如果各位想要传输文件......

        如果各位需要使用完成端口来传送文件的话,这里有个非常需要注意的地方。因为发送文件的做法,按照正常人的思路来讲,都会是先打开一个文件,然后不断的循环调用ReadFile的的()读取一块之后,然后再调用WSASend()去发发送。

        但是我们知道,ReadFile的的的的的()的时候,是需要操作系统通过磁盘的驱动程序,到实际的物理硬盘上去读取文件的,这就会使得操作系统从用户态转换到内核态去调用驱动程序,然后再把读取的结果返回至用户态;同样的道理,的的的的WSARecv()也会涉及到从用户态到内核态切换的问题---这样就使得我们不得不频繁的在用户态到内核态之间转换,效率低下......

        而一个非常好的解决方案是使用微软提供的扩展函数的的的的的TransmitFile()来传输文件,因为只需要传递给的的的的的TransmitFile()一个文件的句柄和需要传输的字节数,程序就会整个切换至内核态,无论是读取数据还是发送文件,都是直接在内核态中执行的,直到文件传输完毕才会返回至用户态给主进程发送通知。这样效率就高多了。

       4.关于重叠结构数据释放的问题

        我们既然使用的是异步通讯的方式,就得要习惯一点,就是我们投递出去的完成请求,不知道什么时候我们才能收到操作完成的通知,而在这段等待通知的时间,我们就得要千万注意得保证我们投递请求的时候所使用的变量在此期间都得是有效的。

        例如我们发送的的的的的WSARecv请求时候所使用的重叠变量,因为在操作完成的时候,这个结构里面会保存很多很重要的数据,对于设备驱动程序来讲,指示保存着我们这个重叠变量的指针,而在操作完成之后,驱动程序会将缓冲区的指针,已经传输的字节数,错误码等等信息都写入到我们传递给它的那个重叠指针中去。如果我们已经不小心把重叠的释放了,或者是又交给别的操作使用了的话,谁知道驱动程序会把这些东西写到哪里去呢?岂不是很崩溃......

猜你喜欢

转载自blog.csdn.net/SwordArcher/article/details/83269291