socket通讯IOCP模型
1.同步与异步模式(Sync/Async)
在一些IO函数如ReadFile(),socket.recv(),默认使用的是同步模式,即函数执行完成后才返回,如果既没有数据,也没有超时设置,则程序会阻塞在这里。在对话框主程序中,如果使用这种方式会把界面卡死。
处理这类问题的常见方法是,启动一个线程,将这些IO操作放在线程中执行。如果需要结束线程的阻塞状态,只要在主线程中关闭Handle/Socket即可。
另一种处理方式为异步模式,IO函数ReadFile()调用后直接返回,通过查询状态来获取其执行结果,如:
HANDLE hFile = CreateFile(xx, GENERIC_READ, 0, NULL,...,FILE_FLAG_OVERLAPPED); //指定FILE_FLAG_OVERLAPPED
OVERLAPPED overlap;
ReadFile(hFile, buf,nsize, &nread, &overlap);//立即返回false,err=IO_PENDING
while(i<100 && !HasOverlappedIoCompleted(&overlap)); // 等待完成……
{ DoSomething();
i++;Sleep(1000);//CancelIO(hFile)...
}
这种方式可以对阻塞过程进行控制,如超时退出或做其他处理。
2.同步模式的多任务
以上两种模式,在单个IO操作时区别不大,异步模式更零活一些。在多任务的情况下,网络服务器面向上万的客户端,同步模式则需要开启上万个线程来通讯
sockServer = socket(...);
_beginThread(Thread_Recving, ...);
Thread_Accepting()
{
while(bRunning)
{
sockClient = accept(sockServer, ...); //blocking
_beginThread(Thread_Recving, ...);
}
}
Thread_Recving() // 10000+个线程
{
while(bRunning)
{
recv(sockClient, ...); //blocking
DoSomthing();
}
}
这种方式非常消耗CPU和内存资源,系统在各个线程之间切换,效率很低。
3.异步模式的多任务——IOCP
在上面的例子中,如果以异步模式建立连接,以轮询的方式来检查各个连接是否收到数据
sockServer = socket(...);
_beginthread(Thread_Accepting, ...);
_beginthread(Thread_Recving, ...);
Thread_Accepting()
{
while(bRunning)
{
sockClient[x] = accept(sockServer, ...); //non-blocking
recv(sockClient[x], &overlap[x]);
}
}
Thread_Recving()
{
while(bRunning)
{
for(i=0; i<10000; i++)
{
if(HasOverlappedIoCompleted(&overlap[i]))
DoSomthing(i);
}
}
}
这种方式显然不可行,并发的时候,单个线程处理,如果DoSomthing(i)处理时间长,则很容易造成数据丢失未处理。
这种情况下,比较好的方法是IOCP(Complete Port).IIS,FTP等许多网络服务器软件,都是采用这种模式。首先根据CPU数量来建立线程连接池,可以利用网卡DMA等特性,直接将网络数据写入内存,再动态调用线程来处理数据。
WSAStartup(MAKEWORD(2, 2), &wsaData); // WinSock 2.2
hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
sockSvr = socket(AF_INET, SOCK_STREAM, 0);
bind(m_sockSvr, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));
listen(m_sockSvr, 10);
CreateThread(Thread_Accepting, ...);
GetSystemInfo(&sysInfo);
for(i=0; i<sysInfo.dwNumberOfProcessors*2+2; i++)
hThread[i] = CreateThread(NULL, 0, Thread_Recving, param, 0, NULL);
Thread_Accepting()
{
while(bRunning)
{
sockClient[x] = accept(sockServer, ...); //non-blocking
CreateIoCompletionPort(sockClient[x], hCompPort, param[x], 0); // 将IO对象sockClient[x]与hCompPort关联
WSARecv(sockClient, wsaBuf, 1, &dwSize, &dwFlag, &overLapped, NULL); //non-blocking
}
}
Thread_Recving() // 4*2+2=10个线程
{
while(bRunning)
{
GetQueuedCompletionStatus(hCompPort, &dwRecv, param, &pOverlap, INFINITE); //blocking
DoSomthing(wsaBuf); // 数据已经接收进了内存
WSARecv(sock, &wsaBuf, 1, &dwRecv, &dwFlag, &overLap, NULL); // continue recving
}
}
这种方式的好处是,恰好利用了全部CPU的资源,因为启动太多线程没有意义,反而耗费资源进行线程切换。