WinSock 异步I/O模型

时间:2023-03-09 15:14:36
WinSock 异步I/O模型

如果你想在Windows平台上构建服务器应用,那么I/O模型是你必须考虑的。 Windows操作系统提供了五种I/O模型,分别是选择(select)模型,异步选择(WSAAsyncSelect)模型,事件选择(WSAEventSelect)模型,重叠I/O(Overlapped I/O)模型,完成端口(Completion Port)模型。

so,接下来就一一介绍这些模型的使用方法:

1、选择(select)模型

  • 选择(select)模型是Winsock中最常见的 I/O模型。核心便是利用 select 函数,实现对 I/O的管理。 利用 select 函数来判断某Socket上是否有数据可读,或者能否向一个套接字写入数据,防止程序在Socket处于阻塞模式中时,在一次 I/O 调用(如send或recv、accept等)过程中,*进入“锁定”状态; 可以同时等待多个套接字,当某个或者多个套接字满足可读写条件时,通知应用程序调用输入或者输出函数进行读写。 同时防止在套接字处于非阻塞模式中时,产生WSAEWOULDBLOCK错误。它意味着请求的操作在调用期间没有时间完成。举个例子来说,假如在系统的输入缓冲区中,尚不存在“待决”的数据,那么recv(接收数据)调用就会返回WSAEWOULDBLOCK错误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。
  • select:select 的函数原型如下:
  • int select( __in int nfds,
  • __in_out fd_set* readfds,
  • __in_out fd_set* writefds,
  • __in_out fd_set* exceptfds,
  • __in const struct timeval* timeout );
  • 其中,第一个参数nfds会被忽略。之所以仍然要提供这个参数,只是为了保持与Berkeley套接字兼容。 后面看到有三个 fd_set类型的参数: 一个用于检查可读性(readfds), 一个用于检查可写性(writefds), 一个用于例外数据(exceptfds)。
  •  fd_set 结构: fd_set 结构的定义如下:
  • typedef struct fd_set {
  • u_int fd_count;
  • SOCKET fd_array[FD_SETSIZE]; } fd_set;
  • #define FD_SETSIZE 64 所以 fd_set 结构中最多只能监视64个套接字。
  • fdset 代表着一系列特定套接字的集合。 其中, readfds 集合包括符合下述任何一个条件的套接字:
  • ● 有数据可以读入。
  • ● 连接已经关闭、重设或中止。
  • ● 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
  • writefds 集合包括符合下述任何一个条件的套接字:
  • ● 有数据可以发出。
  • ● 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
  • exceptfds 集合包括符合下述任何一个条件的套接字:
  • ● 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
  • ● 有带外(Out-of-band,OOB)数据可供读取。
  • 举个例子,假设我们想测试一个套接字是否“可读”,必须将自己的套接字增添到readfds集合中,然后调用 select 函数并等待其完成。select 完成之后,再次判断自己的套接字是否仍为 readfds 集合的一部分。若答案是肯定的,则表明该套接字“可读”,可立即着手从它上面读取数据。 在三个参数中(readfds、writefds 和 exceptfds),任何两个都可以是空值( NULL); 但是,至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄; 否则, select 函数便没有任何东西可以等待。最后一个参数 timeout 对应的是一个指针,它指向一个timeval 结构,用于决定select 最多等待 I/O操作完成多久的时间。如 timeout 是一个空指针,那么 select 调用会无限期地“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束
  •  timeval 结构:对 timeval 结构的定义如下: tv_sec 字段以秒为单位指定等待时间; tv_usec 字段则以毫秒为单位指定等待时间。 1秒 = 1000毫秒 若将超时值设置为(0 , 0),表明 select 会立即返回,出于对性能方面的考虑,应避免这样的设置。
  • selec函数返回值与用法:
  • select 函数返回值: select 成功完成后,会在 fdset 结构中,返回刚好有未完成的 I/O操作的所有套接字句柄的总量。 若超过 timeval 设定的时间,便会返回0。若 select 调用失败,都会返回 SOCKET_ERROR,应该调用 WSAGetLastError 获取错误码! 用 select 对套接字进行监视之前,必须将套接字句柄分配给一个fdset的结构集合,之后再来调用 select,便可知道一个套接字上是否正在发生上述的 I/O 活动。
  • Winsock 提供了下列宏操作,可用来针对 I/O活动,对 fdset 进行处理与检查:
  • ● FD_CLR(s, *set):从set中删除套接字s。
  • ● FD_ISSET(s, *set):检查s是否set集合的一名成员;如答案是肯定的是,则返回TRUE。
  • ● FD_SET(s, *set):将套接字s加入集合set。
  • ●FD_ZERO( * set):将set初始化成空集合。
  • 例如,假定我们想知道是否可从一个套接字中安全地读取数据,同时不会陷于无休止的“锁定”状态,便可使用 FDSET 宏,将自己的套接字分配给 fdread 集合,再来调用 select。要想检测自己的套接字是否仍属 fdread 集合的一部分,可使用 FD_ISSET 宏。采用下述步骤,便可完成用 select 操作一个或多个套接字句柄的全过程: 1) 使用FDZERO宏,初始化一个fdset对象; 2) 使用FDSET宏,将套接字句柄加入到fdset集合中; 3) 调用 select 函数,等待其返回……select 完成后,会返回在所有 fdset 集 合中设置的套接字句柄总数,并对每个集合进行相应的更新。 4) 根据 select的返回值和 FDISSET宏,对 fdset 集合进行检查。 5) 知道了每个集合中“待决”的 I/O操作之后,对 I/O进行处理,然后返回步骤 1 ),继续进行 select 处理。 select 函数返回后,会修改 fdset 结构,删除那些不存在待决 I/O 操作的套接字句柄。 这正是我们在上述的步骤 ( 4 ) 中,为何要使用 FDISSET 宏来判断一个特定的套接字是否仍在集合中的原因。

  • 总结一下,select的优势与不足
  • 优势: 1.可以同时对多个建立起来的套接字进行有序的管理。可以防止应用程序在一次I/O调用过程中,使阻塞模式套接字*进入阻塞状态;使非阻塞套接字产生WASEWOUBDBLOCK错误。 2.selcet()函数好像就是一个消息中心,当消息到来时,通知应用程序接收和发送数据。这使得Windows Sockets应用程序开发人员可以把精力更多集中在如何处理数据的发送和接收上。
  • 不足: 当完成一次I/O操作经历了两次Windows Sockets函数的调用。例如,当接收对方数据时,第一步,调用selcet()函数等待该套接字的满足条件。第二步,调用recv()函数接收数据。这种结果与一个阻塞模式的套接字上调用recv()函数是一样的。因此,使用select()函数的Windows Sockets程序,其效率可能受损。因为,每一个Windows Sockets I/O调用都会经过该函数,因而会导致严重的CPU额外负担。在CPU使用效率不是关键因素时,这种效率可以接受。但是,当需要高效率时,肯定会产生问题。