之前在WinSock2.0 API 中说到，像DisConnectEx 函数这样，它具有回收SOCKET的功能，而像AcceptEx这样的函数，它不会自己在内部创建新的SOCKET，需要外部传入SOCKET作为传输数据用的SOCEKT，使用这两个函数，我们可以做到，事先创建大量的SOCKET，然后使用AcceptEx函数从创建的SOCKET中选择一个作为连接用的SOCKET，在不用这个SOCKET的时候使用DisConnectEx回收。这样的功能就是一个SOCKET池的功能。SOCKET池WinSock 函数就是为了提升程序的性能而产生的，这些函数主要使用与TCP协议，我们可以在程序启动的时候创建大量的SOCKET句柄，在必要的时候直接使用AcceptEx这样的函数来使用已有的SOCKET作为与客户端的连接，在适当的时候使用WSARecv、WSASend等函数金星秀数据收发操作。而在不用SOCKET的时候使用DisConnectEx 回收，这样在响应用户请求的时候就省去了大量SOCKET创建和销毁的时间，从而提高的响应速度。IOCP本身也是一个线程池，如果用它结合WinSock 的线程池将会是Windows系统上最佳的性能组合，当然在此基础上可以考虑加入线程池、内存池的相关技术来进一步提高程序的性能。这里我想顺便扯点关于程序优化的理解。程序优化主要考虑对函数进行优化，毕竟在C/C++中函数是最常用，最基本的语法块，它的使用十分常见。函数的优化一般有下面几个需要考虑的部分是否需要大量调用这个函数。针对需要大量调用某个函数的情况，可以考虑对算法进行优化，减少函数的调用函数中是否有耗时的操作，如果有可以考虑使用异步的方式，或者将函数中的任务放到另外的线程（只有当我们确实不关心这个耗时操作的结果的时候，也就是说不涉及到同步的时候）函数中是否有大量的资源调用，如果有，可以考虑使用资源池的方式避免大量资源的申请与释放操作下面是一个使用SOCKET池的客户端的实例#include <stdio.h> #include <process.h> #include "MSScokFunc.h" #define SERVICE_IP "119.75.213.61" #define MAX_CONNECT_SOCKET 500 unsigned int WINAPI IOCPThreadProc(LPVOID lpParam); LONG g_nPorts = 0; CMSScokFunc g_MsSockFunc; typedef struct _tag_CLEINT_OVERLAPPED { OVERLAPPED overlapped; ULONG ulNetworkEvents; DWORD dwFlags; DWORD wConnectPort; DWORD dwTransBytes; char *pBuf; DWORD dwBufSize; SOCKET sConnect; }CLIENT_OVERLAPPED, *LPCLIENT_OVERLAPPED; SOCKADDR_IN g_LocalSockAddr = {AF_INET}; int main() { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SYSTEM_INFO si = {0}; const int on = 1; GetSystemInfo(&si); HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, si.dwNumberOfProcessors); HANDLE *pThreadArray = (HANDLE *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(HANDLE) * 2 * si.dwNumberOfProcessors); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, IOCPThreadProc, &hIOCP, 0, NULL); pThreadArray[i] = hThread; } g_MsSockFunc.LoadAllFunc(AF_INET, SOCK_STREAM, IPPROTO_IP); LPCLIENT_OVERLAPPED *pOverlappedArray = (LPCLIENT_OVERLAPPED *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(LPCLIENT_OVERLAPPED) * MAX_CONNECT_SOCKET); SOCKET *pSocketsArray = (SOCKET *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SOCKET) * MAX_CONNECT_SOCKET); int nIndex = 0; g_LocalSockAddr.sin_addr.s_addr = INADDR_ANY; g_LocalSockAddr.sin_port = htons(0); //让系统自动分配 printf("开始端口扫描........\n"); for (int i = 0; i < MAX_CONNECT_SOCKET; i++) { g_nPorts++; SOCKET sConnectSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, NULL, WSA_FLAG_OVERLAPPED); //允许地址重用 setsockopt(sConnectSock, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); bind(sConnectSock, (SOCKADDR*)&g_LocalSockAddr, sizeof(SOCKADDR_IN)); SOCKADDR_IN sockAddr = {0}; sockAddr.sin_addr.s_addr = inet_addr(SERVICE_IP); sockAddr.sin_family = AF_INET; sockAddr.sin_port = htons(g_nPorts); LPCLIENT_OVERLAPPED lpoc = (LPCLIENT_OVERLAPPED)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CLIENT_OVERLAPPED)); lpoc->ulNetworkEvents = FD_CONNECT; lpoc->wConnectPort = g_nPorts; lpoc->sConnect = sConnectSock; lpoc->pBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SOCKADDR_IN)); lpoc->dwBufSize = sizeof(SOCKADDR_IN); CopyMemory(lpoc->pBuf, &sockAddr, sizeof(SOCKADDR_IN)); if(!g_MsSockFunc.ConnectEx(sConnectSock, (SOCKADDR*)lpoc->pBuf, sizeof(SOCKADDR_IN), NULL, 0, &lpoc->dwTransBytes, &lpoc->overlapped)) { if (WSAGetLastError() != ERROR_IO_PENDING) { printf("第(%d)个socket调用ConnectEx失败, 错误码为:%08x\n", i, WSAGetLastError()); HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, lpoc); closesocket(sConnectSock); continue; } } CreateIoCompletionPort((HANDLE)sConnectSock, hIOCP, NULL, 0); pSocketsArray[nIndex] = sConnectSock; pOverlappedArray[nIndex] = lpoc; nIndex++; } g_nPorts = nIndex; WaitForMultipleObjects(2 * si.dwNumberOfProcessors, pThreadArray, TRUE, INFINITE); printf("端口扫描结束.......\n"); for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { CloseHandle(pThreadArray[i]); } HeapFree(GetProcessHeap(), 0, pThreadArray); printf("清理对应线程完成........\n"); CloseHandle(hIOCP); printf("清理完成端口句柄完成........\n"); for (int i = 0; i < nIndex; i++) { closesocket(pSocketsArray[i]); HeapFree(GetProcessHeap(), 0, pOverlappedArray[i]); } HeapFree(GetProcessHeap(), 0, pSocketsArray); HeapFree(GetProcessHeap(), 0, pOverlappedArray); printf("清理sockets池成功...............\n"); WSACleanup(); return 0; } unsigned int WINAPI IOCPThreadProc(LPVOID lpParam) { HANDLE hIOCP = *(HANDLE*)lpParam; LPOVERLAPPED lpoverlapped = NULL; LPCLIENT_OVERLAPPED lpoc = NULL; DWORD dwNumbersOfBytesTransfer = 0; ULONG uCompleteKey = 0; while (g_nPorts < 65536) //探测所有的65535个端口号 { int nRet = GetQueuedCompletionStatus(hIOCP, &dwNumbersOfBytesTransfer, &uCompleteKey, &lpoverlapped, INFINITE); lpoc = CONTAINING_RECORD(lpoverlapped, CLIENT_OVERLAPPED, overlapped); switch (lpoc->ulNetworkEvents) { case FD_CONNECT: { int nErrorCode = WSAGetLastError(); if (ERROR_SEM_TIMEOUT != nErrorCode) { printf("当前Ip端口[%d]处于开放状态\n", lpoc->wConnectPort); } lpoc->ulNetworkEvents = FD_CLOSE; shutdown(lpoc->sConnect, SD_BOTH); g_MsSockFunc.DisConnectEx(lpoc->sConnect, lpoverlapped, TF_REUSE_SOCKET, 0); } break; case FD_CLOSE: { InterlockedIncrement(&g_nPorts); //进行原子操作的自增1 lpoc->wConnectPort = g_nPorts; lpoc->ulNetworkEvents = FD_CONNECT; SOCKADDR_IN sockAddr = {0}; sockAddr.sin_addr.s_addr = inet_addr(SERVICE_IP); sockAddr.sin_family = AF_INET; sockAddr.sin_port = htons(g_nPorts); lpoc->pBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(SOCKADDR_IN)); lpoc->dwBufSize = sizeof(SOCKADDR_IN); CopyMemory(lpoc->pBuf, &sockAddr, sizeof(SOCKADDR_IN)); g_MsSockFunc.ConnectEx(lpoc->sConnect, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR), NULL, 0, &lpoc->dwTransBytes, &lpoc->overlapped); } break; default: { lpoc->ulNetworkEvents = FD_CLOSE; HeapFree(GetProcessHeap(), 0, lpoc->pBuf); lpoc->pBuf = NULL; lpoc->dwBufSize = 0; g_MsSockFunc.DisConnectEx(lpoc->sConnect, lpoverlapped, TF_REUSE_SOCKET, 0); } } } return 0; }这例子的主要功能是针对具体的IP或者主机名进行TCP的端口探测，这里的端口探测也是采用最简单的方式，向对应的端口发送TCP连接的请求，如果能连上则表示该端口开放，否则认为端口未开放。上述示例中，首先创建IOCP并绑定线程，这里我们绑定处理器数的2倍个线程，并且指定并行的线程数为CPU核数。接着创建对应的结构保存对应的连接信息。然后就是在循环中创建足够数量的SOCKET，这里我们只创建了500个，在每个SOCKET连接完成并回收后再次进行提交去探测后面的端口。注意这里我们先对每个SOCKET进行了绑定，这个在一般的SOCKET客户端服务器模型中没有这个操作，这个操作是WinSock API2.0需要的操作。 创建了足够的socket后，使用ConnectEx进行连接。在线程池中对相关的完成通知进行了处理，这里分了下面几种情况如果是连接成功了，表示这个端口是开放的，这个时候打印对应的端口并进行断开连接和回收SOCKET如果是断开连接的操作完成，再次进行提交如果是其他的通知我们认为是出错了，也就是这个端口没有开放，此时也是回收当前的SOCKET最后当所有端口都探测完成后完成端口线程退出，程序进入资源回收的阶段，这个阶段的顺序如下:关闭线程句柄关闭IOCP句柄关闭监听的SOCKET关闭其余套接字回收其他资源这个顺序也是有一定讲究的，我们先关闭IOCP的相关，如果后续还有需要处理的完成通知，由于此时IOCP已经关闭了，所以这里程序不再处理这些请求，接着关闭监听套接字表示程序已经不再接受连接的请求。这个时候程序已经与服务端彻底断开。后面再清理其余资源。WSABUF 参数在WSASend 和WSARecv的参数中总有一个WSABUF的参数，这个参数很简单的就只有一个缓冲区指针和缓冲区长度，加上函数后面表示WSABUF的个数的参数，很容易想到这些函数可以发送WSABUF的数组，从而可以发送多个数据，但是这就有问题了，发送大数据的话，我们直接申请大一点的缓冲就完了，为什么要额外的定义这么一个结构呢？回答这个问题的关键在于散播和聚集这种I/O处理的机制散播和聚集I/O是一种起源于高级硬盘I/O的技术，它的本质是将一组比较分散的小碎块数据组合成一个大块的IO数据操作，或者反过来是将一个大块的I/O操作拆分为几个小块的I/O操作。它的好处是,比如分散写入不同大小的几个小数据(各自是几个字节),这对于传统硬盘的写入来说是比较麻烦的一种操作,传统的磁盘需要经历几次机械臂的转动寻址,而通过聚集写操作,它会在驱动层将这些小块内存先拼装成一个大块内存,然后只调用一次写入操作,一次性写入硬盘。这样之后,就充分的发挥了高级硬盘系统(DMA/SCSI/RAID等)的连续写入读取的性能优势,而这些设备对于小块数据的读写是没有任何优势的,甚至性能是下降的。而在Winsock中将这种理念发挥到了SOCKET的传输上。WSABUF正是用于这个理念的产物。作为WSASend、WSASendto、WSARecv、WSARecvFrom等函数的数组参数,最终WSABUF数组可以描述多个分散的缓冲块用于收发。在发送的时候底层驱动会将多个WSABUF数据块组合成一个大块的内存缓冲，并一次发送出去，而在接收时会将收到的数据进行拆分，拆分成原来的小块数据，也就是说聚合散播的特性不仅能提高发送的效率，而且支持发送和接收结构化的数据。其实在使用聚合散播的时候主要是应用它来进行数据包的拆分，方便封装自定义协议。在一些应用中,每个数据包都是有自定义的结构的,这些结构就被称为自定义的协议。其中最常见的封装就是一个协议头用以说明包类型和长度,然后是包数据,最后是一个包尾里面存放用于校验数据的CRC码等。但是对于面向伪流的协议来说,这样的结构会带来一个比较头疼的问题——粘包,即多个小的数据包会被连在一起被接收端接收,然后就是头疼和麻烦的拆包过程。而如果使用了散播和聚集I/O方法,那么所有的工作就简单了,可以定义一个3元素的WSABUF结构数组分别发送包头/包数据/包尾。然后接收端先用一个WSABUF接收包头,然后根据包头指出的长度准备包数据/包尾的缓冲,再用2元素的WSABUF接收剩下的数据。同时对于使用了IOCP+重叠I/O的通讯应用来说,在复杂的多线程环境下散播和聚集I/O方法依然可以很可靠的工作。下面是一个使用聚合散播的服务器的例子:#include "MSScokFunc.h" #include <stdio.h> #include "MSScokFunc.h" #include <process.h> typedef struct _tag_CLIENT_OVERLAPPED { OVERLAPPED overlapped; char *pBuf; size_t dwBufSize; DWORD dwFlag; DWORD dwTransBytes; long lNetworkEvents; SOCKET sListen; SOCKET sClient; }CLIENT_OVERLAPPED, *LPCLIENT_OVERLAPPED; unsigned int WINAPI IOCPThreadProc(LPVOID lpParameter) { HANDLE hIOCP = *(HANDLE*)lpParameter; DWORD dwNumberOfTransfered; ULONG uKey = 0; LPOVERLAPPED lpOverlapped = NULL; LPCLIENT_OVERLAPPED lpoc = NULL; BOOL bLoop = TRUE; while (bLoop) { GetQueuedCompletionStatus(hIOCP, &dwNumberOfTransfered, &uKey, &lpOverlapped, INFINITE); lpoc = CONTAINING_RECORD(lpOverlapped, CLIENT_OVERLAPPED, overlapped); switch (lpoc->lNetworkEvents) { case FD_CLOSE: { if (lpoc->sListen == INVALID_SOCKET) { bLoop = FALSE; }else { //再次提交AcceptEx printf("线程(%08x)回收socket(%08x)成功\n", GetCurrentThreadId(), lpoc->sClient); lpoc->dwBufSize = 2 * (sizeof(SOCKADDR_IN) + 16); lpoc->lNetworkEvents = FD_ACCEPT; lpoc->pBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * (sizeof(SOCKADDR_IN) + 16)); g_MsSockFunc.AcceptEx(lpoc->sListen, lpoc->sClient, lpoc->pBuf, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &lpoc->dwTransBytes, &lpoc->overlapped); } } break; case FD_ACCEPT: { SOCKADDR_IN *pRemoteSockAddr = NULL; int nRemoteSockAddrLength = 0; SOCKADDR_IN *pLocalSockAddr = NULL; int nLocalSockAddrLength = 0; g_MsSockFunc.GetAcceptExSockAddrs(lpoc->pBuf, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, (LPSOCKADDR*)&pLocalSockAddr, &nRemoteSockAddrLength, (LPSOCKADDR*)&pRemoteSockAddr, &nRemoteSockAddrLength); printf("有客户端[%s:%d]连接进来，当前通信地址[%s:%d]......\n", inet_ntoa(pRemoteSockAddr->sin_addr), ntohs(pRemoteSockAddr->sin_port), inet_ntoa(pLocalSockAddr->sin_addr), ntohs(pLocalSockAddr->sin_port)); //设置当前通信用socket继承监听socket的相关属性 int nRet = ::setsockopt(lpoc->sClient, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char *)&lpoc->sListen, sizeof(SOCKET)); //投递WSARecv消息, 这里仅仅是为了发起WSARecv调用而不接受数据，设置缓冲为0可以节约内存 WSABUF buf = {0, NULL}; HeapFree(GetProcessHeap(), 0, lpoc->pBuf); lpoc->lNetworkEvents = FD_READ; lpoc->pBuf = NULL; lpoc->dwBufSize = 0; WSARecv(lpoc->sClient, &buf, 1, &lpoc->dwTransBytes, &lpoc->dwFlag, &lpoc->overlapped, NULL); } break; case FD_READ: { WSABUF buf[2] = {0}; DWORD dwBufLen = 0; buf[0].buf = (char*)&dwBufLen; buf[0].len = sizeof(DWORD); //当调用此处的时候已经完成了接收数据的操作，此时只要调用WSARecv将数据放入指定内存即可,这个时候不需要使用重叠IO操作了 int nRet = WSARecv(lpoc->sClient, buf, 1, &lpoc->dwTransBytes, &lpoc->dwFlag, NULL, NULL); DWORD dwBufSize = dwBufLen; buf[1].buf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufSize); buf[1].len = dwBufLen; WSARecv(lpoc->sClient, &buf[1], 1, &lpoc->dwTransBytes, &lpoc->dwFlag, NULL, NULL); printf("client>%s\n", buf[1].buf); lpoc->pBuf = buf[1].buf; //这块内存将在FD_WRITE事件中释放 lpoc->dwBufSize = buf[1].len; lpoc->lNetworkEvents = FD_WRITE; lpoc->dwFlag = 0; WSASend(lpoc->sClient, buf, 2, &lpoc->dwTransBytes, lpoc->dwFlag, &lpoc->overlapped, NULL); } break; case FD_WRITE: { printf("线程[%08x]完成事件(WSASend),缓冲(%d),长度(%d), 发送长度(%d)\n", GetCurrentThreadId(), lpoc->pBuf, lpoc->dwTransBytes, dwNumberOfTransfered); HeapFree(GetProcessHeap(), 0, lpoc->pBuf); lpoc->dwFlag = 0; lpoc->lNetworkEvents = FD_CLOSE; lpoc->pBuf = NULL; shutdown(lpoc->sClient, SD_BOTH); g_MsSockFunc.DisConnectEx(lpoc->sClient, &lpoc->overlapped, TF_REUSE_SOCKET, 0); } break; } } printf("线程[%08x] 退出.......\n", GetCurrentThreadId()); return 0; }这里没有展示main函数的内容，main函数的内容与之前的相似，在开始的时候进行一些初始化，在结束的时候进行资源的回收操作。这里需要注意的是在main函数中给定的退出条件:CLIENT_OVERLAPPED CloseOverlapped = {0}; CloseOverlapped.lNetworkEvents = FD_CLOSE; CloseOverlapped.sListen = INVALID_SOCKET; for (int i = 0; i < 2 * si.dwNumberOfProcessors; i++) { PostQueuedCompletionStatus(hIOCP, 0, NULL, (LPOVERLAPPED)&CloseOverlapped.overlapped); }在线程中，首先判断当前完成事件的类型如果是 FD_CLOSE 事件，首先判断socket是否为 INVALID_SOCKET，如果是则表明是主程序需要退出线程，此时退出，否则只进行回收与再提交的操作如果是 FD_ACCEPT 事件，则表明有客户端连接上来，此时解析客户端的信息并提交WSARecv的信息，注意这里在提交WSARecv时给的缓冲是NULL,这里表示我只需要一个完成的通知，在完成收到客户端数据后会触发FD_READ,而不需要进行数据的写入，一般在提交WSARecv后，系统内核会锁住我们提供的WSABUF结构所表示的那块内存直到，写入完成，而从我们提交WSARecv到真正获取到客户端传过来的数据，这个是需要一定时间的，而在这个时间中虽然CPU不会等待，但是这个内存被占用了，我们认为这也是一种浪费。特别是在服务端需要频繁的调用WSASend、WSARecv这样的操作，如果每一个都锁定一定的缓冲，这个内存消耗也是惊人的。所以这里传入NULL,只让其进行事件通知，而写入的操作由程序自己做。当事件是FD_READ时才正式进行数据的写入操作，此时再次调用WSARecv，这个时候需要注意，我们已经接收到了客户端的数据完成通知，也就是说现在明确的知道客户端已经发送了数据，而且内核已经收到数据，此时就不需要再使用异步了，只需要使用同步简单的读取数据即可。 在读取数据的时候首先根据WSABUF数组的第一个元素获取数据包的长度，然后分配对应的缓冲，接着接收后面真实的数据包。最后调用WSASend将数据原样返回。当完成通知事件是 FD_WRITE时表示我们已经完成了发送数据到客户端的操作，此时断开与客户端的连接并清理对应的缓冲。提高服务程序性能的一般方式对于实际的面向网络的服务(主要指使用TCP协议的服务应用)来说,大致可以分为两大类:连接密集型/传输密集型。连接密集型服务的主要设计目标就是以最大的性能响应尽可能多的客户端连接请求,比如一个Web服务传输密集型服务的设计目标就是针对每个已有连接做到尽可能大的数据传输量,有时以牺牲连接数为代价,比如一个FTP服务器。而针对像UDP这样无连接的协议，我们认为它主要负责传输数据，所以这里把它归结为传输密集型对于面向连接的服务(主要指使用TCP协议的服务)来说,主要设计考虑的是如下几个环节:接受连接数据传输IOCP+线程池接力其它性能优化考虑接收连接接收连接一般都采用AcceptEx的重叠IO方式来进行等待，并且一般需要加上SOCKET池的机制，开始时可能准备的AcceptEx数量可能会不足,此时可以另起线程在监听SOCKET句柄上等待FD_ACCEPT事件来决定何时再次投递大量的AcceptEx进行等待当然再次调用AcceptEx时需要创建大量的SOCKET句柄,这个工作最好不要在IOCP线程池线程中进行,以防创建过程耗时而造成现有SOCKET服务响应性能下降。最终需要注意的就是,任何处于"等待"AcceptEx状态的SOCKET句柄都不要直接调用closesocket,这样会造成严重的内核内存泄漏。应该先关闭监听套接字，防止在关闭SOCKET的时候有客户端连接进来，然后再调用closesocket来断开。数据传输在这个环节中可以将SOCKET句柄上的接收和发送数据缓冲区设置为0，这样可以节约系统内核的缓冲,尤其是在管理大量连接SOCKET句柄的服务中,因为一个句柄上这个缓冲大小约为17K,当SOCKET句柄数量巨大时这个缓冲耗费还是惊人的。设置缓冲为0的方法如下:int iBufLen= 0; setsockopt(skAccept,SOL_SOCKET,SO_SNDBUF,(const char*)&iBufLen,sizeof(int)); setsockopt(skAccept,SOL_SOCKET,SO_RCVBUF,(const char*)&iBufLen,sizeof(int));IOCP + 线程池为了性能的考虑,一般网络服务应用都使用IOCP+重叠I/O+SOCKET池的方式来实现具体的服务应用。这其中需要注意的一个问题就是IOCP线程池中的线程不要用于过于耗时或复杂的操作,比如:访问数据库操作,存取文件操作,复杂的数据计算操作等。这些操作因为会严重占用SOCKET操作的IOCP线程资源因此会极大降低服务应用的响应性能。但是很多实际的应用中确实需要在服务端进行这些操作,那么如何来平衡这个问题呢? 这时就需要另起线程或线程池来接力IOCP线程池的工作。比如在WSARecv的完成通知中,将接收到的缓冲直接传递给QueueUserWorkItem线程池方法,启动线程池中的线程去处理数据,而IOCP线程池则继续专心于网络服务。这也就是一般书上都会说的专门的线程干专门的事其他的性能考虑其他的性能主要是使用之前提到的聚合与散播机制，或者对函数和代码执行流程进行优化关于IOCP 聚合与散播的代码全放在码云上了: 示例代码
WinSock中提供的5种网络模型已经可以做到很高效了，特别是完成端口，它的高效的原因在于它不仅另外开启了线程来处理完成通知而不是占用主程序的时间，同时也在于我们在完成端口中运用了大量异步IO处理函数。比如WSASend、WSARecv等等。为了高效的处理网络IO，WinSock提供了大量这样的异步函数。这篇博文主要探讨这些函数的用法和他们与传统的巴克利套接字相比更加高效的秘密AcceptEx其实在使用TCP协议编程时，接受连接的过程也是需要进行收发包操作的，具体的过程请参考TCP的三次握手。针对这种特性WinSock提供了对应的异步操作函数AcceptEx。函数原型如下:BOOL AcceptEx( SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped );sListenSocket: 监听套接字sAcceptSocket：该参数是一个SOCKET的句柄，一旦连接成功建立，那么会使用该SOCKET作为通信的SOCKETlpOutputBuffer：是三个数据一体化的缓冲区的指针，这三个数据分别是接收连接时顺带接收客户端发过来的数据的缓冲，之后是本地地址结构的缓冲，最后是远程客户端地址结构的指针dwReceiveDataLength：是lpOutputBuffer的缓冲长度dwLocalAddressLength：是本地地址结构长度,其值等于sizeof(SOCKADDR)+16dwRemoteAddressLength：是远程客户端地址结构长度,其值也等于sizeof(SOCKADDR)+16lpdwBytesReceived：该参数用于返回接受连接请求时接收的数据的长度lpOverlapped：就是重叠I/O需要的结构第一个参数是一个十分重要的参数，这个参数是AcceptEx比较高效的一个重要的原因。从功能上来看它与传统的accept函数并没有什么区别，都是接受客户端连接的。它与accpet相比比较高效的原因如下：从内部机理来说accpet在内部其实有一个创建SOCKET的操作，当函数成功后会返回这个SOCKET，所以AcceptEx与accept相比少了一个创建SOCKET的操作，它的功能更加纯粹，这就给了我们一个启示：我们可以在初始化的时候创建大量的SOCKET，并投递到AcceptEx中，这样在接受连接时省去了创建SOCKET的时间，能够更快速的响应客户端的连接。由于AcceptEx不用创建SOCKET，所以它也将accept内部对socket设置的操作给省去了，也少了一些其他的附带操作，比如地址的解析，其实这里我们可以简单的理解为lpOutputBuffer中保存的信息就是TCP三次握手中的SYN包和ACK包，这些包的信息需要在函数返回后由用户通过其他方法来解析，而accpet帮我们解析了，所以AcceptEx比accept更加高效因为AcceptEx的设计目标纯粹就是为了性能,所以监听套接字的属性不会被代表客户端通讯的套接字自动继承。要继承属性(包括socket内部接受/发送缓存大小等等)就必须调用setsockopt使用选项SO_UPDATE_ACCEPT_CONTEXT,如下:int nRet = ::setsockopt(skClient, SOL_SOCKET ,SO_UPDATE_ACCEPT_CONTEXT ,(char *)&skListen, sizeof(skListen));AcceptEx函数除了能够接受客户端的连接之外，它也可以在接受连接的同时接收客户端随着连接请求一块发过来的数据，只要我们设置dwReceiveDataLength 参数大于0，并在lpOutputBuffer中分配相应的缓冲即可，但是这里会存在一个安全问题，当我们设置了这些之后，如果客户端只发送连接请求，但是不发送数据，AcceptEx会一直等待，如果有大量这样的客户端，那么可能会给服务器造成大量的资源浪费从而不能及时的服务其他正常客户端。要防止这样的情况，可以采用下列措施：设置dwReceiveDataLength为0，并且不分配对应的缓冲，也就是关闭这个接收数据的功能。启动一个监视线程对用于连接的SOCKET轮询调用：int iSecs; int iBytes = sizeof( int ); getsockopt( hAcceptSocket, SOL_SOCKET, SO_CONNECT_TIME, (char *)&iSecs, &iBytes ); //获取SOCKET连接时间iSecs 为 -1 表示还未建立连接, 否则就是已经连接的时间.当iSecs超过某个筏值时,就果断断开这个连接GetAcceptExSockAddr前面说AcceptEx不会对地址进行解析，而是返回一个经过编码的地址信息，可以将它理解为原始的三次握手包。而函数GetAcceptExSockAddr的主要作用就是通过原始的二进制数据得到对应的地址结构。函数原型如下:void GetAcceptExSockaddrs( PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPSOCKADDR* LocalSockaddr, LPINT LocalSockaddrLength, LPSOCKADDR* RemoteSockaddr, LPINT RemoteSockaddrLength);lpOutputBuffer:之前提供给AcceptEx函数的缓冲，如果AcceptEx调用成功，会在这个缓冲中写入地址信息，GetAcceptExSockaddrs通过这个缓冲中保存的地址信息来解析出地址结构dwReceiveDataLength：接收到的数据长度，注意这个长度不是lpOutputBuffer，而是客户端随着连接请求一起发送过来的其他数据的长度，其实这里应该理解为地址信息在缓冲中的偏移dwLocalAddressLength：本地地址信息的长度，这个长度为sizeof(SOCKADDR)+16dwRemoteAddressLength：远程客户端的地址信息的长度，长度为sizeof(SOCKADDR)+16LocalSockaddr：解析出来的本地地址结构LocalSockaddrLength:本地地址结构的长度，这个参数是一个输出参数RemoteSockaddr: 解析出来的远程客户端的地址结构RemoteSockaddrLength：解析出来的远程客户端的地址长度，这个参数是一个输出参数这里为什么要返回本地的地址结构呢，主要有两个原因：一般的服务器可能有多块网卡，返回本地地址我们就可以知道服务器用哪块网卡与客户端通信服务器用来监听的端口与用来进行通信的端口不是同一个，返回本地地址我们就能够知道服务器在使用哪个端口与客户端通信TransmitFile对于一些网络应用来说,发送文件有时是一个基本的功能,比如:web服务,FTP服务等。在Winsock中为此而专门提供了一个高效传输文件的API——TransmitFile。函数原型如下:BOOL TransmitFile( SOCKET hSocket, HANDLE hFile, DWORD nNumberOfBytesToWrite, DWORD nNumberOfBytesPerSend, LPOVERLAPPED lpOverlapped, LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, DWORD dwFlags);这个函数主要工作在TCP协议上hSocket：与客户端进行通信的SOCKEThFile：是对应文件的句柄nNumberOfBytesToWrite：表示发送文件的长度，这个长度可以小于文件长度nNumberOfBytesPerSend：当文件较大时，可以进行拆包发送，这个参数表示每个数据包的大小，如果这个参数为0，将采用系统默认的包大小，NT内核中默认大小为64KlpOverlapped：重叠I/O需要的结构lpTransmitBuffers：是一个TRANSMIT_FILE_BUFFERS结构体,利用它可以指定在文件开始发送前需要发送的额外数据以及文件发送结束后需要发送的额外数据,这个参数也可以置为NULL,仅表示发送文件数据。它的结构如下所示:typedef struct _TRANSMIT_FILE_BUFFERS { PVOID Head; DWORD HeadLength; PVOID Tail; DWORD TailLength; } TRANSMIT_FILE_BUFFERS;dwFlags：它是一个按位组合的标识。它的各个标识的含义如下标识含义TF_DISCONNECT在传输文件结束后,开始一个传输层断开动作TF_REUSE_SOCKET重置套接字,使其可以被AcceptEx等函数重用,这个标志需要与TF_DISCONNECT标志合用TF_USE_DEFAULT_WORKER指定发送文件使用系统默认线程,这对传输大型文件很有利TF_USE_SYSTEM_THREAD使用系统线程发送文件，它与TF_USE_DEFAULT_WORKER作用相同TF_USE_KERNEL_APC指定利用内核APC队列来代替工作线程来处理文件传输. 需要注意的是系统内核APC队列只在应用程序进入等待状态时才工作. 但不一定非要一个可警告状态的等待TF_WRITE_BEHIND指定TransmitFile函数尽可能立即返回,而不管远端是否确认已收到数据.这个标志不能与TF_DISCONNECT和TF_REUSE_SOCKET一起使用可以使用TF_DISCONNECT加上TF_REUSE_SOCKET 来回收SOCKET，以便像AcceptEx这样的函数可以重新利用。此时应该指定hFile为NULL，但这不是这个函数的主业(我觉得应该让专门的函数干专门的事，自己在封装函数的时候也应该要注意，不要向Win32 API这样使用各种标志来控制函数的功能)同时TransmitFile函数只有在服务器版Windows上才能发挥其全部功能。而在专业版或家庭版等Windows上它被限定为最多同时有两个调用在传输,而其他的调用都被置为排队等待状态。发送文件这个功能，是一个十分简单的功能，无非是应用层不断从磁盘文件中读取文件并使用WSASend这样的异步函数来发送，另一端不断用WSARecv接收并写入到文件中，为了性能在读写文件时也可以用IOCP的方式，那么为什么微软为了这么一个简单的功能还要独自封装一个函数，难道它封装的函数就一定比我们自己实现的性能高？上图揭示了TransmitFile能够高效工作的秘密，一般我们来封装这个功能的时候会调用ReadFile，此时由内核层读取文件并将文件文件内容保存在内核的内存空间中，然后通过系统调用们将内容拷贝到R3层，在调用WSASend的时候会将文件内容再从R3层拷贝到R0层，这个过程经过系统调用们，需要调用各种函数，并且进行各种验证。这个操作是十分耗时的。而TransmitFile则相对要高效的多，既然最终是要发送文件，那么它将内容从文件中读取出来后直接将R0层中保存的文件内容通过SOCKET发送出去，有的时候直接采用文件映射的方式将磁盘地址映射到网卡中，直接由网卡读取并发送，这样又省去了从内核中读取文件并拷贝到网卡缓存中的操作。所以它比我们自己封装来的更加高效。TransmitPackets有的时候需要发送超大型数据(有时是几十G)到客户端,有时甚至需要发送多个文件到客户端。这个时候TransmitFile就不再有效了。请注意TransmitFile的第三个参数 nNumberOfBytesToWrite 是一个DWORD类型，这也就标明这个函数最大只能发送4GB的文件，而对于更大的文件它就无能为力了，为了发送更大的文件WinSock专门封装了一个函数——TransmitPacketsBOOL PASCAL TransmitPackets( SOCKET hSocket, LPTRANSMIT_PACKETS_ELEMENT lpPacketArray, DWORD nElementCount, DWORD nSendSize, LPOVERLAPPED lpOverlapped, DWORD dwFlags );这个函数不但可以在面向连接(面向流)式的协议(TCP)上工作,还可以在无连接式的数据报协议(UDP)上工作,而TransmitFile函数只能工作在TCP上hSocket：表示发送所用的SOCKETlpPacketArray：它是一个结构体数组的指针，这个结构体表示发送文件的相关信息，结构体的定义如下:typedef struct _TRANSMIT_PACKETS_ELEMENT { ULONG dwElFlags; ULONG cLength; union { struct { LARGE_INTEGER nFileOffset; HANDLE hFile; }; PVOID pBuffer; }; } TRANSMIT_PACKETS_ELEMENT;这个结构体主要包含3个部分，第一个部分是一个标志，表示该如何解释后面的部分，这个标志有如下几个值标志含义TP_ELEMENT_FILE标明它将发送一个文件，此时会使用共用体中的结构体部分TP_ELEMENT_MEMORY标明它将要将发送内存中的一段空间的数据，此时会使用共用体中pBuffer部分TP_ELEMENT_EOP而最后一个标志用于辅助说明前两个标志,说明当前结构表示的数据应当作为一个结束包来发送,也就说之前所有的数据到当前这个结构描述的数据应当视为一个包第二部分是cLength用以说明当前结构描述的数据长度/发送文件内容的长度第三个部分联合定义根据第一个部分的实际标志值,用于说明是一个文件还是一个内存块,当是一个文件时还可以指定一个64位长整数型的文件内偏移,这为应用利用TransmitPackets发送大于4GB的文件创造了可能.当偏移为-1时,表示从文件当前指针位置开始发送需要注意的是因为TransmitPackets能够很快的处理数据发送,因此可能会造成大量待发送数据堆积在下层协议的协议栈上.而对于无连接的面向数据报的协议来说,有时协议驱动会选择将它们简单丢弃.另外对于TransmitPackets来说也只有服务器版的Windows能够发挥它全部的性能,而对于家庭版和专业版来说,最多能够同时处理两个TransmitPackets调用,其它的调用都会被排队处理最后TransmitPackets在发送文件时工作机理与TransmitFile是类似的,而TransmitPackets可以发送多个文件,并且可以发送超大文件(大于4GB),在发送内存块上,TransmitPackets也有很多优化,调用者可以放心的将超大的缓冲块传递给TransmitPackets而不必过多的担心ConnectEx作为客户端应用来说,或者说一些需要反连接工作的应用来说(如:Active FTP方式的服务器),使用传统的connect进行阻塞式或非阻塞式的编程都无法得到很好的性能响应它的定义如下:BOOL PASCAL ConnectEx( SOCKET s, const struct sockaddr* name, int namelen, PVOID lpSendBuffer, DWORD dwSendDataLength, LPDWORD lpdwBytesSent, LPOVERLAPPED lpOverlapped );s: 进行连接操作的SOCKET句柄,这个SOCKET句柄需要事先绑定，这里与调用普通的connect函数不同，它需要先调用bind函数将本地地址与SOCKET绑定name：要连接的远端服务器的地址结构namelen：就是远端地址结构的长度lpSendBuffer,dwSendDataLength,lpdwBytesSent三个参数共同用于描述在连接到服务器成功之后向服务器直接发送的数据缓冲,长度以及实际发送的数据长度lpOverlapped就是重叠I/O操作需要的结构体与AcceptEx类似，在连接成功后，需要调用 setsocketopt 来设置SOCKET的属性。与传统的connect函数不同,ConnectEx函数要求一个已经绑定过的SOCKET句柄参数,其实这也是将connect内部的绑定操作排除在真正connect操作之外的一种策略。最终连接的操作也会很快的就被完成,而绑定可以提前甚至在初始化的时候就完成。这样做也是为了能够快速的处理网络事件。DisConnectEx前面在TransmitFile中说它可以使用TF_DISCONNECT加上TF_REUSE_SOCKET 来回收SOCKET，也提到应该用专门的函数来干专门的事，这里ConnectEx就是专门的函数。它主要的作用与普通的closesocket函数类似。BOOL DisconnectEx( SOCKET hSocket, LPOVERLAPPED lpOverlapped, DWORD dwFlags, DWORD reserved );hSocket ：表示将要被回收的SOCKETlpOverlapped：重叠IO所使用的结构dwFlags：它是一个标志值，表示是否需要回收SOCKET，如果为0则表示不需要回收，此时它的作用与closesocket类似。如果为TF_REUSE_SOCKET表示将回收SOCKETreserved：是一个保留值直接传0即可当以重叠I/O的方式调用DisconnectEx时,若该SOCKET还有未完成的传输调用时,该函数会返回FALSE,并且最终错误码是WSA_IO_PENDING,即断开/回收操作将在传输完成后执行。如果使用了重叠IO，同样在完成之后会调用完成处理函数。如果未采用重叠IO操作，那么函数会阻塞，直到数据发送完成并断开连接。扩展函数的动态加载之前介绍的这一系列Winsock2.0的扩展API,最好都动态加载之后再行调用,因为它们具体的导出位置在不同平台上变动太大,如果静态联编的话,会给开发编译工作带来巨大的麻烦,所以使用运行时动态加载来调用这些API。但是这些函数的加载与加载普通的dll函数不同，为了方便操作，WinSock提供了一套完整的支持。这表示我们不需要知道它们所在的dll，我们可以直接使用WinSock提供的方法，即使以后它们所在的dll文件变化了，也不会影响我们的使用。加载它们需要使用到函数WSAIoctl,函数原型如下:int WSAIoctl( SOCKET s, DWORD dwIoControlCode, LPVOID lpvInBuffer, DWORD cbInBuffer, LPVOID lpvOutBuffer, DWORD cbOutBuffer, LPDWORD lpcbBytesReturned, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );这个函数的使用方法与ioctlsocket 相似。这里不对它的详细用法进行讨论。这里就简单的说说该怎么用它加载这些函数。要加载WinSock API，首先需要将第二个控制码参数设置为SIO_GET_EXTENSION_FUNCTION_POINTER，表示获取扩展API的指针。设置了这个参数后，lpvInBuffer参数需要设置成相应函数的GUID，下面列举了各个函数的GUID值GDUI函数WSAID_ACCEPTEXAcceptExWSAID_CONNECTEXConnectExWSAID_DISCONNECTEXDisconnectExWSAID_GETACCEPTEXSOCKADDRSGetAcceptExSockaddrsWSAID_TRANSMITFILETransmitFileWSAID_TRANSMITPACKETSTransmitPacketsWSAID_WSARECVMSGWSARecvMsgWSAID_WSASENDMSGWSASendMsg函数的指针通过 lpvOutBuffer 参数返回，而cbOutBuffer表示接受缓冲的长度，lpcbBytesReturned表示返回数据的长度。后面两个参数都与完成IO有关，所以这里可以直接给NULL。下面是一个加载AcceptEx函数的例子typedef BOOL (PASCAL FAR * LPFN_ACCEPTEX)( IN SOCKET sListenSocket, IN SOCKET sAcceptSocket, IN PVOID lpOutputBuffer, IN DWORD dwReceiveDataLength, IN DWORD dwLocalAddressLength, IN DWORD dwRemoteAddressLength, OUT LPDWORD lpdwBytesReceived, IN LPOVERLAPPED lpOverlapped ); LPFN_ACCEPTEX pFun = NULL; SOCKET sTemp = WSASocket(af, type, protocol, NULL, NULL, 0); GUID funGuid = WSAID_ACCEPTEX; if (INVALID_SOCKET != skTemp) { DWORD dwOutBufferSize = 0; int Ret = ::WSAIoctl(skTemp, SIO_GET_EXTENSION_FUNCTION_POINTER, &funGuid, sizeof(funGuid), &pFun, sizeof(pFun), &dwOutBufferSize, NULL, NULL); } 这里调用WSAIoctl加载扩展函数时需要传入SOCKET句柄，它其实是利用传入的SOCKET的相关信息来导出对应版本的扩展函数，比如这里我们传入的是一个用在TCP协议之上的SOCKET，所以它会返回一个使用TCP协议的API，利用这个SOCKET，这个函数以及它返回的API真正做到了与协议无关。
title: WinSock 重叠IO模型tags: [WinSock 模型, 网络编程, 重叠IO模型]date: 2018-06-29 20:26:13categories: Windows 网络编程keywords: WinSock 模型, 网络编程, 重叠IO模型之前介绍的WSAAsyncSelect和WSAEvent模型解决了收发数据的时机问题，但是网卡这种设备相比于CPU和内存来说仍然是慢速设备，而调用send和recv进行数据收发操作仍然是同步的操作，即使我们能够在恰当的时机调用对应的函数进行收发操作，但是仍然需要快速的CPU等待慢速的网卡。这样仍然存在等待的问题，这篇博文介绍的重叠IO模型将解决这个等待的问题之前介绍的WSAAsyncSelect和WSAEvent模型解决了收发数据的时机问题，但是网卡这种设备相比于CPU和内存来说仍然是慢速设备，而调用send和recv进行数据收发操作仍然是同步的操作，即使我们能够在恰当的时机调用对应的函数进行收发操作，但是仍然需要快速的CPU等待慢速的网卡。这样仍然存在等待的问题，这篇博文介绍的重叠IO模型将解决这个等待的问题重叠IO简介一般接触重叠IO最早是在读写磁盘时提出的一种异步操作模型，它主要思想是CPU只管发送读写的命令，而不用等待读写完成，CPU发送命令后接着去执行自己后面的命令，至于具体的读写操作由硬件的DMA来控制，当读写完成时会向CPU发送一个终端信号，此时CPU中断当前的工作转而去进行IO完成的处理。这是在磁盘操作中的一种高效工作的方式，为什么在网络中又拿出来说呢？仔细想想，前面的模型解决了接收数据的时机问题，现在摆在面前的就是如何高效的读写数据，与磁盘操作做类比，当接收到WSAAsyncSelect对应的消息或者WSAEvent返回时就是执行读写操作的时机，下面紧接着就是调用对应的读写函数来进行读写数据了，而联想到linux中的一切皆文件的思想，我们是不是可以认为操作网卡也是在操作文件？这也是在WinSock1中，使用WriteFile和ReadFile来进行网络数据读写的原因。既然它本质上也是CPU需要等待慢速的设备，那么为了效率它必定可以支持异步操作，也就可以使用重叠IO。创建重叠IO的socket要想使用重叠IO，就不能在像之前那样使用socket函数来创建SOCKET, 这函数最多只能创建一个普通SOCKET然后设置它为非阻塞(请注意非阻塞与异步的区别)。要创建异步的SOCKET需要使用WinSock2.0函数 WSASocketSOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags );该函数的前3个参数与socket的参数含义相同,第4个参数是一个协议的具体信息，配合WSAEnumProtocols 使用可以将枚举出来的网络协议信息传入，这样不通过前三个参数就可以创建一个针对具体协议的SOCKET。第5个参数目前不受支持简单的传入0即可。第6个参数是一个标志，如果要创建重叠IO的SOCKET，需要将这个参数设置为WSA_FLAG_OVERLAPPED。否则普通的SOCKET直接传入0即可使用重叠IO除了要将SOCKET设置为支持重叠IO外，还需要使用对应的支持重叠IO的函数，之前了解的巴克利套接字函数最多只能算是支持非阻塞而不支持异步。在WinSock1.0 中可以使用ReadFile和WriteFile来支持重叠IO，但是WinSock2.0 中重新设计的一套函数来支持重叠IOWSASend (send的等价函数)WSASendTo (sendto的等价函数)WSARecv (recv的等价函数)WSARecvFrom (recvfrom的等价函数)WSAIoctl (ioctlsocket的等价函数)WSARecvMsg (recv OOB版的等价函数)AcceptEx (accept 等价函数)ConnectEx (connect 等价函数)TransmitFile (专门用于高效发送文件的扩展API)TransmitPackets (专门用于高效发送大规模数据包的扩展API)DisconnectEx (扩展的断开连接的Winsock API)WSANSPIoctl (用于操作名字空间的重叠I/O版扩展控制API)那么如果使用上述函数但是传入一个非阻塞的SOCKET会怎么样呢，这些函数只看是否传入OVERLAPPED结构而不管SOCKET是否是阻塞的，一律按重叠IO的方式来运行。这也就是说,要使用重叠I/O方式来操作SOCKET,那么不一定非要一开初就创建一个重叠I/O方式的SOCKET对象(但是针对AcceptEx 来说如果传入的是普通的SOCKET，它会以阻塞的方式执行。当时测试时我传入的是使用WSASocket创建的SOCKET，我将函数的最后一个标志设置为0，发现AcceptEx只有当客户端连接时才会返回)重叠IO的通知模型与文件的重叠IO类似，重叠IO的第一种模型就是事件通知模型.利用该模型首先需要把一个event对象绑定到OVERLAPPED(WinSokc中一般是WSAOVERLAPPED)上，然后利用这个OVERLAPPED结构来进行IO操作.如:WSASend/WSARecv等判断对应IO操作的返回值，如果使用重叠IO模式，IO操作函数不会返回成功，而是会返回失败，使用WSAGetLastError得到的错误码为WSA_IO_PENDING,此时认为函数进行一种待决状态，也就是CPU将命令发送出去了，而任务没有最终完成然后CPU可以去做接下来的工作，而在需要操作结果的地方调用对应的等待函数来等待对应的事件对象。如果事件对象为有信号表示操作完成接着可以设置事件对象为无信号，然后继续投递IO操作.要等待这些事件句柄,可以调用WSAWaitForMultipleEvents函数,该函数原型如下:DWORD WSAWaitForMultipleEvents( __in DWORD cEvents, __in const WSAEVENT* lphEvents, __in BOOL fWaitAll, __in DWORD dwTimeout, __in BOOL fAlertable );第一个参数是事件对象的数目；第二个参数是事件对象的数组首地址；第三个参数是一个bool类型表示是否等待数组中所有的对象都变为有信号；第四个参数表示超时值；第五个参数是表示在等待的时候是否进入可警告状态在函数返回后我们只知道IO操作完成了，但是完成的结果是成功还是失败是不知道的，此时可以使用WSAGetOverlappedResult来确定IO操作执行的结果，该函数原型如下:BOOL WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags );第一个参数是对应的socket；第二个参数是对应的OVERLAPPED结构；第三个参数是一个输出参数，表示完成IO操作的字节数，通常出错的时候返回0；第四个参数指明调用者是否等待一个重叠I/O操作完成,通常在成功等待到事件句柄后,这个参数在这个模型中没有意义了；第五个参数是一个输出参数负责接收完成结果的标志。下面是一个事件通知模型的例子typedef struct _tag_CLIENTCONTENT { OVERLAPPED Overlapped; SOCKET sClient; WSABUF DataBuf; char szBuf[WSA_BUFFER_LENGHT]; WSAEVENT hEvent; }CLIENTCONTENT, *LPCLIENTCONTENT; int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); CLIENTCONTENT ClientContent[WSA_MAXIMUM_WAIT_EVENTS] = {0}; WSAEVENT Event[WSA_MAXIMUM_WAIT_EVENTS] = {0}; int nTotal = 0; SOCKET skServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerAddr = {0}; ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(SERVER_PORT); ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&ServerAddr, sizeof(SOCKADDR)); listen(skServer, 5); printf("开始监听...........\n"); Event[nTotal] = WSACreateEvent(); ClientContent[nTotal].hEvent = Event[nTotal]; ClientContent[nTotal].Overlapped.hEvent = Event[nTotal]; ClientContent[nTotal].DataBuf.len = WSA_BUFFER_LENGHT; ClientContent[nTotal].sClient = skServer; //针对监听套接字做特殊的处理 WSAEventSelect(skServer, Event[0], FD_ACCEPT | FD_CLOSE); nTotal++; while (TRUE) { DWORD dwTransfer = 0; DWORD dwFlags = 0; DWORD dwNumberOfBytesRecv = 0; int nIndex = WSAWaitForMultipleEvents(nTotal, Event, FALSE, WSA_INFINITE, FALSE); WSAResetEvent(Event[nIndex - WSA_WAIT_EVENT_0]); //监听socket返回 if (nIndex - WSA_WAIT_EVENT_0 == 0) { SOCKADDR_IN ClientAddr = {AF_INET}; int nClientAddrSize = sizeof(SOCKADDR); SOCKET skClient = WSAAccept(skServer, (SOCKADDR*)&ClientAddr, &nClientAddrSize, NULL, NULL); if (SOCKET_ERROR == skClient) { printf("接受客户端连接请求失败，错误码为:%08x\n", WSAGetLastError()); continue; } printf("有客户端连接进来[%s:%u]\n", inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port)); Event[nTotal] = WSACreateEvent(); ClientContent[nTotal].hEvent = Event[nTotal]; ClientContent[nTotal].Overlapped.hEvent = Event[nTotal]; ClientContent[nTotal].DataBuf.len = WSA_BUFFER_LENGHT; ClientContent[nTotal].DataBuf.buf = ClientContent[nTotal].szBuf; ClientContent[nTotal].sClient = skClient; //获取客户端发送数据,这是为了触发后面的等待 WSARecv(ClientContent[nTotal].sClient, &ClientContent[nTotal].DataBuf, 1, &dwNumberOfBytesRecv, &dwFlags, &ClientContent[nTotal].Overlapped, NULL); nTotal++; continue; }else { //等待发送完成 WSAGetOverlappedResult(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, &ClientContent[nIndex - WSA_WAIT_EVENT_0].Overlapped, &dwTransfer, TRUE, &dwFlags); if (dwTransfer == 0) { printf("接受数据失败:%08x\n", WSAGetLastError()); closesocket(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient); WSACloseEvent(ClientContent[nIndex - WSA_WAIT_EVENT_0].hEvent); for (int i = nIndex - WSA_WAIT_EVENT_0; i < nTotal; i++) { ClientContent[i] = ClientContent[i]; Event[i] = Event[i]; nTotal--; } } if (strcmp("exit", ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf.buf) == 0) { closesocket(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient); WSACloseEvent(ClientContent[nIndex - WSA_WAIT_EVENT_0].hEvent); for (int i = nIndex - WSA_WAIT_EVENT_0; i < nTotal; i++) { ClientContent[i] = ClientContent[i]; Event[i] = Event[i]; nTotal--; } continue; } send(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf.buf, dwTransfer, 0); WSARecv(ClientContent[nIndex - WSA_WAIT_EVENT_0].sClient, &ClientContent[nIndex - WSA_WAIT_EVENT_0].DataBuf, 1, &dwNumberOfBytesRecv, &dwFlags, &ClientContent[nIndex - WSA_WAIT_EVENT_0].Overlapped, NULL); } } WSACleanup(); return 0; }上述代码中定义了一个结构，方便我们根据事件对象获取一些重要信息。在main函数中首先完成了WinSock环境的初始化然后创建监听套接字，绑定，监听。然后定义一个事件对象让他与对应的WSAOVERLAPPED绑定,然后WSAEventSelect来投递监听SOCKET以便获取到客户端的连接请求（这里没有使用AcceptEx，因为它需要特殊的加载方式）接着在循环中首先调用WSAWaitForMultipleEvents等待所有信号，当函数返回时判断当前是否为监听套接字，如果是那么调用WSAAccept函数接收连接,并准备对应的事件和WSAOVERLAPPED结构，接着调用WSARecv接收客户端传入数据如果不是监听套接字则表明客户端发送数据过来，此时调用WSAGetOverlappedResult获取重叠IO执行的结果，如果成功则判断是否为exit，如果是exit关闭当前与客户端的链接，否则调用send函数原样返回数据接着调用WSARecv再次等待客户端传送数据。完成过程模型对于重叠I/O模型来说,前面的事件通知模型在资源的消耗上有时是惊人的。这主要是因为对于每个重叠I/O操作(WSASend/WSARecv等)来说,都必须额外创建一个Event对象。对于一个I/O密集型SOCKET应用来说,这种消耗会造成资源的严重浪费。由于Event对象是一个内核对象，它在应用层表现为一个4字节的句柄值，但是在内核中它对应的是一个具体的结构，而且所有的进程共享同一块内核的内存，因此某几个进程创建大量的内核对象的话，会影响整个系统的性能。为此重叠I/O又提供了一种称之为完成过程方式的模型。该模型不需要像前面那样提供对应的事件句柄。它需要为每个I/O操作提供一个完成之后回调处理的函数。完成历程的本质是一个历程它仍然是使用当前线程的环境。它主要向系统注册一些完成函数，当对应的IO操作完成时，系统会将函数放入到线程的APC队列，当线程陷入可警告状态时，它利用线程的环境来依次执行队列中的APC函数、要使用重叠I/O完成过程模型,那么也需要为每个I/O操作提供WSAOVERLAPPED结构体,只是此时不需要Event对象了。取而代之的是提供一个完成过程的函数完成历程的原型如下:void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);要使对应的完成函数能够执行需要在恰当的时机让对应线程进入可警告状态，一般的方式是调用SleepEx函数，还有就是调用Wait家族的相关Ex函数，但是如果使用Wait函数就需要使用一个内核对象进行等待，如果使用Event对象这样就与之前的事件通知模式有相同的资源消耗大的问问题了。此时我们可以考虑使用线程的句柄来进行等待，但是等待线程句柄时必须设置一个超时值而不能直接使用INFINIT了，因为等待线程就是要等到线程结束，而如果使用INFINIT，这样Wait函数永远不会返回，线程永远不会结束，此时就造成了死锁。下面是一个使用完成过程的模型typedef struct _tag_OVERLAPPED_COMPILE { WSAOVERLAPPED overlapped; LONG lNetworks; SOCKET sClient; WSABUF pszBuf; DWORD dwTransfer; DWORD dwFlags; DWORD dwNumberOfBytesRecv; DWORD dwNumberOfBytesSend; }OVERLAPPED_COMPILE, *LPOVERLAPPED_COMPILE; void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags); int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET skServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED); SOCKADDR_IN ServerClient = {0}; ServerClient.sin_family = AF_INET; ServerClient.sin_port = htons(SERVER_PORT); ServerClient.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&ServerClient, sizeof(SOCKADDR)); listen(skServer, 0); while (TRUE) { SOCKADDR_IN AddrClient = {0}; int AddrSize = sizeof(SOCKADDR); SOCKET skClient = WSAAccept(skServer, (SOCKADDR*)&AddrClient, &AddrSize, NULL, NULL); printf("有客户端[%s:%u]连接进来....\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port)); LPOVERLAPPED_COMPILE lpOc = new OVERLAPPED_COMPILE; ZeroMemory(lpOc, sizeof(OVERLAPPED_COMPILE)); lpOc->dwFlags = 0; lpOc->dwTransfer = 0; lpOc->lNetworks = FD_READ; lpOc->pszBuf.buf = new char[1024]; ZeroMemory(lpOc->pszBuf.buf, 1024); lpOc->pszBuf.len = 1024; lpOc->sClient = skClient; lpOc->dwNumberOfBytesRecv = 0; WSARecv(skClient, &(lpOc->pszBuf), 1, &(lpOc->dwNumberOfBytesRecv), &(lpOc->dwFlags), &(lpOc->overlapped), CompletionROUTINE); SleepEx(2000, TRUE); } WSACleanup(); return 0; } void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags) { LPOVERLAPPED_COMPILE lpOc = (LPOVERLAPPED_COMPILE)lpOverlapped; if (0 != dwError || 0 == cbTransferred) { printf("与客户端通信发生错误，错误码为:%08x\n", WSAGetLastError()); closesocket(lpOc->sClient); delete[] lpOc->pszBuf.buf; delete lpOc; return; } if (lpOc->lNetworks == FD_READ) { if (0 == strcmp(lpOc->pszBuf.buf, "exit")) { closesocket(lpOc->sClient); delete[] lpOc->pszBuf.buf; delete lpOc; return; } send(lpOc->sClient, lpOc->pszBuf.buf, cbTransferred, 0); lpOc->dwNumberOfBytesRecv = 0; ZeroMemory(lpOc->pszBuf.buf, 1024); lpOc->dwFlags = 0; lpOc->dwTransfer = 0; lpOc->lNetworks = FD_READ; WSARecv(lpOc->sClient, &(lpOc->pszBuf), 1, &(lpOc->dwNumberOfBytesRecv), &(lpOc->dwFlags), &(lpOc->overlapped), CompletionROUTINE); } }主函数的写法与之前的例子中的写法类似。也是先初始化环境，绑定，监听等等。在循环中接收连接，当有新客户端连接进来时创建对应的客户端结构，然后调用WSARecv函数接收数据，接下来就是使用SleepEx进入可警告状态，以便让完成历程有机会执行。在完成历程中就不需要像之前那样调用WSAGetOverlappedResult了，因为调用完成历程就一定意味着重叠IO操作已经完成了。在完成历程中根据第一个参数来判断IO操作执行是否成功。如果失败则会直接断开与客户端的连接然后清理对应的结构。如果成功则直接获取获取IO操作得到的数据，如果是exit则需要关闭连接，否则原样返回并准备下一次接收数据
在前面我们说了WSAAsyncSelect 模型，它相比于select模型来说提供了这样一种机制：当发生对应的IO通知时会立即通知操作系统，并调用对应的处理函数，它解决了调用send和 recv的时机问题，但是它有一个明显的缺点，就是它必须依赖窗口。对此WinSock 提供了另一种模型 WSAEventSelect模型简介该模型主要特色在于它使用事件句柄来完成SOCKET事件的通知。与WSAAsyncSelect 模型类似，它也允许使用事件对象来完成多个socket的完成通知。该模型首先在每个socket句柄上调用WSACreateEvent来创建一个WSAEvent对象句柄（早期的WSAEvent与传统的Event句柄有一定的区别，但是从WinSock2.0 以后二者是同一个东西）。接着调用WSAEventSelect将SOCKET句柄和WSAEvent对象绑定，最终通过WSAWaitForMultiEvents来等待WSAEvent变为有信号，然后再来处理对应的socketWSAEvent有两种工作模式和工作状态工作状态有有信号和无信号两种工作模式有手工重置和人工重置，手工重置指的是每当WSAWaitForMultiEvents或者WSAWaitForSingleEvents 返回之后，WSAEvent不会自动变为无信号，需要手工调用WSAResetEvent来将WSAEvent对象设置为无信号，而自动重置表示每次等待函数返回后会自动重置为无信号；调用WSACreateEvent创建的WSAEvent对象是需要手工重置的，如果想创建自动重置的WSAEvent对象可以调用CreateEvent函数来创建（由于WinSock2.0 之后二者没有任何区别，所以只需要调用CreateEvent并将返回值强转为WSAEvent即可）WSAEventSelect函数的原型如下:int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);其中s表示对应的SOCKET，hEventObject表示对应的WSAEvent对象，lNetworkEvents 表示我们需要处理哪些事件，它有一些对应的宏定义网络事件对应的含义FD_READ当前可以进行数据接收操作，此时可以调用像 recv, recvfrom, WSARecv, 或者 WSARecvFrom 这样的函数FD_WRITE此时可以发送数据，可以调用 send, sendto, WSASend, or WSASendToFD_ACCEPT可以调用accept (Windows Sockets) 或者 WSAAccept 除非返回的错误代码是WSATRY_AGAIN.FD_CONNECT表示当前可以连接远程服务器FD_CLOSE当前收到关闭的消息当WSAWaitForMultipleEvents返回时同时会返回一个序号，用于标识是数组中的哪个WSAEvent有信号，我们使用 index - WSA_WAIT_EVENT_0 来获取对应WSAEvent在数组中的下标，然后根据这个事件对象找到对应的SOCKET即可获得了对应的SOCKET以后，还需要获取到当前是哪个事件发生导致它变为有信号，我们可以调用WSAEnumNetworkEvents函数来获取对应发生的网络事件int WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );s就是要获取其具体事件通知的SOCKET句柄hEventObject就是对应的WSAEvent句柄,可以不传入,因为SOCKET句柄已经说明了要获取那个句柄上的通知,当然如果传入了,那么这个函数会对这个WSAEvent做一次重置,置为无信号的状态,相当于WSAResetEvent调用。此时我们就不需要调用WSAResetEvent函数了最后一个参数是一个结构，结构的定义如下:typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS;第一个数据是当前产生的网络事件。iErrorCode数组是对应每个网络事件可能发生的错误代码,对于每个事件错误代码其具体数组下标是预定义的一组FD_开头的串再加上一个_BIT结尾的宏,比如FD_READ事件对应的错误码下标是FD_READ_BIT下面的代码演示了处理接收(读取)数据的事件错误的例子代码if (NetworkEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { printf("FD_READ failed with error %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]); } }到目前为止，我们可以总结一下使用WSAEventSelect模型的步骤调用WSACreateEvent为每一个SOCKET创建一个等待对象，并与对应的SOCKET形成映射关系调用WSAEventSelect函数将SOCKET于WSAEvent对象进行绑定调用WSAWaitForMultipleEvents 函数对所有SOCKET句柄进行等待当WSAWaitForMultipleEvents 函数返回时利用返回的索引找到对应的WSAEvent对象和SOCKET对象调用WSAEnumNetworkEvents来获取对应的网络事件，根据网络事件来进行对应的收发操作重复3~5的步骤示例下面是一个简单的例子int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); SOCKADDR_IN AddrServer = {AF_INET}; AddrServer.sin_port = htons(SERVER_PORT); AddrServer.sin_addr.s_addr = htonl(INADDR_ANY); bind(skServer, (SOCKADDR*)&AddrServer, sizeof(SOCKADDR)); listen(skServer, 5); printf("服务端正在监听...........\n"); CWSAEvent WSAEvent; WSAEvent.InsertClient(skServer, FD_ACCEPT | FD_CLOSE); WSAEvent.EventLoop(); WSACleanup(); return 0; }在代码中定义了一个类CWSAEvent，该类封装了关于该模型的相关操作和对应事件对象和SOCKET对象的操作，在主函数中首先创建监听的SOCKET，然后绑定、监听,并提交监听SOCKET到类中，以便对它进行管理,函数InsertClient的定义如下:void CWSAEvent::InsertClient(SOCKET skClient, long lNetworkEvents) { m_socketArray[m_nTotalItem] = skClient; m_EventArray[m_nTotalItem] = WSACreateEvent(); WSAEventSelect(skClient, m_EventArray[m_nTotalItem++], lNetworkEvents); }这个函数中主要向事件数组和SOCKET数组的对应位置添加了相应的成员，然后调用WSAEventSelect。而类的EventLoop函数定义了一个循环来重复前面的3~5步,函数的部分代码如下:int CWSAEvent::WaitForAllClient() { DWORD dwRet = WSAWaitForMultipleEvents(m_nTotalItem, m_EventArray, FALSE, WSA_INFINITE, FALSE); WSAResetEvent(m_EventArray[dwRet - WSA_WAIT_EVENT_0]); return dwRet - WSA_WAIT_EVENT_0; } int CWSAEvent::EventLoop() { WSANETWORKEVENTS wne = {0}; while (TRUE) { int nRet = WaitForAllClient(); WSAEnumNetworkEvents(m_socketArray[nRet], m_EventArray[nRet], &wne); if (wne.lNetworkEvents & FD_ACCEPT) { if (0 != wne.iErrorCode[FD_ACCEPT_BIT]) { OnAcceptError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_ACCEPT_BIT]); }else { OnAcccept(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_CLOSE) { if (0 != wne.iErrorCode[FD_CLOSE_BIT]) { OnCloseError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_CLOSE_BIT]); }else { OnClose(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_READ) { if (0 != wne.iErrorCode[FD_READ_BIT]) { OnReadError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_READ_BIT]); }else { OnRead(nRet, m_socketArray[nRet]); } }else if (wne.lNetworkEvents & FD_WRITE) { if (0 != wne.iErrorCode[FD_WRITE_BIT]) { OnWriteError(nRet, m_socketArray[nRet], wne.iErrorCode[FD_WRITE_BIT]); }else { OnWrite(nRet, m_socketArray[nRet]); } } } }函数首先进行了等待，当等待函数返回时，获取对应的下标，以此来获取到socket和事件对象，然后调用WSAEnumNetworkEvents来获取对应的网络事件，最后根据事件调用不同的处理函数来处理在上面的代码中，这个循环有一个潜在的问题，我们来设想这么一个场景，当有多个客户端同时连接服务器，在第一次等待返回时，我们主要精力在进行该IO事件的处理，也就是响应这个客户端A的请求，而此时客户端A又发送了一个请求，而另外几个客户端B随后也发送了一个请求，在第一次处理完成后，等待得到的将又是客户端A，而后续客户端B的请求又被排到了后面，如果这个客户端A一直不停的发送请求，可能造成的问题是服务器一直响应A的请求，而对于B来说，它的请求迟迟得不到响应。为了避免这个问题，我们可以在函数WSAWaitForMultipleEvents 返回后，针对数组中的每个SOCKET循环调用WSAWaitForMultipleEvents将等待的数量设置为1，并将超时值设置为0，这个时候这个函数的作用就相当于查看数组中的每个SOCKET，看看是不是有待决的，当所有遍历完成后依次处理这些请求或者专门创建对应的线程来处理请求最后，整个示例代码
select 模型虽然可以管理多个socket，但是它涉及到一个时机的问题，select模型会针对所管理的数组中的每一个socket循环检测它管理是否在对应的数组中，从时间复杂度上来说它是O(n^2)的,而且还有可能发生数组中没有socket处于待决状态而导致本轮循环做无用功的情况，针对这些问题，winsock中有了新的模型——WSAAsyncSelect 消息模型消息模型的核心是基于Windows窗口消息获得网络事件的通知，Windows窗口是用来与用户交互的，而它并不知道用户什么时候会操作窗口，所以Windows窗口本身就是基于消息的异步通知，网络事件本身也是一个通知消息，将二者结合起来可以很好的使socket通知像消息那样当触发通知时调用窗口过程。这样就解决了select中的时机问题和里面两层循环的问题WSAAsyncSelect函数原型如下:int WSAAsyncSelect( __in SOCKET s, __in HWND hWnd, __in unsigned int wMsg, __in long lEvent );第一个参数是绑定的socket，第二个参数是消息所对应的窗口句柄，第三个参数是对应的消息，这个消息需要自己定义，第4个参数是我们所关心的事件，当在s这个socket发生lEvent这个事件发生时会向hWnd对应的窗口发送wMsg消息。在消息附带的两个参数wParam和lParam中，lParam的高位16位表示当前的错误码，低16位表示当前socket上发生的事件。其中事件的取值如下:FD_WRITE : 当socket上可写时触发该事件，FD_WRITE的触发与调用send没有必然的联系，FD_WRITE只是表示socket已经为发送准备好了必要的条件，其实调用时可以不必理会这个事件，只需要在想发送数据的场合调用send，一般来说FD_WRITE只在这些条件下触发：a) 调用connect函数成功连接到服务器 b) 调用accept接受连接成功后（该条件是绑定在accept返回的那个与客户端通讯的socket上) c）调用send，sendto 失败并返回WSAWOULDBLOCK(由于是异步操作，可能同时客户端也在发数据, 此时可能导致send失败)为了方便我们处理这些参数，WinSock 提供了两个宏来解析它的高16位和低16位，分别是WSAGETSELECTERROR和WSAGETSELECTEVENT而lParam则保存了当前触发事件的socket句柄如果对一个句柄调用了WSAAsyncSelect 并成功后，对应的socket会自动编程非阻塞模式。它就不像前面的select模型那样需要显示调用ioctrlsocket将socekt设置为非阻塞。另外不需要每个socket都定义一个消息ID，通常一个ID已经足够处理所有的socket事件。下面是一个具体的例子int _tmain(int argc, TCHAR *argv[]) { WSADATA wd = {0}; WSAStartup(MAKEWORD(2, 2), &wd); SOCKADDR_IN SrvAddr = {AF_INET}; SrvAddr.sin_addr.s_addr = htonl(INADDR_ANY); SrvAddr.sin_port = htons(SERVER_PORT); SOCKET skServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == skServer) { printf("初始化socket失败，错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } if (0 != bind(skServer, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR))) { printf("绑定失败，错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } if (0 != listen(skServer, 5)) { printf("监听失败，错误码为:%08x\n", WSAGetLastError()); goto __CLEAR_UP; } RegisterWindow(); CreateAndShowWnd(); g_uSockMsgID = RegisterWindowMessage(SOCKNOTIFY_MESSAGE); WSAAsyncSelect(skServer, g_hMainWnd, g_uSockMsgID, FD_ACCEPT | FD_CLOSE); MessageLoop(); __CLEAR_UP: if (INVALID_SOCKET != skServer) { closesocket(skServer); } WSACleanup(); return 0; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LRESULT lRes = 0; switch (uMsg) { case WM_CLOSE: { CloseWindow(hwnd); DestroyWindow(hwnd); } break; case WM_PAINT: { PAINTSTRUCT ps = {0}; BeginPaint(hwnd, &ps); EndPaint(hwnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; default: if (uMsg == g_uSockMsgID) { lRes = ParseNotifyMessage(wParam, lParam); } lRes = DefWindowProc(hwnd, uMsg, wParam, lParam); } return lRes; } LRESULT ParseNotifyMessage(WPARAM wParam, LPARAM lParam) { WORD wNotify = WSAGETSELECTEVENT(lParam); WORD wError = WSAGETSELECTERROR(lParam); if (wNotify == FD_ACCEPT) { return OnAcceptMsg((SOCKET)wParam, lParam); }else if (wNotify == FD_READ) { return OnReadMsg((SOCKET)wParam, lParam); } return 1; } LRESULT OnAcceptMsg(SOCKET s, LPARAM lParam) { SOCKADDR_IN AddrClient = {0}; int nAddrSize = sizeof(SOCKADDR); SOCKET sClient = accept(s, (SOCKADDR*)&AddrClient, &nAddrSize); printf("有客户端连接进来[%s:%u]\n", inet_ntoa(AddrClient.sin_addr), ntohs(AddrClient.sin_port)); return WSAAsyncSelect(sClient, g_hMainWnd, g_uSockMsgID, FD_WRITE | FD_READ | FD_CLOSE); } LRESULT OnReadMsg(SOCKET s, LPARAM lParam) { char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024); ZeroMemory(pszBuf, 1024); int nTotalSize = 0; int i = 1; while (TRUE) { i++; int nReadSize = recv(s, pszBuf + nTotalSize, 1024, 0); if (nReadSize < 1024) { nTotalSize += nReadSize; break; } nTotalSize += nReadSize; HeapReAlloc(GetProcessHeap(), 0, pszBuf, 1024 * i); } if (strcmp(pszBuf, "exit") == 0) { shutdown(s, SD_BOTH); closesocket(s); } send(s, pszBuf, nTotalSize, 0); HeapFree(GetProcessHeap(), 0, pszBuf); return 0; } 在上面的代码中我们在main函数中创建了窗口程序，而常规的都是在WinMain中创建，其实从本质上讲控制台程序和窗口程序都是一个进程，至于以main作为入口还是以WinMain作为入口只是习惯上这样，但是并没有硬性规定。 在创建窗口之后我们将监听socket也绑定到窗口消息中，然后在对应的消息中判断FD_ACCEPT事件，如果是则调用accept进行连接。并将对生成的socket进行绑定。 在接下来的socket消息中主要处理FD_READ事件，当发生READ事件时调用read接收数据，然后调用send将数据原封不动的发送出去。 从上面的代码上看，该模型相对于select来说省去了查看socket是否在对应数组中的操作，减少了循环。而且可以很好的把握什么调用时机问题。 主要的缺点是它需要一个窗口，这样在服务程序中基本就排除掉了这个模型，它基本上只会出现在客户端程序中。 另外如果在一个窗口中需要管理成千上万个句柄时，它的性能会急剧下降，因此它的伸缩性较差。但是在客户端中基本不存在这个问题，所以如果要在客户端中想要减少编程难度，它是一个不二的选择