在接下来讨论"Overlapped I/O 事件通知模型"、"Overlapped I/O 完成例程模型"、"IOCP模型"之前,先来看一下Windows的Overlapped I/O,它实际上对应于"网络编程之 Socket的模式(二) --- Linux网络I/O模型"里的异步I/O(asynchronous I/O或者nonblocking I/O)。通过使用overlapped I/O,开发者可以要求操作系统主动传送数据,并且在传送完毕时通知开发者。这使得开发者的程序在I/O进行过程中,仍然可以处理其他事务。事实上,操作系统内部正是以线程来完成overlapped I/O的。
关于这一点,插上两句。我们自己在写程序的时候,一般很少会把需要循环读写的I/O操作放在主线程中,通常情况下会对此类I/O操作单独开线程处理,并通过消息队列来进行设计上的解耦,当然这需要一点点设计的技巧。如果需要操作的I/O数目很多,程序结构上设计不好的话,这通常会成为开发者的一个负担。而这正是Window的Overlapped I/O想解决的问题,在Windows的Overlapped I/O机制中,I/O的操作线程被封装在了操作系统内核中,API只暴露出有限的接口,Overlapped I/O的API并不需要用户对线程进行管理,用户对内部实现完全不可见。当然这对于想要盘根问底的程序员来说,并不是什么好事。何况在Window的异步I/O模型中,系统内部对I/O的处理在性能上有很大的优化。typedef struct _OVERLAPPED { DWORD Internal; // 通常被保留,当GetOverlappedResult()传回False并且GatLastError()并非传回ERROR_IO_PENDINO时,该状态置为系统定的状态。 DWORD InternalHigh; // 通常被保留,当GetOverlappedResult()传回False时,为被传输数据的长度。 DWORD Offset; // 指定文件的位置,从该位置传送数据,文件位置是相对文件开始处的字节偏移量。调用 ReadFile或WriteFile函数之前调用进 // 程设置这个成员,读写命名管道及通信设备时调用进程忽略这个成员; DWORD OffsetHigh; // 64位的文件偏移位置中,较高的32位,读写命名管道及通信设备时调用进程忽略这个成员(因为流式的I/O不支持文件位置); HANDLE hEvent; // 一个手动重置的event事件,当overlapped I/O完成时被激发。ReadFileEx()和WriteFileEx()会忽略这个栏位,此时这个地方 // 可能被传递一个用户自定义的指针 } OVERLAPPED, *LPOVERLAPPED;
BOOL GetOverlappedResult( HANDLE hFile, // 串口的句柄 LPOVERLAPPED lpOverlapped, // 指向重叠操作开始时指定的OVERLAPPED结构 LPDWORD lpNumberOfBytesTransferred, // 指向一个32位变量,该变量的值返回实际读写操作传输的字节数。 BOOL bWait // 该参数用于指定函数是否一直等到重叠操作结束。 // 如果该参数为TRUE,函数直到操作结束才返回。 // 如果该参数为FALSE,函数直接返回。 );返回值:
DWORD WINAPI WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); DWORD WINAPI WaitForMultipleObject( DWORD dwCount, CONST HANDLE* phObject, BOOL fWaitAll, DWORD dwMillisecinds);
typedef struct _WSAOVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; WSAEVENT hEvent; // 唯一需要关注的参数,用来关联WSAEvent对象 } WSAOVERLAPPED, *LPWSAOVERLAPPED;
BOOL WSAGetOverlappedResult( SOCKET s, // SOCKET LPWSAOVERLAPPED lpOverlapped, // 这里是想要查询结果的那个重叠结构的指针 LPDWORD lpcbTransfer, // 本次重叠操作的实际接收(或发送)的字节数 BOOL fWait, // 设置为TRUE,除非重叠操作完成,否则函数不会返回 // 设置FALSE,而且操作仍处于挂起状态,那么函数就会返回FALSE,错误为WSA_IO_INCOMPLETE LPDWORD lpdwFlags // 指向DWORD的指针,负责接收结果标志 );
DWORD WSAWaitForMultipleEvents( DWORD cEvents, // 等候事件的总数量 const WSAEVENT* lphEvents, // 事件数组的指针 BOOL fWaitAll, // 这个要多说两句: 如果设置为 TRUE,则事件数组中所有事件被传信的时候函数才会返回, // FALSE则任何一个事件被传信函数都要返回,我们这里肯定是要设置为FALSE的 DWORD dwTimeout, // 超时时间,如果超时,函数会返回WSA_WAIT_TIMEOUT,如果设置为0,函数会立即返回, // 如果设置为 WSA_INFINITE只有在某一个事件被传信后才会返回,在这里不建议设置为WSA_INFINITE BOOL fAlertable // 在完成例程中会用到这个参数,这里我们先设置为FALSE );返回值:
int WSARecv( SOCKET s, // 当然是投递这个操作的套接字 LPWSABUF lpBuffers, // 接收缓冲区,与Recv函数不同 // 这里需要一个由WSABUF结构构成的数组 DWORD dwBufferCount, // 数组中WSABUF结构的数量 LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回函数调用所接收到的字节数 LPDWORD lpFlags, //一个指向标志位的指针。 LPWSAOVERLAPPED lpOverlapped, // “绑定”的重叠结构 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成例程中将会用到的参数,我们这里设置为 NULL );返回值:
int WSASend ( SOCKET s, // s:标识一个已连接套接口的描述字。 LPWSABUF lpBuffers, // 一个指向WSABUF结构数组的指针。每个WSABUF结构包含缓冲区的指针和缓冲区的大小。 DWORD dwBufferCount, // lpBuffers数组中WSABUF结构的数目。 LPDWORD lpNumberOfBytesSent, // 如果发送操作立即完成,则为一个指向所发送数据字节数的指针。 DWORD dwFlags, // 标志位。 LPWSAOVERLAPPED lpOverlapped, // 指向WSAOVERLAPPED结构的指针(对于非重叠套接口则忽略)。 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 一个指向发送操作完成后调用的完成例程的指针。(对于非重叠套接口则忽略)。 );
#include <winsock2.h> #include <stdio.h> #pragma comment(lib,"ws2_32.lib") #define DATA_BUF_LEN 1024 // 接收缓冲区大小 SOCKET SLisent; SOCKET SWorker[DATA_BUF_LEN] = {0}; WSABUF RecvBuffer[DATA_BUF_LEN]; WSAOVERLAPPED Overlapped[DATA_BUF_LEN]; // 重叠结构 WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 用来通知重叠操作完成的事件句柄数组 DWORD dwRecvBytes = 0, // 接收到的字符长度 DWORD Flags = 0; // WSARecv的参数 DWORD volatile dwEventTotal = 0; // 程序中事件的总数 //由于EVENT数量限制,目前最多只能支持64个连接 DWORD WINAPI AcceptThread(LPVOID lpParameter) { WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData); // 使用Overlapped I/O模型必须设置WSA_FLAG_OVERLAPPED参数 SLisent = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,NULL,WSA_FLAG_OVERLAPPED); // 创建Socket SOCKADDR_IN ServerAddr; ServerAddr.sin_family = AF_INET; ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); ServerAddr.sin_port = htons(1234); // 监听 bind(SLisent,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr)); listen(SLisent,100); int i = 0; SOCKADDR_IN ClientAddr; int addr_length=sizeof(ClientAddr); while (TRUE) { while((SWorker[i] == 0) && (SWorker[i] = accept(SLisent,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET) { printf("accept %d ip:%s port:%dn",i+1,inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port); // 创建触发事件 EventArray[i] = WSACreateEvent(); dwEventTotal++; memset(&Overlapped[i],0,sizeof(WSAOVERLAPPED)); // 绑定overlapped i/o和事件 Overlapped[i].hEvent = EventArray[i]; char * buffer = new char[DATA_BUF_LEN]; memset(buffer,0,DATA_BUF_LEN); RecvBuffer[i].buf = buffer; RecvBuffer[i].len = DATA_BUF_LEN; if(WSARecv(SWorker[i], &RecvBuffer[i], dwEventTotal, &dwRecvBytes, &Flags, &Overlapped[i], NULL) == SOCKET_ERROR) { int err = WSAGetLastError(); if(WSAGetLastError() != WSA_IO_PENDING) { printf("disconnect: %dn",i+1); closesocket(SWorker[i]); SWorker[i] = 0; WSACloseEvent(EventArray[i]); // 关闭事件 RecvBuffer[i].buf = NULL; RecvBuffer[i].len = NULL; continue; } } i = (i+1)%WSA_MAXIMUM_WAIT_EVENTS; } } return FALSE; } DWORD WINAPI ReceiveThread(LPVOID lpParameter) { DWORD dwIndex = 0; while (true) { dwIndex = WSAWaitForMultipleEvents(dwEventTotal, EventArray, FALSE, 1000, FALSE); if (dwIndex == WSA_WAIT_FAILED || dwIndex == WSA_WAIT_TIMEOUT) continue; dwIndex = dwIndex - WSA_WAIT_EVENT_0; WSAResetEvent(EventArray[dwIndex]); DWORD dwBytesTransferred; WSAGetOverlappedResult( SWorker[dwIndex], &Overlapped[dwIndex], &dwBytesTransferred, FALSE, &Flags); if(dwBytesTransferred == 0) { printf("disconnect: %dn",dwIndex+1); closesocket(SWorker[dwIndex]); SWorker[dwIndex] = 0; WSACloseEvent(EventArray[dwIndex]); // 关闭事件 RecvBuffer[dwIndex].buf = NULL; RecvBuffer[dwIndex].len = NULL; continue; } //使用数据 printf("%sn",RecvBuffer[dwIndex].buf); memset(RecvBuffer[dwIndex].buf,0,DATA_BUF_LEN); if(WSARecv(SWorker[dwInde x], &RecvBuffer[dwIndex], dwEventTotal, &dwRecvBytes, &Flags, &Overlapped[dwIndex], NULL) == SOCKET_ERROR) { if(WSAGetLastError() != WSA_IO_PENDING) { printf("disconnect: %dn",dwIndex+1); closesocket(SWorker[dwIndex]); SWorker[dwIndex] = 0; WSACloseEvent(EventArray[dwIndex]); // 关闭事件 RecvBuffer[dwIndex].buf = NULL; RecvBuffer[dwIndex].len = NULL; continue; } } } return FALSE; } void main() { HANDLE hThreads[2]; hThreads[0] = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL); hThreads[1] = CreateThread(NULL, 0, ReceiveThread, NULL, NULL, NULL); WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); printf("exitn"); CloseHandle(hThreads[0]); CloseHandle(hThreads[1]); }
#include <winsock2.h> #include <stdio.h> #pragma comment(lib,"ws2_32.lib") #define DATA_BUF_LEN 1024 // 接收缓冲区大小 #define MAXSESSION 10000 // 最大连接数 typedef struct _SOCKET_INFORMATION { OVERLAPPED Overlapped; SOCKET Socket; WSABUF DataBuf; DWORD BytesSEND; DWORD BytesRECV; } SOCKET_INFORMATION, * LPSOCKET_INFORMATION; SOCKET ListenSocket = INVALID_SOCKET; DWORD Flags = 0; void CALLBACK WorkerRoutine(DWORD Error, DWORD BytesTransferred,LPWSAOVERLAPPED Overlapped, DWORD InFlags); DWORD WINAPI AcceptThread(LPVOID lpParameter) { WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData); // 创建Socket,设置异步I/O标志WSA_FLAG_OVERLAPPED ListenSocket = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,NULL,WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerAddr; ServerAddr.sin_family = AF_INET; ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); ServerAddr.sin_port = htons(1234); // 绑定 bind(ListenSocket,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr)); // 监听 listen(ListenSocket,MAXSESSION); printf("listenning.../n"); SOCKADDR_IN ClientAddr; int addr_length=sizeof(ClientAddr); while (TRUE) { LPSOCKET_INFORMATION SI = new SOCKET_INFORMATION; // 接受新连接 if ((SI->Socket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET) { printf("accept ip:%s port:%d/n",inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port); memset(&SI->Overlapped,0,sizeof(WSAOVERLAPPED)); SI->DataBuf.buf = new char[DATA_BUF_LEN]; SI->DataBuf.len = DATA_BUF_LEN; memset(SI->DataBuf.buf,0,DATA_BUF_LEN); // 设置回调函数 if(WSARecv(SI->Socket, &SI->DataBuf, 1, &SI->BytesRECV, &Flags, &SI->Overlapped, WorkerRoutine) == SOCKET_ERROR) { int err = WSAGetLastError(); if(WSAGetLastError() != WSA_IO_PENDING) { printf("disconnect/n"); closesocket(SI->Socket); delete [] SI->DataBuf.buf; delete SI; continue; } } } } return FALSE; } void CALLBACK WorkerRoutine(DWORD Error, DWORD BytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD InFlags) { LPSOCKET_INFORMATION SI = (LPSOCKET_INFORMATION)Overlapped; if (Error != 0 || BytesTransferred == 0) { printf("disconnect/n"); closesocket(SI->Socket); delete [] SI->DataBuf.buf; delete SI; return; } // 使用数据 printf("call back:%s/n",SI->DataBuf.buf); memset(SI->DataBuf.buf,0,DATA_BUF_LEN); // 设置回调函数 if(WSARecv(SI->Socket, &SI->DataBuf, 1, &SI->BytesRECV, &Flags, &SI->Overlapped, WorkerRoutine) == SOCKET_ERROR) { int err = WSAGetLastError(); if(WSAGetLastError() != WSA_IO_PENDING) { printf("disconnect/n"); closesocket(SI->Socket); delete [] SI->DataBuf.buf; delete SI; return; } } } void main() { HANDLE hThreads = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL); WaitForSingleObject(hThreads,INFINITE); printf("exit/n"); CloseHandle(hThreads); }
HANDLE CreateIoCompletionPort( HANDLE FileHandle, // 文件或设备(device)的handle。在Windows NT3.51之后,此栏位可设定为INVALID_HANDLE_VALUE,于是产生一个没有和 // 任何文件handle有关系的port HANDLE ExistingCompletionPort, // 如果此栏位被指定,那么上一栏位FileHandle就会被加到此port之上,而不会产生新的port。指定NULL可以产生一个新 // 的port ULONG_PTR CompletionKey, // 用户自定义的一个数值,将被交给提供服务的线程。此值和FileHandle有关联。 DWORD NumberOfConcurrentThreads // 与此I/O completion port有关联的线程个数。 );返回值:
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // CompletionPort参数对应于要在上面等待的完成端口 LPDWORD lpNumberOfBytes, // 参数负责在完成了一次I/O操作后(如WSASend或WSARecv),接收实际传输的字节数 PULONG_PTR lpCompletionKey, // 为原先传递进入CreateIoCompletionPort函数的套接字返回“单句柄数据”,最好将套接字句柄保存在这个“键”(Key)中。 LPOVERLAPPED* lpOverlapped, // 用于接收完成的I/O操作的重叠结果 DWORD dwMilliseconds // 用于指定调用者希望等待一个完成数据包在完成端口上出现的时间 );返回值:
BOOL PostQueuedCompletionStatus( HANDLE CompletionPort, // 指定想向其发送一个完成数据包的完成端口对象 DWORD dwNumberOfBytesTransferred, // 参数负责在完成了一次I/O操作后(如WSASend或WSARecv),接收实际传输的字节数。设置为0,完成端口上的工作线程被强制释放 ULONG_PTR dwCompletionKey, // 为原先传递进入CreateIoCompletionPort函数的套接字返回“单句柄数据”,最好将套接字句柄保存在这个“键”(Key)中。 LPOVERLAPPED lpOverlapped // 用于接收完成的I/O操作的重叠结果 );
对于完成端口的更多内容可以参考下面一些资料:
完成端口(CompletionPort)详解 - 手把手教你玩转网络编程系列之三
完成端口通讯服务器设计
Windows Socket 最大连接数