首页 > 代码库 > winsock的io模型(终极篇)

winsock的io模型(终极篇)

最近在看服务器框架的搭建,看了不少,都是零零碎碎的,觉得看的差不多了,可以写点最后的总结了,然后,竟然发现了这篇文章,总结做的特别好,肯定比我总结写要好多了,所以我也就不写了,直接转吧。。。。。。

套接字模式:锁定、非锁定
套接字I/O模型:       select(选择)
WSAAsyncSelect(异步选择)
WSAEventSelect(事件选择)
Overlapped I/O(重叠式I / O)
Completion port(完成端口)



一、 简介

套接字模型的出现,是为了解决套接字模式存在的某些限制。
所有Wi n d o w s平台都支持套接字以锁定或非锁定方式工作。然而,并非每种平台都支持每一种I / O模型。
操作系统对套接字I / O模型的支持情况

平台
select
WSAAsync
WSAEventSelect
Overlapped
Completion Port
Windows CE
支持
不支持
不支持
不支持
不支持
Windows 95(Winsock 1)
支持
支持
不支持
不支持
不支持
Windows 95(Winsock 2)
支持
支持
支持
支持
不支持
Windows 98
支持
支持
支持
支持
不支持
Windows NT
支持
支持
支持
支持
支持
Windows 2000
支持
支持
支持
支持
支持


二、          套接字模式

在锁定模式下,在I / O操作完成前,执行操作的Winsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序)。而在非锁定模式下,Winsock函数无论如何都会立即返回。

锁定模式
处在锁定模式的套接字,因为在一个锁定套接字上调用任何一个Winsock API函数,都会产生相同的后果—耗费或长或短的时间"等待"。

非锁定模式
SOCKET s;
unsigned long ul=1;
int nRet;
s = socket(AF_INET,SOCK_STREAM,0);
nRet = ioctlsocket(s,FIOBIO,(unsigned long *) &ul);
if(nRet == SOCKET_ERROR)
{
      //Failed to put the socket into nonblocking mode
}

三、  套接字I/O模型

1. select模型
select函数,可以判断套接字上是否存在数据,或者能否向一个套接字写入数据。
处于锁定模式时,防止I / O调用(如send或recv),进入"锁定"状态;同时防止在套处于非锁定模式时,防止产生WSAEWOULDBLOCK错误。
除非满足事先用参数规定的条件,否则select函数会在进行I / O操作时锁定。

select的函数原型:
int select(
      int nfds,//会被忽略, 为了保持与早期的B e r k e l e y套接字应用程序的兼容
fd_set FAR * readfds,//检查可读性
fd_set FAR * writefds,//检查可写性
fd_set FAR * exceptfds,//于例外数据
const struct timeval FAR * timeout//最多等待I / O操作完成多久的时间。如是空指针,无限期等
);
//fd_set数据类型代表着一系列特定套接字的集合
Wi n s o c k提供下列宏对f d _ s e t进行处理与检查:
■ FD_CLR(s, *set):从s e t中删除套接字s。
■ FD_ISSET(s, *set):检查s是否s e t集合的一名成员;如答案是肯定的是,则返回T R U E。
■ FD_SET(s, *set):将套接字s加入集合s e t。
■ F D _ Z E R O ( * s e t ):将s e t初始化成空集合。

步骤:
1) 使用F D _ Z E R O宏,初始化自己感兴趣的每一个f d _ s e t。
2) 使用F D _ S E T宏,将套接字句柄分配给自己感兴趣的每个f d _ s e t。
3) 调用s e l e c t函数,然后等待在指定的f d _ s e t集合中,I / O活动设置好一个或多个套接字句柄。s e l e c t完成后,会返回在所有f d _ s e t集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
4) 根据s e l e c t的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成的I / O操作—具体的方法是使用F D _ I S S E T宏,对每个f d _ s e t集合进行检查。
5) 知道了每个集合中"待决"的I / O操作之后,对I / O进行处理,然后返回步骤1 ),继续进行s e l e c t处理。
s e l e c t返回后,它会修改每个f d _ s e t结构,删除那些不存在待决I / O操作的套接字句柄。这正是我们在上述的步骤( 4 )中,为何要使用F D _ I S S E T宏来判断一个特定的套接字是否仍在集合中的原因。
SOCKET s;
fd_set fdread;
int ret;
//create a socket, and accept a connection
//Manage I/O on the socket
while(TRUE)
{
//Always clear the read set before calling select()
FD_ZERO(&fdread);
//Add socket s to the read set
FD_SET(s,&fdread);
If((ret=select(0,&fdread,NULL,NULL,NULL))== SOCKET_ERROR){
    //Error condition
}
if(ret>0)
{
    //For this simple case, select() should return the value 1. An application dealing with
      //more than one socket could get a value greater than 1.At this point,your application
//should check to see whether the socket is part of a set.
If(FD_ISSET(s,&fdread))
{
                //A read event has occurred on socket s
}
}
}

2. WSAAsyncSelect
有用的异步I / O模型,利用这个模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。
函数原型:
int WSAAsyncSelect(
     SOCKET s,
     HWND hWnd,//收到通知消息的那个窗口
     unsigned int wMsg, //指定在发生网络事件时,打算接收的消息
     long lEvent//位掩码,对应于一系列网络事件的组合
);

用于WSAAsyncSelect函数的网络事件:
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_OOB 应用程序想接收是否有带外(OOB)数据抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通知
FD_QOS 应用程序想接收套接字"服务质量"(QoS)发生更改的通知
FD_GROUP_QOS 应用程序想接收套接字组"服务质量"发生更改的通知(为未来套接字组的使用保留)
FD_ROUTING_INTERFACE_CHANGE 应用程序想接收在指定的方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE 应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知

窗口例程,函数原型:
LRESULT CALLBACK WindowProc(
     HWND hWnd,
     UINT uMsg,
     WPARAM wParam,
     LPARAM lParam
);
WSAAsyncSelect调用中定义的消息。wParam参数指定在其上面发生了一个网络事件的套接字。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。在lParam参数中,包含了两方面重要的信息。其中, lParam的低位字指定了已经发生的网络事件,而lParam的高位字包含了可能出现的任何错误代码。
网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在套接字上发生了一个网络错误。这里有一个特殊的宏: WSAGETSELECTERROR,可用它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,造成了这条Windows消息的触发—具体的做法便是读取lParam之低字位的内容。此时可使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。

3.WSAEventSelect
异步I / O模型, 网络事件投递至一个事件对象句柄,而非投递至一个窗口例程。

1.      针对打算使用的每一个套接字,首先创建一个事件对象。
WSAEVENT WSACreateEvent(void);
*相关的函数:BOOL WSAResetEvent(WSAEVENT hEvent);BOOL WSACloseEvent(WSAEVENT hEvent);
WSACreateEvent创建的事件拥有两种工作状态:signaled和nonsignaled,以及两种工作模式:manual reset和auto reset。

2.     将其与某个套接字关联在一起,同时注册自己感兴趣的网络事件类型。
int WSAEventSelect(Socket s,WSAEVENT hEventObject,long lNetworkEvents);

3.      套接字同一个事件对象句柄关联在一起后,应用程序便可开始I/O处理;方法是等待网络事件触发事件对象句柄的工作状态。WSAWaitForMultipleEvents函数的设计宗旨便是用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入"已传信"状态后,或在超过了一个规定的时间周期后,立即返回。
DWORD WSAWaitForMultipleEvents(
           DWORD cEvents,
           const WSAEVENT FAR * lphEvents,
           BOOL fWaitAll,
           DWORD dwTimeOut,
           BOOL fAlertable
);
WSAWaitForMultipleEvents只能支持由WSA_MAXIMUM_WAIT_EVENTS对象规定的一个最大值,在此定义成64个。因此,针对发出WSAWaitForMultipleEvents调用的每个线程,该I/O模型一次最多都只能支持6 4个套接字。假如想让这个模型同时管理不止64个套接字,必须创建额外的工作者线程,以便等待更多的事件对象。

4.     知道了造成网络事件的套接字后,接下来调用WSAEnumNetworkEvents函数,调查发生了什么类型的网络事件。
int WSAEnumNetworkEvents(SOCKET s,WSAEVENT hEventObject,LPWSANETWORKEVENTS lpNetworkEvents);

4. Overlapped I/O
重叠I / O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。
重叠模型的基本设计原理便是让应用程序使用一个重叠的数据结构,一次投递一个或多个Winsock I/O请求。
首先使用WSA_FLAG_OVERLAPPED这个标志,创建一个套接字。s = WSASocket(AF_INET, SOCK_STREAM, 0 ,NULL,0,WSA_FLAG_OVERLAPPED);创建套接字的时候,假如使用的是socket函数,而非WSASocket函数,那么会默认设置WSA_FLAG_OVERLAPPED标志。成功建好一个套接字,同时将其与一个本地接口绑定到一起后,便可开始进行重叠I / O 操作,方法是调用下述的Wi n s o c k 函数,同时指定一个WSAOVERLAPPED结构(可选):
■ WSASend
■ WSASendTo
■ WSARecv
■ WSARecvFrom
■ WSAIoctl
■ AcceptEx
■ TrnasmitFile
步骤:
1) 创建一个套接字,开始在指定的端口上监听连接请求。
2) 接受一个进入的连接请求。
3) 为接受的套接字新建一个WSAOVERLAPPED结构,并为该结构分配一个事件对象句柄。也将事件对象句柄分配给一个事件数组,以便稍后由WSAWaitForMultipleEvents函数使用。
4) 在套接字上投递一个异步WSARecv请求,指定参数为WSAOVERLAPPED结构。注意函数通常会以失败告终,返回SOCKET_ERROR错误状态WSA_IO_PENDING(I/O操作尚未完成)。
5) 使用步骤3 )的事件数组,调用WSAWaitForMultipleEvents函数,并等待与重叠调用关联在一起的事件进入"已传信"状态(换言之,等待那个事件的"触发")。
6) WSAWaitForMultipleEvents函数完成后,针对事件数组,调用WSAResetEvent(重设事件)函数,从而重设事件对象,并对完成的重叠请求进行处理。
7) 使用WSAGetOverlappedResult函数,判断重叠调用的返回状态是什么。
8) 在套接字上投递另一个重叠WSARecv请求。
9) 重复步骤5 ) ~ 8 )。
                     
              void main(void)
{
      WSABUF DataBuf;
      DWORD EventTotal = 0;
      WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
      WSAOVERLAPPED AcceptOverlapped;
      SOCKET ListenSocket,AcceptSocket;
      //Step 1:Start Winsock and set up a listening socket
      ...
      //Step 2:Accept an inbound connection
      AcceptSocket = accept(ListenSocket, NULL , NULL);
      //Step 3:Set up an overlapped structure
      EventArray[EventTotal] = WSACreateEvent();
      ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
      AcceptOverlapped.hEvent= EventArray[EventTotal];          
      DataBuf.len = DATA_BUFSIZE;
      DataBuf.buf = buffer;
      EventTotal ++;
      //Step 4:Post a WSARecv request to begin receiving data on the socket
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL);
While(TRUE)
{
      //Step 5:Wait for the overlapped I/O call to complete
      Index = WSAWaitForMultipleEvents(EventTotal,EventArray,FALSE,WSA_INFINITE,FALSE);
//Index should be 0,because we have only one event handle in EventArray
//Step 6:Reset the signaled event
WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
//Step 7:Determine the status of the overlapped request
WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags);
//First check to see whether the peer has closed the connection, and if so, close the socket
if(BytesTransferred == 0){
     closesocket(AcceptSocket);
     WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
     Return;
}
//Do something with the received data.
//DataBuf contains the received data.
...
//Step 8:Post another WSARecv() request on the socket
Flags = 0;
ZeroMemory(&AcceptOverlapped,sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped,NULL);
}
}
在Windows NT和Windows 2000中,重叠I/O模型也允许应用程序以一种重叠方式,实现对连接的接受。具体的做法是在监听套接字上调用AcceptEx函数。

完成例程
"完成例程"是我们的应用程序用来管理完成的重叠I / O请求的另一种方法。完成例程其实就是一些函数。设计宗旨是通过调用者的线程,为一个已完成的I / O请求提供服务。
void CALLBACK CompletionROUTINE(
       DWORD dwError, //表明了一个重叠操作(由l p O v e r l a p p e d指定)的完成状态是什么。
       DWORD cbTransferred,//实际传输的字节量
   LPWSAOVERLAPPED lpOverlapped,
   DWORD dwFlags // 0
);
在用一个完成例程提交的重叠请求,与用一个事件对象提交的重叠请求之间,存在着一项非常重要的区别。WSAOVERLAPPED结构的事件字段hEvent并未使用;
步骤:
1) 新建一个套接字,开始在指定端口上,监听一个进入的连接。
2) 接受一个进入的连接请求。
3) 为接受的套接字创建一个WSAOVERLAPPED结构。
4) 在套接字上投递一个异步WSARecv请求,方法是将WSAOVERLAPPED指定成为参数,同时提供一个完成例程。
5) 在将fAlertable参数设为TRUE的前提下,调用WSAWaitForMultipleEvents,并等待一个重叠I/O请求完成。重叠请求完成后,完成例程会自动执行,而且WSAWaitForMultipleEven ts会返回一个WSA_IO_COMPLETION。在完成例程内,可随一个完成例程一道,投递另一个重叠WSARecv请求。
6) 检查WSAWaitForMultipleEvents是否返回WSA_IO_COMPLETION。
7) 重复步骤5 )和6 )。

SOCKET AcceptSocket;
WSABUF DataBuf;
void main(void)
{
             WSAOVERLAPPED Overlapped;
//Step 1:Start Winsock, and set up a listening socket
     ...
//Step 2:Accept a new connection
     AcceptSocket = accept(ListenSocket, NULL, NULL);
//Step 3:Now that we have an accepted socket,start processing I/O using overlapped I/O with a completion routine.
//To get the overlapped I/O processing started,first submit an overlapped WSARECV() request.
     Flags = 0;
     ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
     DataBuf.len = DATA_BUFSIZE;
     DataBuf.buf = Buffer;
//Step 4: Post an asynchronous WSARecv() request on the socket by specifying the WSAOVERLAPPED structure
//as a parameter, and supply the WorkerRoutine function below as the completion routine
if(WSARecv(AcceptSocket,&DataBuf,1,&RecvBytes,&Flags,&Overlapped,WorkerRoutine) == SOCKET_ERROR){
     if(WSAGetLastError()!=WSA_IO_PENDING){
                   return;
}
     //Since the WSAWaitForMultipleEvents() API requires waiting on one or more event objects
//we will have to create a dummy event object. As an alternative, we can use SleepEx() instead.
EventArray[0] = WSACreatedEvent();
While(TRUE)
{  
            //Step 5:
Index = WSAWaitForMultipleEvents(1, EventArray, FALSE, WSA_INFINITE, TRUE);
//Step 6:
    if(Index = = WAIT_IO_COMPLETION) {
            //An overlapped request completion routine just completed.Continue servicing more completion rouines.
                   break;
}
else{
    //A bad error occurred - stop processing.
    Return;
}
}
}
}

void CALLBACK WorkerRoutine(DWORD Error,DWORD BytesTransferred,LPWSAOVERLAPPED overlapped,DWORD InFlags)
{
     DWORD SendBytes, RecvBytes;
     DWORD Flags;
     If(Error!=0||BytesTransferred == 0){
      //a bad error occurred on the socket
     closesocket(AcceptSocket);
     return;
}
//At this point, an overlapped WSARecv() request completed successfully.Now we can retrieve the received data that is
//contained in the variable DataBuf. After Processing the received data,We need to post another overlapped
//WSARecv() or WSASend() request.For Simplicity,we need to post another overlapped WSARecv() or WSASend()
//request.For Simplicity,we will post another WSARecv() request.
Flags = 0;
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
if (WSARecv(AcceptSocket,*DataBuf,1,&RecvBytes,&Flags,&Overlapped,WorkerRoutine)==SOCKET_ERROR){
     if(WSAGetLastError()!=WSA_IO_PENDING){
    return;
}
}   
}

5完成端口模型
"完成端口"模型是迄今为止最为复杂的一种I / O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的C P U数量的增多,应用程序的性能也可以线性提升,才应考虑采用"完成端口"模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I / O请求提供服务(We b服务器便是这方面的典型例子),那么I / O完成端口模型便是最佳选择!

6. 个人感悟

按照我现在对这些模型的理解,个人觉得select模型和WSAAsyncSelect(异步选择)WSAEventSelect(事件选择)的差别并不是很大,都是在主线程保存一个socket数组,记录的是已经接受的clientsocket和listensocket,然后就有点区别了,select需要自己在主线程来通过阻塞的方式来判断是否有IO请求进来,而WSAAsyncSelect(异步选择)和WSAEventSelect(事件选择)是通过异步的方式,当有请求进来时会以信息的方式来通知,我们只要响应这个信息就行了,在信息响应函数中进行IO操作,将接受的数据写到程序的内存区。

重叠IO是专门开一个线程,在这个线程里专门accept,当一个client联入后,为它创建一个事件对象和重叠结构,然后调用WSARecv(),或者WSASend()然后这个线程的使命就完成了,继续监听端口等待新的client联进来。然后在另一个线程等待着完成事件的触发,然后进行相应的操作。怎么感觉这个和上面的也差不了多少呢。不过相比较而言这个是多线程的异步,而上面的是单线程的异步。

最后是完成端口模型,也就是IOCP,传说中最高效的异步模型。现在对这个理解并不好,还要好好看看实例。

 

 

 

 

 

WSAAsyncSelect(异步选择)WSAEventSelect(事件选择)