Winsock I/O方法
1.blocking(阻塞)模型
最简单的模式,也是最基础的模式。
2.select模型
其使用select函数实现对I/O的管理。select函数可以判断套接字上是否有数据,或者是否能够向套接字上写数据。设计这可函数的目的是,为了防止套接字处于阻塞模式时,I/O调用过程处于阻塞模式;或者当套接字处于非阻塞模式时,产生WSAEWOULDBLOCK错误。如果不满足实现规定的参数条件,那么select函数在进行I/O操作时会阻塞。其定义如下:
int select( int nfds, fd_set FAR* readfds, fd_set FAR* writefds, fd_set FAR* exceptfds, const struct timeval FAR* timeout);nfds:这个参数会被忽略。readfds:其为sd_set类型,其是一系列套接字的集合,用于检查可读性。这个集合要满足下面条件之一:1)有数据可读入 2)连接已经被关闭、重启或终止 3)假如已经调用listen,且有一个连接处于搁置状态,那么accept调用成功。wrtiefds:用于检查可写性。其套接字要满足下面条件之一:1)有数据发出 2)如果正在对一个非阻塞连接调用进行处理,则连接就成功了。exceptfds:用于带外数据。其套机字要满足下面条件之一:1)加入正在对一个非阻塞连接调用进行处理,连接尝试机会失败。 2)有00B数据可读操作。timeout:其一个指向timeval结构体的指针,用于表示select函数在调用返回前的等待时间,如果为空指针({0,0}),表示无限期等待。不为0,表示其中至少一个套接字满足条件。返回值:如果select调用成功,会在fd_set结构中,返回被挂起的I/O操作的所有套接字句柄总量。超时,返回0。失败,返回SOCKET_ERROR。
select返回后,会对每个fd_set结构体进行修改,会将那些不存在被挂起I/O操作的套接字删除。也就是说,我们可以通过FD-ISSET宏来判断等待的套接字是还处于宏中。
timeval结构体定义如下:
struct timeval{ long tv_sec; long tv_usec;};tv_sec:以秒为单位指定等待时间。tv_usec:以毫秒为单位指定等待时间。
在用select函数对套接字进行监听前,需要将套接字分配给一个集合。对fd_set集合进行处理与检查的宏:
FD_ZERO(*,set):将set集合初始化为空。FD_CLR(s,* set):从set中删除套接字s.FD_ISSET(s,* set):从集合set中检查s是否在其中;是,就返回TRUE。FD_SET(s,* set):将套接字s加入集合set中。FD_SETSIZE:对fd_set结构中的最多套接字进行设置。因为默认情况下最多能包含64个套接字。下面是一个框架:
SOCKET s;fd_set fread;int ret;while(TRUE){ FD_SERO(&fread); FD_SET(s,&fread); if((ret=connect(0,&fread,NULL,NULL,NULL))==SOCKET_ERROR) { .... } if(ret>0) { if(FD_ISSET(s,&fread)) { } }}
3.WSAAsyncSelect模型
这个模型是在一个套接字上,可以接收以windows消息为基础的网络事件通知。要想使用该函数模型,首先必须用CreateWindow函数来创建一个窗口,并且为该窗口提供一个窗口过程支持函数。WSAAsyncSelect和WSAEventSelect一样提供了异步数据读写能力的通知,但不提供异步数据的传输,而重叠和完成端口都提供了异步数据传输功能。该函数定义如下:
int WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);s:为感兴趣的套接字。hWnd:为一个窗口句柄,用于接收消息。wMsg:发生网络事件时,打算接收的消息。该消息会被传到标识的窗口。lEvent:是一位掩码,指定一系列网络事件集合,有如下值(可以用“或”运算):FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT和FD_CLOSE。
接收网络消息的窗口过程:
LPRESULT CALLBACK WindowPro( HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam);对于WSAAsyncSelect来说,其中wMsg为网路事件发生时发给窗口过程的消息;wParam为发生网络事件的套接字;lParam高字节为可能发生的错误,可用宏WSAGETSELECTERROR获得lParam参数低字节的错误信息;lParam低字节为发生的网络事件,可用WSAGETSELECTEVENT返回lparam低字节。
一个模型如下:
define WM_SOCKET WM_USER+1 int WINAPI WinMain(....) { ..... HWND window=CreateWindow(...); .... WSAAsyncSelect(s,window,WM_SOCKET,FD_ACCEPT|FD_CLOSE); ... } BOOL CALLBACK ServerPro(HWND hWnd,UINT wMsg,WPARAM wParam,LPARAM lparam) { switch(wMsg) { case WM_PAINT: .... break; case WM_SOCKET: if(WSAGETSELECTERROR | lParam) { ..... break; } switch(WSAGETSELECTEVENT(lPram)) { case FD_ACCPET: accept=accept(wParam,NULL,NULL); WSAAsyncSelect(accept,window,WM_SOCKET,FD_READ|FD_WRITE|FD_CLOSE); break; case FD_FD_READ: break; ....... } break; } return TRUT;}
其中FD_WRITE事件发生的条件:
1)使用conenct或WSAConnect,一个套接字首次建立连接
2)使用accept或WSAAccept,套接字被接收之后。
3)若send、WSASend、sendto或WSASendTo调用失败后,返回WSAEWOULDBLOCK错误,而且缓冲空间变得可用时。
WSAAsyncSelect模型优点是可以子啊花销不大情况下处理多个连接,缺点是必须建立窗口。
4.WSAEventSelect模型
其与WSAAsyncSelect一样,都运行在一个或多个套接字上接收网络事件,不同之处在于WSAEventSelect网络事件通知是由事件对象句柄完成的,而不是窗口。事件通知模型需要应用程序使用的每个套接字,首先建立一个事件对象,通过函数WSACreateEvent来实现。WSACreateEvent函数返回的是一个人工重置的事件对象句柄。
其定义如下:
WSAWVENT WSACreateEvent(void);
创建了事件对象句柄后,就需要将其连接到一个套接字上,同时注册感兴趣的网络事件,可以通过WSAEventSelect函数来实现的,其定义如下:
int WSAEventelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);s:为程序感兴趣的套接字。hEventObject:为要关联的事件对象句柄。lNetworkEvents:为感兴趣的网络事件,与WSAAsyncSelect一样。
WSAEventSelect有两种工作状态(传信和未传信)和工作模式(人工重置和自动重置)。WSACreateEvent在一个为未传信的状态下,建立一个人工重置的事件对象。当网络事件触发了相关的事件对象后,事件对象将从未传信变为传信状态。由于其为人工重置,所以在处理完后,需调用WSAResetEvent函数将事件对象重置为未传信。其定义如下:
BOOL WSAResetEvent(WSAEVENT hEvent);
应用程序完成对摸个事件对象的处理后,将调用WSACloseEvent函数关闭事件对象。其定义如下:
BOOL WSACloseEvent(WSAEVENT hEvent);
上面两个函数调用成功都返回TRUE,失败都返回FALSE。
套接字和一个事件对象关联后,应用程序就可以进行I/O处理了:这就需要应用程序等待网络事件触发事件句柄的工作状态。WSAWaitForMultipleObject函数的设计宗旨就是用来等待一个或多个事件对象句柄。该函数会在事先设定一个或多个事件处于已传信后返回,或者等待超时后返回。其定义如下:
DWORD WSAWaitForMultipleEvents( DWORD cEvents, const WSAENENT FAR* lphEvents, BOOL fWaitAll, DWORD dwTimeOut, BOOL fAlertable);cEvents:为事件对象的数量。lphEvents:为事件对象数组。fWaitAll:TRUE是指所有事件对象变为传信后才返回;FALSE表示数组中有一个事件对象变为传信后就返回。dwTiemOut:表示等待的时间,时间一到就返回(返回WSA_WAIT_TIMEOUT)。设为0,表示无限等待,只有对象为传信才返回。fAertable:对于该模型来说,设为FALSE就可以了。
应用程序通过返回的值,来判断是哪个套接字的网络事件被触发。当知道哪个套接字被触发后,就可以通过WSAEnumNetworkEvents函数,来判断是发生了哪些网络事件。其定义如下:
int WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);
hEventObject:可选参数,选择后将使事件对象变为未传信,不选就使用WSAResetEvent将事件设为未传信。
lpNetworkEvents:为指向WSANETWORKEVENTS结构体,用于判断套接字上发生的网络事件或者出现的错误代码。
WSANETWORKEVENTS定义如下:
typedef struct _WSANETWORKEVENTS{ long lNetworkEvents, int iErrorCode[FD_MAX_EVENTS]}WSANETWORKEVENTS,FAR *LPWSANETWORKEVENTS;
lNetworkEvents:为一个网络事件
iErrorCode:为一个错误代码数组,每个网络事件都有一个索引。
下面是WSAEventSelect模型的一个架构:
SOCKET SocketArray[WSA_MAINUM_WAIT_EVENTS];WSAEVENT EventArraY[WSA_MAINUM_WAIT_EVENTS];.......newEvent=WSACreateEvent();WSAEventSelect(Listen,newEvent,FD_ACCEPT|FD_CLOSE);listen(Listen,5);....while(TRUE){ Index=WSAWaitForMultipleEvent(EventToal,EventArray,FALSE,WSA_INFINETE,FALSE); Index=Index-WSA_WAIT_WAIT_0; for(int i=Index;i<EventTotal;i++)//遍历所有事件,查看不传信的事件是否多于一个 { Index=WSAWaitForMultipleEvent(1,&EventArray[Index],TRUE,1000,FALSE); if((Index==WSA_WAIT_FAILED)||(Index==WSA_WAIT_TIMEOUT)) continue; else { Index=i; WSAEnumNetworkevents(SocketArray[Index],EventArray[Index],&NetworkEvents); if(Nerworkvents.lNetworkEvents & FD_ACCEPT) { if(Nerworkvents.iErrorCode[FD_ACCEPT_BLT]!=0) { 错误处理; break; } 连接请求处理 } if(Nerworkvents.lNetworkEvents & FD_READ) { if(Nerworkvents.iErrorCode[FD_READ_BLT]!=0) { 错误处理; break; } 连接请求处理 } ............ } } }
WSAEventSelect其结构简单,不需要窗口。但其不足是最多只能等待64个事件。
5.重叠I/O
重叠I/O是系统性能更佳,其原理是让应用程序使用重叠的数据结构,一次传递一个或多个Winsock I/O请求。针对那些提交的请求,在它们完成后,应用程序可为它们提供服务。这种机制可通过ReadFile和WriteFile函数,在设备上进行I/O操作。
Winsock 重叠I/O开始只能在Windows NT上的Winsock 1.1使用,应用程序使用对套接字句柄调用ReadFile和WriteFile函数函数,同时指定重叠结构。后来,在Winsock 2中,重叠I/O集成到了新的Winsock函数中,如WSASend和WSARecv。
要想在一个套接字上使用重叠I/O,首先必须创建一个设置了重叠标志的套接字。然后将套与一个本地接口绑定在一起,然后就可以进行重叠I/O操作。方法是调用下了Winsock函数,同时指定一个可选的WSAOVERLPPED结构:
函数有:WSASend、WSARecv、WSASendTo、WSARecvFrom、WSAloctl、WSARecvMsg、AcceptEx、ConnectEx、TransmitFile、TransmitPackets、DisconectEx和WSANSPloctl。使用这些函数需要使用WSAOVERLPPED结构作为参数,函数会立即完成调用并返回,不管套接字是否处于阻塞模式。这些函数使用WSAOVERLPPED结构来管理I/O请求的完成。有两种方法来管理:应用程序可以通过等待事件对象通知,也可通过完成例程,对已经完成的请求加以处理。上面的函数,出AcceptEx外还有一个参数:WSAOVERLPPED_COMPLETION_ROUTINE。该参数是一个可选指针,指向一个完成例程函数,该函数在冲抵请求完成后调用。
事件通知
重叠I/O的事件通知方式是将windows事件对象和WSAOVERLPPED结构关联起来,使用想WSARecv和WSASend函数会立即返回。这些调用通常会返回SOCKET_ERROR错误,通过WSAGetLastError便会返回一个与WSA_IO_PENDING错误状态相关的一个报告,表示I/O操作正在完成。应用程序通过与WSAOVERLPPED结构相关的事件对象来判断摸个重叠I/O请求何时完成。WSAOVERLPPED结构定义如下:
typedef struct WSAOVERLPPED{ DWORD Internal; DWORD InternalHigh; DWORD Offest; DWORD OffestHigh; WSAEVENT hEvent;}WSAOVERLPPED,FAR* LPWSAOVERLPPED;Internal、InternalHigh、Offest和OffestHigh有系统内部使用,不能由应有程序直接进行处理或使用。 hEvent:要关联的事件对象。
一个重叠I/O完成后,与WSAOVERLPPED结构关联的事件会有未传信变为传信状态。因此可以用WSAWaitForMultipleEvent函数(最多等待64个事件对象)来判断重叠I/O何时完成。确认某个重叠I/O完成后,就可以调用WSAGetverlappedResult函数来判断这个重叠I/O是成功还是失败。函数的定义如下:
BOOL WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags);s:为重叠I/O操作的那个套接字。lpOverlapped:为传给重叠操作的那个WSAOVERLPPED结构。lpcbTrabsfer:是一个指向DWORD变量的指针,该变量负责接收一次重叠发送或接收操作实际完成的字节数。fWait:决定是否应该等待重叠操作完成。设为TRUE,表示要等待操作完成才会返回;设为FALSE,不等待重叠操作完成,函数会返回FALSE,同时返回一个WSA_IO_INCOMPLETE错误。lpwsFlags:一个指向DWORD变量的指针,用于接收结果标志。返回值:成功返回TRUE,并且lpcbTrabsfer参数指向的值已经更新;失败返回FALSE,lpcbTrabsfer参数不会变,可以通过调用WSAGetLastError获取失败原因。其原因有:1)重叠I/O操作仍然处于等待状态 2)重叠操作已经完成,但含有错误 3)传给函数的参数有错,无法判断重叠操作的状态。
完成例程
完成例程是一些函数,我们将这些函数传递给重叠I/O请求,让系统在重叠操作完成后调用。为了使用完成例程我们需要为Winsock函数指定一个完成例程,同时指定一个WSAOVERLPPED结构。一个完成例程函数的原型如下:
void CALLBACK CompletionROUTINE( DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);dwError:表明一个重叠I/O的完成状态是什么。cbTransferred:表明重叠I/O期间,实际传输的字节量是多大。lpOverlapped:传递到最初重叠操作的WSAOVERLAPPED结构。dwFlags:返回操作结束时可能的标识。
用一个完成例程提交的重叠I/O和用一个事件对象提交的重叠请求间有一个重要区别,就是:WSAOVERLAPPED结构的事件字段未被使用,即不能将事件对象和重叠请求关联在一起。为了让等地线程调用完成例程提供的服务,必须将完成例程置于一种警觉的等待状态。当重叠I/O完成后,对完成例程加以处理。可以使用WSAWaitForMultipleEvent函数来实现。也可以使用SleepEx函数来实现。其定义如下:
DORD SleepEx( DWORD dwMilliseconds, BOOL bAltertable );dwMilliseconds:一毫秒为单位的等待时间。设为INFINITE,表示无限时间。bAltertable:设为TRUE,完成例程会得到执行,同时返回WAIT_IO_COMPLITION;设为FALSE,而且进行了一次I/O完成回叫,那么完成函数不会被调用,且函数不会被返回,除非时间用完。
优点是这种模型的应用程序通知缓冲区收发系统直接使用数据,和前面几种不同。也就是说这种模型,如果应用程序提供了一个接收缓冲区,且数据已经到了,那么数据会被直接复制到接收缓冲区中。前面几种模型,是数据到了后,通知应用程序,应用程序通过调用接收函数,将数据拷贝到缓冲区中。缺点是最多能等待64 事件。
6.完成端口
完成端口适用于要处理的套接字多的情况。完成端口是windows采用的一种I/O构造机制,除了套接字句柄之外,还可接受其他东西。完成端口模型首先创建一个windows完成端口对象,该对象通过指定数量的线程,对重叠I/O操作管理,一遍对已经完成的重叠操作提供服务。创建完成端口可以使用CreateIoCompletionPort函数,该函数将句柄关联大完成端口上,其定义如下:
HANDLE CreateIoCompletionPort( HANDLE FileHandle, HANDLE ExistingComletionPort, DWORD CompletionKey, DWORD NumberOfConcurrentThreads);
FileHandle:与完成端口关联在一起的套接字句柄。
ExistingComletionPort:标识是一个现有的完成端口套接字句柄已经于他关联在一起。
CompletionKey:要与某个特定套接字句柄关联在一起的蛋句柄数据。该参数用于保存于套接字对应的任意类型的信息。
NumberOfConcurrentThreads:用于设置在完成端口上同时执行的线程数量,设置为0,表示有多少处理器就运行同时执行多少线程。
eg:CompletionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
在创建了完成端口对象后,就可以将套接字句柄和对象关联起来。在关联套接字前,需要建立一个或多个工作器线程,以便在套接字的I/O请求投递给完成端口对象后,为完成端口提供服务。关联再次调用CreateIoCompletionPort函数,对前3个参数进行设置。
在本质上,完成端口利用了windows重叠I/O模型。在某个时间,判断OVERLAPPED结构来检索调用结果。在完成端口中,可以通过函数GetQueuedComplrtionStaus来实现,让一个或多个工作器线程在完成端口上等待。其定义如下:
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, LPDWORD lpNumberOfBytesTransferred, PULONG_PTR lpComplrtionKey, LPOVERLAPPED *lpOverlapped, DWORD dwMiliseconds);CompletionPort:表示对应于线程所在的完成端口。 lpNumberOfBytesTransferred:负责在一次完成了一次I/O操作后,实际传输的字节数。lpComplrtionKey:单句柄数据。lpOverlapped:用于接收已完成I/O操作的OVERLAPPED结构。dwMiliseconds:用于指定等待完成数据包到完成端口的等待时间。设为INFINITE,表示无时间限制。
终止所有的线程,调用PostQuenuedCompletionStatus函数来指示每个线程立即结束并退出。