本章的IOCP(Input Output CompletionPort,输入输出完成端口)服务器端模型是很多Windows程序员关注的焦点。各位若急于求成而跳过了第21章的内容,建议大家最好回顾一下。因为第21章和第22章介绍了本章的背景知识,而且,关于IOCP的内容实际上是从第22章开始的。
第22章中只介绍了执行重叠I/O的Sender和Receiver,但还未利用该模型实现过服务器端。因此,我们先利用重叠I/O模型实现回声服务器喘。首先介绍创建非阻塞模式套接字的方法。我们曾在第17章创建过非阻塞模式的套接字,与之类似,在Windows中通过如下函数调用将套接字属性改为非阻塞模式。
SOCKET hLisnSock;
int mode=1;
.....
hListSock=WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);//创建重叠IO
ioctlsocket(hLisnSock,FIONBIO,&mode);//设定套接字为非阻塞属性
.....
上述代码中调用的ioctlsocket函数负责控制套接字I/O方式,其调用具有如下含义:“将hLisnSock句柄引用的套接字I/O模式(FIONBIO)改为变量mode中指定的形式。”
也就是说,FIONBIO是用于更改套接字I/O模式的选项,该函数的第三个参数中传入的变量中若存有0,则说明套接字是阻塞模式的;如果存有非0值,则说明已将套接字模式改为非阻塞模式。改为非阻塞模式后,除了以非阻塞模式进行I/O外,还具有如下特点。
□如果在没有客户端连接请求的状态下调用accept函数,将直接返回INVALID_SOCKET。调用WSAGetLastError函数时返回WSAEWOULDBLOCK。
□调用accept函数时创建的套接字同样具有非阻塞属性。
因此,针对非阻塞套接字调用accept函数并返回INVALID_SOCKET时,应该通WSAGetLastError
函数确认返回INVALID_SOCKET的理由,再进行适当处理。
要想实现基于重叠I/O的服务器端,必须具备非阻塞套接字,所以先介绍了其创建方法。实
际上,因为有IOCP模型,所以很少有人只用重叠I/O实现服务器端。但我认为:“为了正确理解IOCP,应当尝试用纯重叠I/O方式实现服务器端。”
即使坚持不用IOCP,也应具备仅用重叠I/O方式实现类似IOCP的服务器端的能力。这样就可以在其他操作系统平台实现类似IOCP方式的服务器端,而且不会因IOCP的限制而忽略服务器端
功能的实现。
下面用纯重叠I/O模型实现回声服务器端,由于代码量较大,我们分3个部分学习。
#include
#include
#include
#define BUF_SIZE 1024
void CALLBACK ReadCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteCompRoutine(DWORD,DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);
typedef struct{
SOCKET hClntSock;
char buf[BUF_SIZE];
WSABUF wsaBuf;
} PER_IO_DATA, *LPPER IO_DATA;
该结构体中的信息足够进行数据交换,下列代码将介绍该结构体的填充及使用方法。
int main(int argc, char* argv[]){
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
LPWSAOVERLAPPED lpOvLp;
DWORD recvBytes;
LPPER_IO_DATA hbInfo;
int mode=1, recvAdrsz, flagInfo=0;
if(argc!=2) {
printf("Usage: %s \n",argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hLisnSock=WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
ioctlsocket(hLisnSock, FIONBIO, &mode);
memset(&lisnAdr,0, sizeof(lisnAdr));
lisnAdr.sin_family=AF_INET;
lisnAdr.sin_addr.s_addr=htonl(INADDR_ANY);
lisnAdr.sin_port=htons(atoi(argv[1]));
if(bind(hLisnSock,(SOCKADDR*)&lisnAdr, sizeof(lisnAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hLisnSock,5)==SOCKET_ERROR)
ErrorHandling("listen() error");
recvAdrSz=sizeof(recvAdr);
while(1){
SleepEx(100,TRUE);
hRecvSock=accept(hLisnsock,(SOCKADDR*)&recvAdr,&recvAdrSz);
if(hRecvSock==INVALID_SOCKET){
if(WSAGetLastError()==WSAEWOULDBLOCK)continue;
else ErrorHandling("accept() error");
}
puts("Client connected.....");
lpOvLp=(LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
memset(lpOvLp, 0, sizeof(WSAOVERLAPPED));
hbInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
hbInfo->hClntSock=(DWORD)hRecvSock;
(hbInfo->wsaBuf).buf=hbInfo->buf;
(hbInfo->wsaBuf).len=BUF_SIZE;
lpOvLp->hEvent=(HANDLE)hbInfo;
WSARecv(hRecvSock,&(hbInfo->wsaBuf),1,&recvBytes, &flagInfo, lpOvLP,
ReadcompRoutine);
}
closesocket(hRecvSock);
closesocket(hLisnSock);
WSACleanup();
return 0;
}
有几点需要注意:
第46、47行:申请重叠I/O中需要使用的结构体变量的内存空间并初始化。之所以在循环
内部申请WSAOVERLAPPED结构体空间,是因为每个客户端都需要独立的WSAOVERLAPPED结构体变量。
第54行:WSAOVERLAPPED结构体变量的hEvent成员中将写入第49行分配过空间的变量
地址值。基于Completion Routine函数的重叠I/O中不需要事件对象,因此,hEvent中可以写入其他信息。
第55行:调用WSARecv函数时将ReadCompRoutine函数指定为Completion Routine。其
中第六个参数WSAOVERLAPPED结构体变量地址值将传递到Completion Routine的第三个参数,因此,Completion Routine函数内可以访问完成I/O的套接字句柄和缓冲。另外,为了运行Completion Routine函数,第35行循环调用SleepEx函数。
最后介绍两个Completion Routine函数。实际的回声服务是通过这两个函数完成的。
void CALLBACK ReadCompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED ipOverlapped, DWORD flags){
LPPER_IO_DATA hbInfo=(LPPER_IO_DATA)(1pOverlapped->hEvent);
hSock=hbInfo->hClntsock;
LPWSABUF bufInfo=&(hbInfo->wsaBuf);
DWORD sentBytes;
if(szRecvBytes==0){//如果接收到了EOF那么则关闭套接字以及释放对应的空间
closesocket(hSock);
free(lpoverlapped->hEvent);
free(lpoverlapped);
puts("Client disconnected.....");
}
else{//如果不为零那么一定有需要回声的内容
bufInfo->len=szRecvBytes;
WSASend(hSock,bufInfo,1, &sentBytes, 0, lpoverlapped, WriteCompRoutine);
}
}
void CALLBACK WriteCompRoutine(DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpoverlapped, DWORD flags){
LPPER_IO_DATA hbInfO=(LPPER_IO_DATA)(lpOverlapped->hEvent);
SOCKET hSock=hbInfo->hClntSock;
LPWSABUF bufInfo=&(hbInfo->wsaBuf);
DWORD recvBytes;
int flagInfo=0;
WSARecv(hSock,bufInfo,1,&recvBytes,&flagInfo, lpoverlapped, ReadCompRoutine);
//发送后默认等待再次回声
}
void ErrorHandling(char *message){
fputs(message, stderr);
fputc('\n',stderr);
exit(1);
}
有几个注意点:
第4~6行:提取完成输入的套接字句柄和缓冲信息,因为WSAOVERLAPPED结构体变量
的hEvent成员中保存了PER_IO_DATA结构体变量地址值。
第9行:变量szRecvBytes的值为0就意味着收到了EOF,因此需要进行相应处理。
第17、18行:将WriteCompRoutine函数指定为Completion Routine,同时调用WSASend
函数。程序通过该语句向客户端发送回声消息。
第22、30行:发送回声消息后调用该函数。但需要再次接收数据,所以执行第30行的函数
上述示例的工作原理整理如下。
□有新的客户端连接时调用WSARecv函数,并以非阻塞模式接收数据,接收完成后调用
ReadCompRoutine函数。
□调用ReadCompRoutine函数后调用WSASend函数,并以非阻塞模式发送数据,发送完成后
调用WriteCompRoutine函数。
□此时调用的WriteCompRoutine雨数将再次调用WSARecv函数,并以非阻塞模式等待接收
数据。
通过交替调用ReadCompRoutine函数和WriteCompRoutine函数,反复执行数据的接收和发送操作。另外,每次增加1个客户端都会定义PERIO_DATA结构体,以便将新创建的套接字句柄和缓冲信息传递给ReadCompRoutine函数和WriteCompRoutine函数。同时将该结构体地址值写人WSAOVERLAPPED结构体成员hEvent,并传递给Completion Routine函数。这非常重要,可概括如下:
“使用WSAOVERLAPPED结构体成员hEvent向完成I/O时自动调用的Completion Routine函数内部传递客户端信息(套接字和缓冲)。"
接下来需要验证运行结果,先要编写回声客户端,因为使用第4章的回声客户端会无法得到预想的结果。
其实第4章实现并使用至今的回声客户端存在一些问题,关于这些问题及解决方案已在第5章进行了充分讲解。虽然在目前为止的各种模型的服务器端中使用稍有缺陷的回声客户端也不会引起太大问题,但本章的回声服务器端则不同。因此,需要按照第5章的提示解决客户端存在的问题,并结合改进后的客户端运行本章服务器端。之前已介绍过解决方法,故只给出代码。
#include <“头声明与之前示例一致。“>
#define BUF SIZE 1024
void ErrorHandling(char *message);
int main(int argc, char *argv[]){
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAdr;
char message[BUF_SIZE];
int strlen, readLen;
if(argc!=3){
printf("Usage: %s \n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)ErrorHandling("WSAStartup() error!");
hSocket=socket(PF_INET,SOCK_STREAM, 0);
if(hSocket==INVALID_SOCKET)ErrorHandling("socket() error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s _addr=inet_addr(argv[1]);
servAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSocket,(SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("connect()error!");
else
puts("Connected......");
while(1){
fputs("Input message(Q to quit):", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n")||!strcmp(message,"Q\n"))break;
strLen=strlen(message);
send(hSocket, message, strLen, 0);
readLen=0;
while(1){
readLen+=recv(hSocket,&message[readlen], BUF_SIZE-1-readLen,0);
if(readLen>=strLen)break;
}
message[strLen]=0;
printf("Message from server: %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char *message){
//与其他Windows相关示例一致,故省略。
}
上述代码第44行的循环语句考虑到TCP的传输特性而重复调用了recv函数,直至接收完所有数据。将上述客户端结合之前的回声服务器端运行可以得到正确的运行结果,具体结果与一般的回声服务器端/客户端没有区别,故省略。
下面分析重叠I/O模型回声服务器端的缺点。
“重复调用非阻塞模式的accept函数和以进入alertable wait状态为目的的SleepEx函数将影响性能!"
如果正确理解了之前的示例,应该不难发现这一点。既不能为了处理连接请求而只调用accept函数,也不能为了Completion Routine而只调用SleepEx函数,因此轮流调用了非阻塞模式的accept函数和SleepEx函数(设置较短的超时时间)。这个恰恰是影响性能的代码结构。
我也不知该如何弥补这一缺点,这属于重叠I/O结构固有的缺陷,但可以考虑如下方法:
“让main线程(在main函数内部)调用accept函数,再单独创建1个线程负责客户端I/O。”
其实这就是IOCP中采用的服务器端模型。换言之,IOCP将创建专用的I/O线程、该线程负责
与所有客户端进行I/O。
本节我们编写最后一种服务器模型IOCP,比阅读代码更重要的是理解IOCP本身。
IOCP中已完成的I/O信息将注册到完成端口对象(Completion Port,简称CP对象),但这个过
程并非单纯的注册,首先需要经过如下请求过程:“该套接字的I/O完成时,请把状态信息注册到指定CP对象。"
该过程称为“套接字和CP对象之间的连接请求”。因此,为了实现基于IOCP模型的服务器端,
需要做如下2项工作。
□创建完成端口对象。
□建立完成端口对象和套接字之间的联系。
此时的套接字必须被赋予重叠属性。上述2项工作可以通过1个函数完成,但为了创建CP对
象,先介绍如下函数。
#include
HANDLE CreateIoCompletionport(HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR Completionkey,DWORD NumberofConcurrentThreads);
//成功时返回CP对象句柄,失败时返回NULL。
FileHandle //创建CP对象时传递INVALID_HANDLE_VALUE。
ExistingCompletionPort //创建CP对象时传递NULL。
CompletionKey //创建CP对象时传递0。
NumberOfConcurrentThreads//分配给CP对象的用于处理I/O的线程数。例如,该参数为2时,说明分配
//给CP对象的可以同时运行的线程数最多为2个;如果该参数为0,系统中
//CPU个数就是可同时运行的最大线程数。
以创建CP对象为目的调用上述函数时,只有最后一个参数才真正具有含义。可以用如下代码段将分配给CP对象的用于处理IO的线程数指定为2。
HANDLE hCpObject;
.....
hCpObject =CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,2);
既然有了CP对象,接下来就要将该对象连接到套接字,只有这样才能使已完成的套接字I/O信息注册到CP对象。下面以建立连接为目的再次介绍CreateCompletionPort函数。
#include
HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionport, ULONG_PTR Completionkey,DWORD NumberofConcurrentThreads);
//成功时返回CP对象句柄,失败时返回NULL。
FileHandle //要连接到CP对象的套接字句柄。
ExistingCompletionPort //要连接套接字的CP对象句柄。
CompletionKey //传递已完成I/O相关信息,关于该参数将在稍后介绍的GetQueued
//CompletionStatus函数中共同讨论。
NumberOfConcurrentThreads //无论传递何值,只要该函数的第二个参数非NULL就会忽略。
上述函数的第二种功能就是将FileHandle句柄指向的套接字和ExistingCompletionPort指向的
CP对象相连。该函数的调用方式如下。
HANDLE hCpObject;
SOCKET hSock;
......
CreateIoCompletionPort((HANDLE)hSock,hCpObject,(DWORD)ioInfo,0);
调用CreateIoCompletionPort函数后,只要针对hSock的I/O完成,相关信息就将注册到hCpObject指向的CP对象。
我们已经掌握了CP对象的创建及其与套接字建立连接的方法,接下来就要学习如何确认CP
中注册的已完成的IO。完成该功能的函数如下。
#include
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes,PULONG_PTR lpCompletionKey,LPOVERLAPPED * lpoverlapped, DWORD dwMilliseconds);
//成功时返回TRUE,失败时返回FALSE.
CompletionPort //注册有已完成I/O信息的CP对象句柄
IpNumberOfBytes //用于保存I/O过程中传输的数据大小的变量地址值。
IpCompletionKey //用于保存CreateIoCompletionPort函数的第三个参数值的变量地址值。
IpOverlapped //用于保存调用WSASend、WSARecv函数时传递的OVERLAPPED结构体地址的变量地址
//值。
dwMilliseconds //超时信息超过该指定时间后将返回FALSE并跳出函数。传递INFINITE时,程序
//将阻塞,直到已完成I/O信息写入CP对象。
虽然只介绍了2个IOCP相关函数,但依然有些复杂,特别是上述函数的第三个和第四个参数更是如此。其实这2个参数主要是为了获取需要的信息而设置的,下面介绍这2种信息的含义。
“通过GetQueuedCompletionStatus函数的第三个参数得到的是以连接套接字和CP对象为目的而调用的CreateloCompletionPort函数的第三个参数值。”
“通过GetQueueCompletionStatus函数的第四个参数得到的是调用WSASend、WSARecv函数时传入的WSAOVERLAPPED结构体变量地址值。”
各位需要通过示例理解这2个参数的使用方法。接下来讨论其调用主体,究竟由谁(何时)调用上述函数比较合理呢?如各位所料,应该由处理IOCP中已完成I/O的线程调用。可能有人又有疑问:“那I/O如何分配给线程呢?"
如前所述,IOCP中将创建全职I/O线程,由该线程针对所有客户端进行I/O。而且CreateloCompletionPort函数中也有参数用于指定分配给CP对象的最大线程数,所以各位或许会有如下疑问:“是否自动创建线程并处理I/O?”
当然不是!应该由程序员自行创建调用WSASend、WSARecv等I/O函数的线程,只是该线程为了确认I/O的完成会调用GetQueuedCompletionStatus函数。虽然任何线程都能调用GetQueuedCompletionStatus函数,但实际得到I/O完成信息的线程数不会超过调用CreateIoCompletionPort函数时指定的最大线程数。
以上就是IOCP服务器端实现时需要的全部函数及其理论说明,下面通过源代码理解程序的整体结构。
虽然介绍了IOCP相关的理论知识,但离开示例很难真正掌握IOCP的使用方法。因此,我将介绍便于理解和运用的(极为普通的)基于IOCP的回声服务器端。首先给出IOCP回声服务器端的main函数之前的部分。
#include
#include
#include
#include
#include
#define BUF SIZE 100
#define READ 3
#define WRITE 5
typedef struct{
SOCKET hClntsock;
SOCKADDR_IN clntAdr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
typedef struct{
OVERLAPPED overlapped;
WSABUF wsaBuf;
char buffer[BUF_SIZE];
int rwMode;
} PER_IO_DATA, *LPPER_IO_DATA;
DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIo);
void ErrorHandling(char *message);
第11行:保存与客户端相连套接字的结构体。
第17行:将I/O中使用的缓冲和重叠I/O中需要的OVERLAPPED结构体变量封装到同一结构
体中进行定义。
接下来介绍main函数部分。
int main(int argc, char* argv[]){
WSADATA wsaData;
HANDLE hComPort;
SYSTEM_INFO sysInfo;
LPPER_IO_DATA ioInfo;
LPPER_HANDLE_DATA handleInfo;
SOCKET hServSock;
SOCKADDR_IN servAdr;
int recvBytes, i, flags=0;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hComPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL, 0, 0);//创建CP对象
GetSystemInfo(&sysInfo);//获得当前系统的信息
for(i=0; ihClntSock=hClntSock;
memcpy(&(handleInfo->clntAdr),&clntAdr, addrLen);
CreateIoCompletionPort((HANDLE)hCIntSock,hComport,(DWORD)handleInfo,0)//建立连接
ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=BUF_SIZE;
ioInfo->wsaBuf.buf=ioInfo->buffer;
ioInfo->rwMode=READ;
WSARecv(handleInfo->hClntSock,&(ioInfo->wsaBuf),1,&recvBytes,&flags,&(ioInfo-
overlapped),NULL);
}
return 0;
}
有几个注意点:
第36~38行:动态分配PER_HANOLE_DATA结构体,并写入客户端相连套接字和客户端地
址信息。
第40行:连接第15行创建的CP对象和第35行创建的套接字。针对这套接字的重叠I/O完成
时,已完成信息将写入连接的CP对象,这会引起GetQueue...函数的返回。请注意观察第三个参数的值。该值是第36~38行中声明开初始化的结构体变量地址值,它同样是在GetQueued..函数返回时得到的。
第42行:动态分配PER_IO_DATA结构体变量空间。相当于同时准备了WSARecv函数中需
要的OVERLAPPED结构体变量、WSABUF结构体变量及缓冲。
第46行:IOCP本身不会帮我们区分输入完成和输出完成的状态。无论输入还是输出,只
通知完成I/O的状态,因此需要通过额外的变量区分这2种I/O。PER_IO_DATA结构体中的rwMode就用于完成该功能。
第47行:WSARecv函数的第七个参数为OVERLAPPED结构体变量地址值,该值可以在
GetQueue...函数返回时得到。但结构体变量地址值与第一个成员的地址值相同,也就相当于传入了PER_IO_DATA结构体变量地址值。
最后给出线程main函数,则部分代码需要结合之前的main函数进行分析。
DWORD WINAPI EchoThreadMain(LPVOID pComPort){
HANDLE hComPort=(HANDLE)pComPort;
SOCKET sock;
DWORD bytesTrans;
LPPER_HANDLE_DATA handleInfo;
LPPER_IO_DATA ioInfo;
DWORD flags=0;
while(1){
GetQueuedCompletionStatus(hComport,&bytesTrans,
(LPDWORD)&handletnfo(LPOVERLAPPED*)&ioInfo, INFINITE);
sock=handleInfo->hclntsock,
if(ioInfo->rwMode==READ){
puts("message received!");
if(bytesTrans==0){//传输EOF时
closesocket(sock);
free(handleInfo);
free(ioInfo);
continue;
}
memset(&(ioInfo->overlapped), 0,sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=bytesTrans;
ioInfo->rwMode=WRITE;
WSASend(sock,&(ioInfo->wsaBuf),1,NULL, 0, &(ioInfo->overlapped),NULL);
ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
memset(&(ioInfo->overlapped),0, sizeof(OVERLAPPED));
ioInfo->wsaBuf.len=BUF_SIZE;
ioInfo->wsaBuf.buf=ioInfo->buffer;
ioInfo->rwMode=READ;
WSARecv(sock,&(ioInfo->wsaBuf),1,NULL, &flags,&(ioInfo->overlapped),NULL);
}
else{
puts("message sent!");
free(ioInfo);
}
}
return 0;
void ErrorHandling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
第12行:GetQueued..函数在I/O完成且己注册相关信息时返回(因为最后一个参数为
INFINITE)。另外,返回时可以通过第三个和第四个参数得到之前提过的2个信
息。
第16行:指针iolnfo中保存的既是OVERLAPPED结构体变量地址值,也是PER_IO_DATA
结构体变量地址值。因此,可以通过检查rwMode成员中的值判断是输入完成还是输出完成。
第26~30行:将服务器端收到的消息发送给客户端。
第32~38行:再次发送消息后接收客户端消息。
第40行:完成的I/O为输出时执行的else区域。
盲目地认为“因为是IOCP,所以性能更好”的想法并不可取。之前已介绍了Linux和Windows下多种服务器端模型,各位应该可以分析出它们在性能上的优势。将其在代码级别与select进行对比,可以发现如下特点。
1.因为是非阻塞模式的I/O,所以不会由I/O引发延迟
2.查找已完成I/O时无需添加循环
3.无需将作为I/O对象的套接字句柄保存到数组进行管理
4.可以调整处理I/O的线程数,所以可在实验数据的基础上选用合适的线程数。
仅凭这些特点也能判断IOCP属于高性能模型,IOCP是Windows特有的功能,所以很大程度上要归功于操作系统。无需怀疑它提供的性能,我认为IOCP和Linux的epoll都是非常优秀的服务器端模型。