980 likes | 1.38k Views
Winsock I/O 方法. 设计、讲授 : 谭献海 Email : xhtan@home.swjtu.edu.cn. 本节主要内容. 1. 套接字模式 1.1 阻塞模式 1.2 非阻塞模式 2. 套接字 I/O 模型 2.1 select 模型 2.2 WSAAsyncSelect 2.3 WSAEventSelect 2.4 重叠 (overlapped) 模型 2.5 完成端口模型 .
E N D
Winsock I/O方法 设计、讲授:谭献海 Email:xhtan@home.swjtu.edu.cn
本节主要内容 • 1. 套接字模式 • 1.1 阻塞模式 • 1.2 非阻塞模式 • 2. 套接字I/O模型 • 2.1 select模型 • 2.2 WSAAsyncSelect • 2.3 WSAEventSelect • 2.4 重叠(overlapped)模型 • 2.5 完成端口模型
Winsock分别提供“套接字模式”和“套接字I/O模型” 来对一个套接字上的I/O行为加以控制。其中,套接字模式用于决定在随一个套接字调用时该Winsock函数的行为。而套接字I/O模型描述了一个应用程序如何对套接字上的I/O进行管理及处理。 • 套接字模式: • 阻塞 (Blocking) • 非阻塞 (non-blocking) • 套接字I/O模型: • Select(选择) • WSAAsyncSelect(异步选择) • WSAEventSelect(事件选择) • Overlapped I/O (重叠I/O) • Completion port(完成端口)
性能测试结果 • 下表是Network Programming for Microsoft Windows (2nd )一书中对不同模式的一个性能测试结果。服务器采用Pentium 4,1.7 GHz Xeon的CPU,768M内存;客户端有3台PC,配置分别是Pentium 2 233MHz ,128 MB 内存,Pentium 2 350 MHz ,128 MB内存,Itanium 733 MHz ,1 GB内存。
1. 套接字模式 • Windows套接字在两种模式下执行I/O操作:阻塞和非阻塞。 • 在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序),直到该函数操作完成,或出错。 • 在非阻塞模式下, Winsock函数无论如何都会立即返回。
1.1 阻塞模式 • 对于处在阻塞模式的套接字,我们必须多加留意,因为在一个阻塞套接字上调用任何一个Winsock API函数,都会产生相同的后果—耗费或长或短的时间“等待”。 • 一个典型的例子
简单的阻塞模式示例 代码的问题: 假如没有数据处于“待决”状态,那么recv函数可能永远都无法返回。 只有从系统的输入缓冲区中读回点什么东西或出错,才返回! SOCKET sock; char buff[256]; int done = 0; …… while(!done) { nBytes = recv(sock,buff,65,0); if (nBytes == SOCKET_ERROR) { printf(“recv failed with error %d\n”,WSAGetLastError()); return; } DoComputationData(buff); } ……
解决办法之一:在recv中使用MSG_PEEK标志,在系统的缓冲区中,事先“偷看”是否存在足够的字节数量。解决办法之一:在recv中使用MSG_PEEK标志,在系统的缓冲区中,事先“偷看”是否存在足够的字节数量。 • 但在“偷看”的时候,对系统造成的开销是极大的。应尽量避免 • 措施二: 将应用程序划分为一个读线程和一个处理线程,两个线程共享同一个数据缓冲区。 用一个同步对象,比如事件+临界区或者Mutex(互斥体)进行线程之间的同步。 “读线程”的职责是从网络连续地读入数据,并将其置入共享缓冲区内。读线程将处理线程开始工作至少需要的数据量拿到手后,便会触发一个事件,通知处理线程从缓冲区取去数据,进行相应的处理。
基于临界区的多线程阻塞套接字示例 • 分别提供了两个函数,一个负责读取网络数据( ReadThread),另一个则负责对数据执行处理( ProcessThread)。 定义临界区对象 定义事件对象 接收线程
进入临界区 离开临界区 设置信号,通知处理线程
处理线程 等待读线程信号 进入临界区 离开临界区
多线程方法尽管会增大一些开销,但的确是一种可行的处理阻塞套接字方案。唯一的缺点便是扩展性差,难以处理大量套接字。多线程方法尽管会增大一些开销,但的确是一种可行的处理阻塞套接字方案。唯一的缺点便是扩展性差,难以处理大量套接字。
1.2 非阻塞模式 • 非阻塞模式的套接字在使用上稍显困难,但它在功能和效率上要比阻塞模式强大得多。 • 创建一个套接字,并将其置为非阻塞模式的程序示例: 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 }
设置一个非阻塞套接字示例 • 将一个套接字置为非阻塞模式之后, Winsock API调用会立即返回。大多数情况下,这些调用都会报告“失败”,并返回一个WSAEWOULDBLOCK错误。例如在系统的输入缓冲区中尚不存在“待决”的数据时,recv(接收数据)调用就会返回WSAEWOULDBLOCK错误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。 • 非阻塞套接字上报告的WSAEWOULDBLOCK错误的可能情况
阻塞和非阻塞套接字模式各有优点和缺点。 • 从概念上说,阻塞套接字更易使用。但在应付建立连接的多个套接字时,或在数据的收发量不均,或时间不定时,却显得极难管理。 • 而另一方面,由于需要编写更多的代码,以便在每个Winsock调用中对收到一个WSAEWOULDBLOCK错误的可能性加以处理,非阻塞套接字便显得有些难于操作。 • 在这些情况下,可考虑使用“套接字I/O模型”,它有助于应用程序通过一种异步方式,同时对一个或多个套接字上发生的通信事件加以管理。
2 套接字I/O模型 • 下列五种类型的套接字I/O模型,可让Winsock应用程序对I/O进行异步管理。 • select模型(选择) • WSAAsyncSelect模型(异步选择) • WSAEventSelect模型(事件选择) • overlapped模型(重叠) • Completion-port模型(完成端口)
2.1 select模型 • select()可以提供类似windows中的消息驱动机制,实现对I/O事件的管理。通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否有接收数据,或者能否向一个套接字写入数据,或者出现意外。目的是防止应用程序在套接字处于阻塞模式中时,在一次I/O绑定调用(如send或recv)过程中,被迫进入“阻塞”状态;同时防止在套接字处于非阻塞模式中时,产生WSAEWOULDBLOCK错误。 • select的函数原型如下: #include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_est *exceptfds, struct timeval *timeout);
说明: • 函数的返回值: # of ready objects, -1 if error 0 if timeout。 • readfds、writefds、exceptfds三个变量至少有一个不为空,同时这个不为空的套接字组中至少有一个socket。 • 参数: • 参数nfds(1 + largest file descriptor to check)会被忽略。提供这个参数主要是为了保持与早期的Berkeley套接字应用程序相兼容。 • 参数readfds用于检查可读性,readfds集合包括符合下述任何一个条件的套接字: • 有数据可以读入。 • 连接已经关闭、重设(Reset0或中止)。 • 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
参数writefds用于写数据,writefds集合包括符合下述任何一个条件的套接字:参数writefds用于写数据,writefds集合包括符合下述任何一个条件的套接字: • 有数据可以发出。 • 如果已完成了对一个非阻塞连接调用的处理,连接就会成功。 • 参数exceptfds用于例外,exceptfds集合包括符合下述任何一个条件的套接字: • 假如已完成了对一个非阻塞连接调用的处理,连接尝试失败。 • 有带外(Out-of-band,OOB)数据可供读取。 • 参数timeout对应的是一个时间指针,它指向一个timeval结构,用于决定select最多等待I/O操作完成多久时间。
timeval结构 struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; • 参数: • tv_sec: 以秒为单位指定等待时间; • tv_usec:以毫秒为单位指定等待时间 • 说明:此结构主要是设置select()函数的等待值,如果将该结构设置为(0,0),则select()函数会立即返回。
fd_set结构 • #define FD_SETSIZE 64 • typedef struct fd_set { • u_int fd_count; /* how many fd are SET ? */ • SOCKET fd_array[FD_SETSIZE]; /* an array of • SOCKETs */ • } fd_set; • 参数: • fd_count:已设定socket的数量 • fd_array:socket列表,FD_SETSIZE为最大socket数量,建议不小于64(微软建议)。
用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄加入到集合fd_set,设置好一个或全部读、写以及例外fd_set结构。将一个套接字加入到任何一个集合fd_set后,再来调用select,便可知道一个套接字上是否正在发生上述的I/O活动。用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄加入到集合fd_set,设置好一个或全部读、写以及例外fd_set结构。将一个套接字加入到任何一个集合fd_set后,再来调用select,便可知道一个套接字上是否正在发生上述的I/O活动。 • Winsock提供了下列宏操作,用来针对I/O活动,对fd_set进行处理与检查: • FD_CLR(s,*set) :从set中删除套接字s。 • FD_ISSET (s,*set) :检查s是否是set集合的一名成员 • FD_SET (s,*set) :将套接字s加入集合set。 • FD_ZERO ( *set ) :将set初始化为空集合。 • Before calling select: • FD_ZERO(&fdvar): clears the structure • FD_SET(i, &fdvar): to set socket i to the file desc. • After calling select: • int FD_ISSET(i, &fdvar): boolean returns TRUE iff i is “ready”
用select操作一个或多个套接字句柄的全过程 • 使用FD_ZERO宏,将fd_set初始化为空集合。 • 使用FD_SET宏,将套接字句柄加入到fd_set。 • 调用select函数,在指定的fd_set集合中设置好一个或多个套接字句柄(系统将自动将这些套接字设置为非阻塞模式),并注册一个或多个网络事件。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。 • 根据select的返回值,应用程序便可判断出哪些套接字存在着尚未完成(待决)的I/O操作—具体的方法是使用FD_ISSET宏,对fd_set集合进行检查。 • 知道了fd_set集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤(1),继续进行select处理。
例子:用select管理一个套接字上的I/O操作 procedure TListenThread.Execute;var addr : TSockAddrIn; fd_read : TFDSet; timeout : TTimeVal; ASock, MainSock : TSocket; len, i : Integer;begin MainSock := socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); addr.sin_family := AF_INET; addr.sin_port := htons(5678); addr.sin_addr.S_addr := htonl(INADDR_ANY);bind( MainSock, @addr, sizeof(addr) );listen( MainSock, 5 );
while (not Terminated) do beginFD_ZERO( fd_read ); FD_SET( MainSock, fd_read ); timeout.tv_sec := 0; timeout.tv_usec := 500; if select( 0, @fd_read, nil, nil, @timeout ) > 0 then //至少有1个等待Accept的connection begin if FD_ISSET( MainSock, fd_read ) then begin for i:=0 to fd_read.fd_count-1 do //注意,fd_count <= 64,也就是说select只能同时管理最多64个连接begin len := sizeof(addr); ASock := accept( MainSock, addr, len );
if ASock <> INVALID_SOCKET then.... //为ASock创建一个新的线程,在新的线程中再不停地select end; end; end; end; //while (not self.Terminated)shutdown( MainSock, SD_BOTH );closesocket( MainSock );end;
Select I/O模型优缺点 • 优点:能从单个线程的多个套接字上进行多重I/O操作,避免多线程的资源消耗 • 缺点:fd_set结构中的最大套接字数量通常为64,不适合于大型应用
2.2 WSAAsynSelect模型 • WSAAsynSelect模型(特点:利用Windows窗口消息机制) 常用的异步I/O模型。 应用程序在一个套接字上接收以Windows消息为基础的网络事件通知(消息)。 • 该模型的工作原理: 通过调用WSAAsynSelect函数将套接字设置为非阻塞模式,并向Windows注册一个或多个网络事件,并提供一个通知时使用的窗口句柄,以及事件发生时向窗口发送的消息名称。当注册的事件发生时,对应的窗口将会收到一个基于消息的通知。 • 要想使用WSAAsyncSelect模型,在应用程序中,首先必须用CreateWindow函数创建一个窗口,并为该窗口提供一个窗口例程支持函数( WinProc)。亦可使用一个对话框,为其提供一个对话例程,而非窗口例程,因为对话框本质也是“窗口”。
WSAAsynSelect函数 int WSAAsyncSelect( SOCKET s, //需要事件通知的套接字 HWND hWnd, //指定的是一个窗口句柄 u_int wMsg, //指定在发生网络事件时,打算接收的消息 long lEvent //掩码,指定应用程序感兴趣的网络事件组合 ); • 用于WSAAsynSelect函数的网络事件类型:
WSAAsyncSelect函数举例 WSAAysncSelect(s,hwnd,WM_SOCKET, FD_CONNECT | FD_READ | FD_COLSE);
消息通知 • 针对一个套接字调用了WSAAsyncSelect后,套接字的IO模式会从“阻塞”自动变成“非阻塞”,应用程序可以在与hWnd窗口句柄参数对应的窗口例程中,以Windows消息的形式,接收网络事件通知。 • 窗口例程通常定义如下: LRESULT CALLBACK WindowProc( HWND hWnd, //指定一个窗口的句柄 UNIT uMsg, //指定需要对哪些消息进行处理 WPARAM wParam, //指定在其上面发生了一个网络事件的套接字 LPARAM lParam //在lParam参数中,包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。 ); • 网络事件消息抵达一个窗口例程后,应用程序应首先检查lParam的高字位,以判断是否在套接字上发生了网络错误。这里有一个特殊的宏: WSAGETSELECTERROR,可用它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应判断到底是哪个网络事件类型造成了这条Windows消息的触发——具体的做法是读取lParam的低字位的内容。此时可使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。
注意 • 多个事件务必在套接字上一次注册 • 一旦在某个套接字上允许了事件通知,事件通知会永远有效!但下列两种情况例外: • 调用了closesocket命令 • 应用程序针对该套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型 • 将IEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。
举例 • 要接收读写通知: int nResult= WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE); if(nResult==SOCKET_ERROR) { /*错误处理*/ } • 取消读写通知: int nResult= WSAAsyncSelect(s,hWnd,0,0); • 说明:当应用程序窗口hWnd收到消息时,wMsg.wParam参数标识了套接字,lParam的低字标明了网络事件,高字则包含错误代码。
WSAAsyncSelect程序举例 • AsyncSelect\asyncSelect.cpp
窗口句柄 创建窗口
窗口例程 套接字消息 处理套接字错误 处理套接字消息 处理新的连接
关于FD_WRITE事件通知处理 • 只有在三种条件下,才会发出FD_WRITE通知: • 使用connect或 WSAConnect,一个套接字首次建立了连接。 • 使用accept或 WSAAccept,套接字被接受以后。 • 若send、WSASend、sendto 或 WSASendTo操作完成或失败(返回了WSAEWOULDBLOCK错误),而且缓冲区的空间变得可用 • 作为一个应用程序,自收到首条FD_WRITE消息开始,便应认为自己必然能在一个套接字上发出数据,直至一个send、WSASend、sendto或WSASendTo返回套接字错误WSAEWOULDBLOCK 。经过了这样的失败以后,要再用另一条FD_WRITE通知应用程序再次发送数据。
WSAAsyncSelect模型缺点 总是需要创建工作窗口!
2.3 WSAEventSelect模型 • WSAEventSelect模型类似于WSAAsynSelect模型,最主要的区别是网络事件发生时会被发送到一个事件对象句柄,而不是发送到一个窗口。 • 一般步骤: • 创建事件对象来接收网络事件.WSACreateEvent • 将事件对象与套接字关联,同时注册网络事件,使事件对象的工作状态从无信号转变为有信号。 WSAEventSelect • I/O处理后,设置事件对象为未传信. WSAResetEvent • 等待网络事件来触发事件句柄的工作状态: WSAWaitForMultipleEvents • 判断网络事件类型:WSAEnumNetworkEvents • 关闭事件对象句柄:WSACloseEvent
(1) 创建事件对象来接收网络事件 • 创建一个事件对象的方法是调用WSACreateEvent函数,函数原型为: #define WSAEVENT HANDLE /* 事件句柄 */ #define LPWSAEVENT LPHANDLE/* 事件列表 */ WSAEVENT WSACreateEvent( void ); 该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信(signaled)和未传信 (nonsignaled),以及两种工作模式:人工重设(manual reset)和自动重设(auto reset)。默认为未传信的工作状态和人工重设模式。
(2)将事件对象与套接字关联,同时注册网络事件,使事件对象的工作状态从未传信转变为已传信(2)将事件对象与套接字关联,同时注册网络事件,使事件对象的工作状态从未传信转变为已传信 • 方法是调用WSAEventSelect函数,函数原型为: int WSAEventSelect( SOCKET s, //感兴趣的套接字 WSAEVENT hEventObject, /*指定要与套接字关联在一起的事件 对象*/ long lNetworkEvents /*对应一个“位掩码”,用于指定 应用程序感兴趣的各种网络事件类型的一个组合*/ ); • 为WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。WSACreateEvent初始处在一种未传信的工作状态中,并采用人工重设模式来创建事件句柄。随着网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。
(3) I/O处理后,设置事件对象为无信号 • 由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I / O请求的处理之后,应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用WSAResetEvent函数。 • WSAResetEvent函数的定义: BOOL WSAResetEvent( WSAEVENT hEvent ); /*参数hEvent为事件对象*/ /*成功返回TRUE,失败返回FALSE*/ • 完成了对一个事件对象的处理后,便调用WSACloseEvent函数,释放由事件句柄使用的系统资源。 • WSACloseEvent函数的定义如下: BOOL WSACloseEvent( WSAEVENT hEvent ); /*参数 hEvent为事件对象*/ /*成功返回TRUE,失败返回FALSE*/
(4) 等待网络事件来触发事件句柄的工作状态 • WSAWaitForMultipleEvents函数用来等待一个或多个事件对象发生,并在事先指定的一个或所有句柄进入“已传信”状态后,或在超时后,立即返回。 • DWORD WSAWaitForMultipleEvents( DWORD cEvents, const WSAEVENT FAR * lpEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable ); • 参数: • cEvents:事件句柄的数目,其最大值为WSA_MAXIMUM_WAIT_EVENTS • lpEvent:事件句柄数组的指针 • fWaitAll:指定等待类型。TRUE:当lpEvent数组中所有事件对象同时有信号时返回;FALSE:任一事件对象有信号就返回。 • dwTimeout: 等待超时时间(毫秒) • fAlertable:指定函数返回时是否执行完成例程 • 说明:对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去预声明值WSA_WAIT_EVENT_0,得到具体的事件引用值。 • 例如:nIndex = WSAWaitForMultipleEvents(…); MyEvent=EventArray[Index- WSA_WAIT_EVENT_0];
(5) 判断网络事件类型 • 知道了造成网络事件的套接字后,接下来可调用WSAEnumNetworkEvents函数,判断发生了什么类型的网络事件。 • WSAEnumNetworkEvents函数定义如下: int WSAEnumNetworkEvents( SOCKET s, /*套接字*/ WSAEVENT hEventObject, /*需要重设的事件对象*/ LPWSANETWORKEVENTS lpNetworkEvents /*代表一个指针,指 向WSANETWORKEVENTS结构,用于接收套接字上发生的网络事件类型以及可能出现的任何错误代码。*/ ); • WSANETWORKEVENTS结构定义如下: typedef struct _WSANETWORKEVENTS { • long lNetworkEvents;/*表示发生的所有网络事件,通过与FD_XXX作按位与操作判断FD_XXX事件有没有发生 */ int iErrorCode[FD_MAX_EVENTS];/*错误代码数组,同lNetworkEvents中的事件关联在一起, 0表示无错误 */ } WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
(6) 关闭事件对象句柄 调用WSACloseEvent函数 调用成功返回TRUE,否则返回FALSE。
采用WSAEventSelect I/O模型实例 -- 服务器源代码 定义多个套接字对象 和事件对象 创建事件对象并与套接字关联 等待事件发生 得到发生什么事件
处理新的客户端连接 得到新的连接套接字 为新的套接字与事件对象相关联,并加入到事件数组中
处理套接字读事件 读数据