首页 > 代码库 > 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(事件选择)