winsock i/o 方法

98
1 Winsock I/O 方方 设设 设设 设设 设设 设设设 Email Email [email protected] u.cn

Upload: dudley

Post on 15-Jan-2016

272 views

Category:

Documents


0 download

DESCRIPTION

Winsock I/O 方法. 设计、讲授 : 谭献海 Email : [email protected]. 本节主要内容. 1. 套接字模式 1.1 阻塞模式 1.2 非阻塞模式 2. 套接字 I/O 模型 2.1 select 模型 2.2 WSAAsyncSelect 2.3 WSAEventSelect 2.4 重叠 (overlapped) 模型 2.5 完成端口模型. - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Winsock I/O 方法

1

Winsock I/O 方法

设计、讲授设计、讲授:谭献海EmailEmail :: [email protected]

Page 2: Winsock I/O 方法

2

本节主要内容1. 套接字模式1.1 阻塞模式1.2 非阻塞模式

2. 套接字 I/O 模型2.1 select 模型2.2 WSAAsyncSelect2.3 WSAEventSelect2.4 重叠 (overlapped) 模型2.5 完成端口模型

Page 3: Winsock I/O 方法

3

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 (完成端口)

Page 4: Winsock I/O 方法

4

操作系统对套接字 I/O 模型的支持情况

Page 5: Winsock I/O 方法

5

性能测试结果 下表是 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 内存。

Page 6: Winsock I/O 方法

6

1. 套接字模式

Windows 套接字在两种模式下执行 I/O 操作:阻塞和非阻塞。

在阻塞模式下,在 I/O 操作完成前,执行操作的 Winsock 函数(比如 send 和 recv )会一直等候下去,不会立即返回程序(将控制权交还给程序),直到该函数操作完成,或出错。

在非阻塞模式下, Winsock 函数无论如何都会立即返回。

Page 7: Winsock I/O 方法

7

1.1 阻塞模式对于处在阻塞模式的套接字,我们必须多加留意,因为在一个阻塞套接字上调用任何一个 Winsock API 函数,都会产生相同的后果—耗费或长或短的时间“等待”。一个典型的例子

Page 8: Winsock I/O 方法

8

简单的阻塞模式示例

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 函数可能永远都无法返回。只有从系统的输入缓冲区中读回点什么东西或出错,才返回!

Page 9: Winsock I/O 方法

9

解决办法之一:在 recv 中使用 MSG_PEEK 标志,在系统的缓冲区中,事先“偷看”是否存在足够的字节数量。

但在“偷看”的时候,对系统造成的开销是极大的。应尽量避免

措施二: 将应用程序划分为一个读线程和一个处理线程,两个线程

共享同一个数据缓冲区。 用一个同步对象,比如事件 + 临界区或者 Mutex (互斥

体)进行线程之间的同步。 “ 读线程”的职责是从网络连续地读入数据,并将其置入

共享缓冲区内。读线程将处理线程开始工作至少需要的数据量拿到手后,便会触发一个事件,通知处理线程从缓冲区取去数据,进行相应的处理。

Page 10: Winsock I/O 方法

10

基于临界区的多线程阻塞套接字示例分别提供了两个函数,一个负责读取网络数据( ReadThread ),另一个则负责对数据执行处理( ProcessThread )。

接收线程

定义临界区对象

定义事件对象

Page 11: Winsock I/O 方法

11

设置信号,通知处理线程

进入临界区

离开临界区

Page 12: Winsock I/O 方法

12

处理线程

等待读线程信号

进入临界区

离开临界区

Page 13: Winsock I/O 方法

13

多线程方法尽管会增大一些开销,但的确是一种可行的处理阻塞套接字方案。唯一的缺点便是扩展性差,难以处理大量套接字。

Page 14: Winsock I/O 方法

14

1.2 非阻塞模式非阻塞模式的套接字在使用上稍显困难,但它在功能和效率上要比阻塞模式强大得多。 创建一个套接字,并将其置为非阻塞模式的程序示例:SOCKET s;

Unsigned long ul=1;

intnRet;

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

}

Page 15: Winsock I/O 方法

15

设置一个非阻塞套接字示例将一个套接字置为非阻塞模式之后, Winsock API 调用会立即返回。大多数情况下,这些调用都会报告“失败”,并返回一个 WSAEWOULDBLOCK 错误。例如在系统的输入缓冲区中尚不存在“待决”的数据时, recv (接收数据)调用就会返回 WSAEWOULDBLOCK 错误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。非阻塞套接字上报告的 WSAEWOULDBLOCK 错误的可能情况

Page 16: Winsock I/O 方法

16

阻塞和非阻塞套接字模式各有优点和缺点。

从概念上说,阻塞套接字更易使用。但在应付建立连接的多个套接字时,或在数据的收发量不均,或时间不定时,却显得极难管理。

而另一方面,由于需要编写更多的代码,以便在每个 Winsock 调用中对收到一个 WSAEWOULDBLOCK 错误的可能性加以处理,非阻塞套接字便显得有些难于操作。

在这些情况下,可考虑使用“套接字 I/O 模型”,它有助于应用程序通过一种异步方式,同时对一个或多个套接字上发生的通信事件加以管理。

Page 17: Winsock I/O 方法

17

2 套接字 I/O 模型

下列五种类型的套接字 I/O 模型,可让Winsock 应用程序对 I/O 进行异步管理。select 模型(选择)WSAAsyncSelect 模型(异步选择)WSAEventSelect 模型(事件选择)overlapped 模型(重叠)Completion-port 模型(完成端口)

Page 18: Winsock I/O 方法

18

2.1 select 模型select() 可以提供类似 windows 中的消息驱动机制,实现对 I/O 事件的管理。通过调用 select 函数可以确定一个或多个套接字的状态,判断套接字上是否有接收数据,或者能否向一个套接字写入数据,或者出现意外。目的是防止应用程序在套接字处于阻塞模式中时,在一次 I/O 绑定调用(如 send 或 recv )过程中,被迫进入“阻塞”状态;同时防止在套接字处于非阻塞模式中时,产生WSAEW

OULDBLOCK 错误。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) ;

Page 19: Winsock I/O 方法

19

说明: 函数的返回值: # 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 函数调用会成功。

Page 20: Winsock I/O 方法

20

参数writefds 用于写数据,writefds集合包括符合下述任何一个条件的套接字:

有数据可以发出。如果已完成了对一个非阻塞连接调用的处理,连接就

会成功。参数 exceptfds 用于例外 ,exceptfds集合包括符合下述任何一个条件的套接字:

假如已完成了对一个非阻塞连接调用的处理,连接尝试失败。

有带外( Out-of-band , OOB )数据可供读取。参数 timeout 对应的是一个时间指针,它指向一个 time

val结构,用于决定 select最多等待 I/O 操作完成多久时间。

Page 21: Winsock I/O 方法

21

timeval结构struct timeval {

long tv_sec; /* seconds */long tv_usec; /* microseconds */

};参数: tv_sec : 以秒为单位指定等待时间; tv_usec :以毫秒为单位指定等待时间

说明:此结构主要是设置 select() 函数的等待值,如果将该结构设置为 (0,0) ,则 select() 函数会立即返回。

Page 22: Winsock I/O 方法

22

fd_set结构

#define FD_SETSIZE 64typedef 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(微软建议)。

Page 23: Winsock I/O 方法

23

用 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”

Page 24: Winsock I/O 方法

24

用 select 操作一个或多个套接字句柄的全过程

1. 使用 FD_ZERO宏,将 fd_set初始化为空集合。2. 使用 FD_SET宏,将套接字句柄加入到 fd_set 。3. 调用 select 函数,在指定的 fd_set集合中设置好一个或多个套接字句柄(系统将自动将这些套接字设置为非阻塞模式),并注册一个或多个网络事件。 select 完成后,会返回在所有 fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。

4.根据 select 的返回值,应用程序便可判断出哪些套接字存在着尚未完成(待决)的 I/O 操作—具体的方法是使用FD_ISSET宏,对 fd_set集合进行检查。

5. 知道了 fd_set集合中“待决”的 I/O 操作之后,对 I/O进行处理,然后返回步骤( 1 ),继续进行 select 处理。

Page 25: Winsock I/O 方法

25

例子:用 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 );

Page 26: Winsock I/O 方法

26

while (not Terminated) do  begin    FD_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 );

Page 27: Winsock I/O 方法

27

          if ASock <> INVALID_SOCKET then             ....

// 为 ASock 创建一个新的线程,在新的线程中再不停地 select        end;       end;     end;   end; //while (not self.Terminated)  shutdown( MainSock, SD_BOTH );  closesocket( MainSock );end;

Page 28: Winsock I/O 方法

28

Select I/O 模型优缺点优点:能从单个线程的多个套接字上进行多重 I/O 操作,避免多线程的资源消耗

缺点: fd_set 结构中的最大套接字数量通常为 64 ,不适合于大型应用

Page 29: Winsock I/O 方法

29

2.2 WSAAsynSelect 模型WSAAsynSelect 模型(特点:利用 Windows窗口消息机制)

常用的异步 I/O 模型。 应用程序在一个套接字上接收以 Windows消息为基础的网络事

件通知 (消息 ) 。该模型的工作原理 :

通过调用 WSAAsynSelect 函数将套接字设置为非阻塞模式,并向Windows注册一个或多个网络事件,并提供一个通知时使用的窗口句柄,以及事件发生时向窗口发送的消息名称。当注册的事件发生时,对应的窗口将会收到一个基于消息的通知。要想使用 WSAAsyncSelect 模型,在应用程序中,首先必须用CreateWindow函数创建一个窗口,并为该窗口提供一个窗口例程支持函数( WinProc )。亦可使用一个对话框,为其提供一个对话例程,而非窗口例程,因为对话框本质也是“窗口”。

Page 30: Winsock I/O 方法

30

WSAAsynSelect 函数int WSAAsyncSelect(

SOCKET s, // 需要事件通知的套接字HWND hWnd, //指定的是一个窗口句柄u_int wMsg, //指定在发生网络事件时,打算接收的消息long lEvent //掩码,指定应用程序感兴趣的网络事件组合

); 用于 WSAAsynSelect 函数的网络事件类型:

Page 31: Winsock I/O 方法

31

WSAAsyncSelect 函数举例

WSAAysncSelect(s,hwnd,WM_SOCKET,

FD_CONNECT | FD_READ |

FD_COLSE);

Page 32: Winsock I/O 方法

32

消息通知针对一个套接字调用了 WSAAsyncSelect 后,套接字的 IO 模式会从“阻塞”自动变成“非阻塞”,应用程序可以在与hWnd窗口句柄参数对应的窗口例程中,以 Windows消息的形式,接收网络事件通知。

窗口例程通常定义如下:LRESULT CALLBACK WindowProc(

HWND hWnd, // 指定一个窗口的句柄UNIT uMsg, // 指定需要对哪些消息进行处理WPARAM wParam, // 指定在其上面发生了一个网络事件的套

接字LPARAM lParam // 在 lParam 参数中,包含了两方面重要的

信息。其中, lParam 的低字(低位字)指定了已经发生的网络事件,而 lParam 的高字(高位字)包含了可能出现的任何错误代码。);

网络事件消息抵达一个窗口例程后,应用程序应首先检查 lParam 的高字位,以判断是否在套接字上发生了网络错误。这里有一个特殊的宏: WSAGETSELECTERROR,可用它返回高字位包含的错误信息。若应用程序发现套接字上没有产生任何错误,接着便应判断到底是哪个网络事件类型造成了这条Windows消息的触发——具体的做法是读取 lParam 的低字位的内容。此时可使用另一个特殊的宏: WSAGETSELECTEVENT,用它返回 lParam 的低字部分。

Page 33: Winsock I/O 方法

33

注意多个事件务必在套接字上一次注册

一旦在某个套接字上允许了事件通知,事件通知会永远有效!但下列两种情况例外:调用了 closesocket 命令应用程序针对该套接字调用了 WSAAsyncSele

ct ,从而更改了注册的网络事件类型

将 IEvent 参数设为 0 ,效果相当于停止在套接字上进行的所有网络事件通知。

Page 34: Winsock I/O 方法

34

举例要接收读写通知:

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 的低字标明了网络事件,高字则包含错误代码。

Page 35: Winsock I/O 方法

35

WSAAsyncSelect 程序举例AsyncSelect\asyncSelect.cpp

Page 36: Winsock I/O 方法

36

创建窗口

窗口句柄

Page 37: Winsock I/O 方法

37

窗口例程

套接字消息

处理套接字错误

处理套接字消息

处理新的连接

Page 38: Winsock I/O 方法

38

关闭套接字

Page 39: Winsock I/O 方法

39

关于 FD_WRITE 事件通知处理

只有在三种条件下,才会发出 FD_WRITE 通知: 使用 connect 或 WSAConnect ,一个套接字首次建立了连接。 使用 accept 或 WSAAccept ,套接字被接受以后。 若 send、WSASend、 sendto 或 WSASendTo 操作完成或失败(返回了 WSAEWOULDBLOCK 错误),而且缓冲区的空间变得可用

作为一个应用程序,自收到首条FD_WRITE消息开始,便应认为自己必然能在一个套接字上发出数据,直至一个 send、WSASend、 sendto 或 WSASendTo 返回套接字错误WSAEWOULDBLOCK 。经过了这样的失败以后,要再用另一条FD_WRITE 通知应用程序再次发送数据。

Page 40: Winsock I/O 方法

40

总是需要创建工作窗口!

WSAAsyncSelect 模型缺点

Page 41: Winsock I/O 方法

41

2.3 WSAEventSelect 模型WSAEventSelect 模型类似于 WSAAsynSelect 模型,最主要的区别是网络事件发生时会被发送到一个事件对象句柄,而不是发送到一个窗口。

一般步骤:1. 创建事件对象来接收网络事件 .WSACreateEvent 2. 将事件对象与套接字关联,同时注册网络事件,使事件

对象的工作状态从无信号转变为有信号。 WSAEventSelect

3. I/O 处理后,设置事件对象为未传信 . WSAResetEvent4. 等待网络事件来触发事件句柄的工作状态: WSAWaitF

orMultipleEvents5. 判断网络事件类型: WSAEnumNetworkEvents6. 关闭事件对象句柄: WSACloseEvent

Page 42: Winsock I/O 方法

42

(1) 创建事件对象来接收网络事件创建一个事件对象的方法是调用 WSACreateEve

nt 函数,函数原型为: #define WSAEVENT HANDLE /* 事件句柄 */ #define LPWSAEVENT LPHANDLE /* 事件列表 */

WSAEVENT WSACreateEvent( void );

该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信 (signaled) 和未传信 (nonsignaled) ,以及两种工作模式:人工重设 (manual reset) 和自动重设 (auto reset) 。默认为未传信的工作状态和人工重设模式。

Page 43: Winsock I/O 方法

43

(2) 将事件对象与套接字关联,同时注册网络事件,使事件对象的工作状态从未传信转变为已传信 方法是调用 WSAEventSelect 函数,函数原型为:

int WSAEventSelect( SOCKET s, //感兴趣的套接字WSAEVENT hEventObject, /* 指定要与套接字关联在一起的事件

对象 */long lNetworkEvents /*对应一个“位掩码”,用于指定

应用程序感兴趣的各种网络事件类型的一个组合 */ ); 为 WSAEventSelect创建的事件拥有两种工作状态,以及

两种工作模式。 WSACreateEvent初始处在一种未传信的工作状态中,并采用人工重设模式来创建事件句柄。随着网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。

Page 44: Winsock I/O 方法

44

(3) I/O 处理后,设置事件对象为无信号 由于事件对象是在一种人工重设模式中创建的,所以在完成了一个 I /

O请求的处理之后,应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用 WSAResetEvent 函数。

WSAResetEvent 函数的定义:

BOOL WSAResetEvent( WSAEVENT hEvent );/*参数 hEvent 为事件对象 *//*成功返回 TRUE ,失败返回 FALSE*/

完成了对一个事件对象的处理后,便调用 WSACloseEvent 函数,释放由事件句柄使用的系统资源。

WSACloseEvent 函数的定义如下:

BOOL WSACloseEvent( WSAEVENT hEvent );/*参数 hEvent 为事件对象 *//*成功返回 TRUE ,失败返回 FALSE*/

Page 45: Winsock I/O 方法

45

(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];

Page 46: Winsock I/O 方法

46

(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;

Page 47: Winsock I/O 方法

47

(6) 关闭事件对象句柄 调用 WSACloseEvent 函数 调用成功返回 TRUE ,否则返回 FALSE 。

Page 48: Winsock I/O 方法

48

采用 WSAEventSelect I/O 模型实例 -- 服务器源代码定义多个套接字对象

和事件对象

创建事件对象并与套接字关联

等待事件发生

得到发生什么事件

Page 49: Winsock I/O 方法

49

处理新的客户端连接

得到新的连接套接字

为新的套接字与事件对象相关联,并加入到事件数组中

Page 50: Winsock I/O 方法

50

处理套接字读事件

读数据

Page 51: Winsock I/O 方法

51

处理套接字写事件

写数据

处理套接字关闭事件

关闭套接字

Page 52: Winsock I/O 方法

52

2.4 重叠模型overlapped 的含义:当程序在等待设备操作的时候,可以继续往下做而不必阻塞在那个地方等待设备操作的返回,这就造成了程序运行和设备操作时间上的重叠。

在所有 I/O 模型中,重叠 I/O ( Overlapped I/O )模型能使应用程序达到更佳的系统性能。

重叠 I/O 模型是让应用程序使用重叠数据结构 (WSAOVERLAPPED) ,一次投递一个或多个 Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,就可以通过另外的代码来处理这些数据了。

重叠模型适用于除Windows CE 之外的各种 Windows平台。

Page 53: Winsock I/O 方法

53

通过Winsock 2函数使用重叠 I/O 模型要想在一个套接字上使用重叠 I/O 模型,首先必须使用 WSA_FLAG_OVERLAPPED标志来创建一个套接字。

s=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0, WSA_FLAG_OVERLAPPED); 说明:创建套接字的时候,假如使用的是 socket 函数,而非 WSASo

cket 函数,会默认设置 WSA_FLAG_OVERLAPPED标志。进行重叠 I/O 操作的方法 :

调用下面的 Winsock 函数,同时指定一个 WSAOVERLAPPED结构,表示要执行的是 Overlapped 操作 :WSASend、WSASendTo、WSARecv、WSARecvFrom、WSAIoctl、 AcceptEx 。注意:既然要使用重叠结构,需要将 send, sendto, recv, recvfrom替换为 WSASend, WSASendto, WSARecv, WSARecvFrom

Page 54: Winsock I/O 方法

54

例:在接收函数中指定一个 WSAOVERLAPPED结构的函数

int WSARecv(SOCKET s,         // 投递这个操作的套接字 LPWSABUF lpBuffers,          // 接收缓冲区,与 Recv 函数不同 // 这里需要一个由 WSABUF 结构构成的数组 DWORD dwBufferCount,        // 数组中 WSABUF 结构的数量 LPDWORD lpNumberOfBytesRecvd,  // 返回函数调用所接收到 的字节数 LPDWORD lpFlags,             // 一般设置为 0 即可 LPWSAOVERLAPPED lpOverlapped,  // “绑定”的重叠结构 LPWSAOVERLAPPED_COMPLETION_ROUTINE

lpCompletionRoutine // 完成例程,完成例程中将会用到的参数 ); 返回值: WSA_IO_PENDING : 最常见的返回值,说明WSARecv 操作成功了,但是 I/O 操作还没有完成,需要绑定一个事件来通知我们操作何时完成

Page 55: Winsock I/O 方法

55

关于 WSAOVERLAPPED结构typedef struct _WSAOVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; WSAEVENT hEvent; } WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;

其中 hEvent 最重要,允许应用程序将一个事件对象句柄同一个套接字关联起来。可用 WSACreateEvent 函数来创建一个事件对象句柄,赋给重叠结构的 hEvent 字段,再使用重叠结构,调用一个 Winsock 函数即可。

Page 56: Winsock I/O 方法

56

管理一个重叠 I/O请求有两个方法可用来管理一个重叠 I/O请求的完成:① “ 事件对象通知”② “ 完成例程”

“ 事件对象通知”

typedef struct _WSAOVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; WSAEVENT hEvent; // 唯一需要关注的参数,用来关联 WSAEvent 对象 } WSAOVERLAPPED

“ 完成例程” int WSARecv(

SOCKET s, LPWSABUF lpBuffers,DWORD dwBufferCount,   LPDWORD lpNumberOfBytesRecvd,  LPDWORD lpFlags,   LPWSAOVERLAPPED lpOverlapped, // “绑定”的

重叠结构 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );

Page 57: Winsock I/O 方法

57

重叠 I/O 处理方式1-事件通知重叠 I/O 的事件通知方法要求将 Win32事件对象与WSAOVERLAPPED结构关联在一起。若使用一个 WSAOVERLAPPED结构,发出像WSASend 和 WSARecv这样的 I/O 调用,它们会立即返回。

通常,这些 I/O 调用会以失败告终,返回 SOCKET_ERROR( WSA_IO_PENDING ),说明函数调用成功了,但是 I/O 操作还没有完成 。

一个重叠请求操作最终完成之后,在事件通知方法中,Winsock会更改与一个WSAOVERLAPPED结构对应的一个事件对象的事件传信状

“ ” “ ”态,将其从 未传信 变成 已传信 。

发现一次重叠请求完成之后,需要接着调用 WSAGetOverlappedResult (取得重叠结构)函数,判断该重叠调用到底是成功,还是失败。

如果WSAGetOverlappedResult 函数的返回值是 TRUE ,就意味着重叠 I/O 操作已成功完成,而且由 lpcbTransfer参数指向的值已进行了更新。若返回值是 FALSE ,失败后,由 lpcbTransfer参数指向的值不会进行更新,这时应用程序应调用 WSAGetLastError 函数,查看到底是何种原因造成了调用失败。

Page 58: Winsock I/O 方法

58

基于事件通知的重叠 I / O编程步骤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 )。

Page 59: Winsock I/O 方法

59

WSAWaitForMultipleEvents 函数DWORD WSAWaitForMultipleEvents(        DWORD cEvents,          // 等候事件的总数量 const WSAEVENT* lphEvents, // 事件数组的指针 BOOL fWaitAll,          // 如果设置为 TRUE ,则事件数组中所有事件被传信的时候函数才会返回; FALSE则任何一个事件被传信函数就返回 DWORD dwTimeout,    // 超时时间,如果超时,函数会返回WSA_WAIT_TIMEOUT ;如果设置为 0 ,函数会立即返回;如果设置为 WSA_INFINITE只有在某一个事件被传信后才会返回;

  BOOL fAlertable       // 在完成例程中会用到这个参数,在事件通知中设置为 FALSE );

Page 60: Winsock I/O 方法

60

WSAGetOverlappedResult 函数 BOOL WSAGetOverlappedResult(        SOCKET s,                   // SOCKET 句柄 LPWSAOVERLAPPED lpOverlapped,  // 重叠结构的指针 LPDWORD lpcbTransfer,     // 本次重叠操作的实际接收 ( 或发送 ) 的字节数 BOOL fWait,        // 当设置为 TRUE 时,除非重叠操作完成,否则函数不会返回;设置为 FALSE ,而且操作仍处于挂起状态,那么函数就会返回 FALSE ,错误为 WSA_IO_INCOMPLETE

LPDWORD lpdwFlags       // 指向 DWORD 的指针,负责接收结果标志 );

Page 61: Winsock I/O 方法

61

基于事件通知的重叠 I / O 实例

const int BUF_LEN = 4096;    char buffer[BUF_LEN];    WSABUF buf;    buf.buf = buffer;    buf.len = BUF_LEN;    SOCKADDR_IN ServerAddr;    WSADATA wsaData;    SOCKET sockListen,sockClient;    DWORD RecvBytes = 0,    Flag = 0;    WSAOVERLAPPED Overlapped;    WSAEVENT hEvent = WSACreateEvent();    ::ZeroMemory(&Overlapped,sizeof

(WSAOVERLAPPED));    Overlapped.hEvent = hEvent;

Page 62: Winsock I/O 方法

62

基于事件通知的重叠 I / O 实例 WSAStartup(MAKEWORD(2,2),&wsaData);

    ServerAddr.sin_family = AF_INET;    ServerAddr.sin_port = htons(8888);    ServerAddr.sin_addr.s_addr = htons(INADDR_ANY);    sockListen = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,

WSA_FLAG_OVERLAPPED); bind(sockListen,(sockaddr *) &ServerAddr,sizeof (SOCKADDR_IN));

listen(sockListen,5); 

sockClient = accept(sockListen,NULL,NULL); if (SOCKET_ERROR == WSARecv(sockClient,&buf,1,&RecvBytes,&Flag,&Overlapped,NULL))    {        if (WSAGetLastError() != WSA_IO_PENDING)        {            cerr<<"Soket"<<(int) sockClient<<" error!\n";            return 0;        }    }

Page 63: Winsock I/O 方法

63

基于事件通知的重叠 I / O 实例  while (true)

    {       WSAWaitForMultipleEvents(1,&hEvent,FALSE,WSA_INFINITE,FALSE);        WSAResetEvent(hEvent);

WSAGetOverlappedResult(sockClient,&Overlapped,&RecvBytes,FALSE,&Flag);         if (RecvBytes == 0)

        {            cout<<"Socket "<<(int) sockClient<<" has closed!\n";            closesocket(sockClient);            WSACloseEvent(hEvent);            return 0;        }

        cout<<"Receve message: "<<buf.buf<<endl;        Flag = 0;        ::ZeroMemory(&Overlapped,sizeof (WSAOVERLAPPED));        Overlapped.hEvent = hEvent;        if (SOCKET_ERROR ==

WSARecv(sockClient,&buf,1,&RecvBytes,&Flag,&Overlapped,NULL))        {            if (WSAGetLastError() != WSA_IO_PENDING)            {                cerr<<"Soket"<<(int) sockClient<<" error!\n";                return 0;            }        }    }}

Page 64: Winsock I/O 方法

64

参数定义如下:■ 参数 dwError 表明了一个重叠操作(由 lpOverlapped 指定)的完 成状态是什么。■ cbTransferred 参数指定了在重叠操作期间,实际传输的字节量 是多少。■ lpOverlapped 参数指定的是传递到最初的 I/O 调用内的一个 WSAOVERLAPPED 结构。■ dwFlags 参数目前尚未使用,应设为 0 。

重叠 I/O 处理方式2- 完成例程完成例程是用来管理完成的重叠 I/O请求的另一种方法。基本设计宗旨是通过调用者的线程,为一个已完成的 I/O请求提供服务。应用程序可通过完成例程继续进行重叠 I/O 处理。如果希望用完成例程为重叠 I/O请求提供服务,在应用程序中,必须为一个 Winsock I/O 函数指定一个完成例程,同时指定一个 WSAOVERLAPPED 结构。一个完成例程必须拥有下述函数原型:Void CALLBACK CompletionROUTINE( DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);

Page 65: Winsock I/O 方法

65

在用一个完成例程提交的重叠请求与用一个事件对象提交的重叠请求之间,存在着一项非常重要的区别 : WSAOVERLAPPED结构的事件字段 hEvent 并未使用。用完成例程发出了一个重叠 I/O 调用之后,作为我们的调用线程,一旦完成,它最终必须为完成例程提供服务。完成例程定义好后,可用 Win32的 SleepEx 函数将自己的线程置为一种可警告等待状态。 SleepEx 函数的行为实际上和 WSAWaitForMultipleEvents差不多,只是它不需要任何事件对象。对 SleepEx 函数的定义如下:DWORD SleepEx( DWORD dwMiliseconds, BOOL bAlertable)

Page 66: Winsock I/O 方法

66

用完成例程方式实现的重叠 I/O 模型 #include <WINSOCK2.H>

#include <stdio.h>#define PORT    5150#define MSGSIZE 1024#pragma comment(lib, "ws2_32.lib")typedef struct { WSAOVERLAPPED overlap; WSABUF        Buffer;   char          szMessage[MSGSIZE]; DWORD         NumberOfBytesRecvd; DWORD         Flags;  SOCKET        sClient; }PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;DWORD WINAPI WorkerThread(LPVOID);void CALLBACK CompletionROUTINE(DWORD, DWORD,

LPWSAOVERLAPPED, DWORD);SOCKET g_sNewClientConnection;BOOL   g_bNewConnectionArrived = FALSE;

Page 67: Winsock I/O 方法

67

用完成例程方式实现的重叠 I/O 模型int main(){  WSADATA     wsaData;  SOCKET       sListen;  SOCKADDR_IN local, client;  DWORD       dwThreadId;  int         iaddrSize = sizeof(SOCKADDR_IN);  // Initialize Windows Socket library  WSAStartup(MAKEWORD(2,2 ) , &wsaData);  // Create listening socket  sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  // Bind  local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_family = AF_INET; local.sin_port = htons(PORT);  bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));  // Listen  listen(sListen, 5);  // Create worker thread  CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);  while (TRUE)  {    // Accept a connection    g_sNewClientConnection = accept(sListen, (struct sockaddr *)&client, &iaddrSize);    g_bNewConnectionArrived = TRUE;    printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));  }}

Page 68: Winsock I/O 方法

68

用完成例程方式实现的重叠 I/O 模型DWORD WINAPI WorkerThread(LPVOID lpParam){LPPER_IO_OPERATION_DATA lpPerIOData = NULL;  while (TRUE)  {    if (g_bNewConnectionArrived)    {      // Launch an asynchronous operation for new arrived connection      lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(        GetProcessHeap(),        HEAP_ZERO_MEMORY,        sizeof(PER_IO_OPERATION_DATA));      lpPerIOData->Buffer.len = MSGSIZE;      lpPerIOData->Buffer.buf = lpPerIOData->szMessage;      lpPerIOData->sClient = g_sNewClientConnection;            WSARecv(lpPerIOData->sClient,        &lpPerIOData->Buffer,        1,        &lpPerIOData->NumberOfBytesRecvd,        &lpPerIOData->Flags,        &lpPerIOData->overlap,        CompletionROUTINE);                  g_bNewConnectionArrived = FALSE;    }    SleepEx(1000, TRUE);  }  return 0;}

Page 69: Winsock I/O 方法

69

用完成例程方式实现的重叠 I/O 模型void CALLBACK CompletionROUTINE(DWORD dwError,                                DWORD cbTransferred,                                LPWSAOVERLAPPED lpOverlapped,                                DWORD dwFlags){  LPPER_IO_OPERATION_DATA lpPerIOData =

(LPPER_IO_OPERATION_DATA)lpOverlapped;    if (dwError != 0 || cbTransferred == 0){    // Connection was closed by client  closesocket(lpPerIOData->sClient);  HeapFree(GetProcessHeap(), 0, lpPerIOData);}  else  {    lpPerIOData->szMessage[cbTransferred] = '\0';    send(lpPerIOData->sClient, lpPerIOData->szMessage, cbTransferred, 0);        // Launch another asynchronous operation    memset(&lpPerIOData->overlap, 0, sizeof(WSAOVERLAPPED));    lpPerIOData->Buffer.len = MSGSIZE;    lpPerIOData->Buffer.buf = lpPerIOData->szMessage;        WSARecv(lpPerIOData->sClient,      &lpPerIOData->Buffer,      1,      &lpPerIOData->NumberOfBytesRecvd,      &lpPerIOData->Flags,      &lpPerIOData->overlap,      CompletionROUTINE);

Page 70: Winsock I/O 方法

70

2.5 完成端口模型完成端口模型要求创建一个 Win32 完成端口对象,通过指定数量的线程来对重叠 I/O请求进行管理,以便为已经完成的重叠 I/O请求提供服务。可以把完成端口看成系统维护的一个队列,操作系统把重叠 IO 操作完成的事件通知放到该队列里,由于是暴露 “操作完成”的事件通知,所以命名为“完成端口”( Completion Ports )。完成端口模型是迄今为止最为复杂但效率最高的一种 I/O 模型在这个模型中使用一个完成端口句柄,将重叠模型中的线程调度、事件信号发出交由完成端口句柄统一管理。 一个 socket被创建后,可以在任何时刻和一个完成端口联系起来。

Page 71: Winsock I/O 方法

71

Completion Port I/O完成端口就像一种消息通知机制,通过创建一个或多个线程来不断读取完成端口状态,接收到相应的完成通知后,就进行相应的处理。完成端口与 WSAAsyncSelect 有些类似,但不相同:比如我们想接收报文, WSAAsyncSelect 会在报文到来的时候直接通知 Windows 消息循环,然后就可以调用 WSARecv来接收报文了;而完成端口则首先调用一个 WSARecv 表示程序需要接收报文(这时可能还没有任何报文到来),只有当报文到来的时候WSARecv才算完成,用户就可以处理报文了,然后再调用一个 WSARecv来等待下一个报文,如此不停循环。

Page 72: Winsock I/O 方法

72

Completion Port I/O 在完成端口模型中,每个客户端与一个管道进行交互,在交互过程中 I/O 操作结束后产生的完成包就会进入“ I/O 完成包队列”。完成端口的线程队列中的线程使用 GetQueuedCompletionStatus函数来检测“ I/O 完成包队列”中是否有完成包信息。

Page 73: Winsock I/O 方法

73

完成端口模型使用完成端口模型之前,首先要创建一个 I/O 完成端口对象,用它面向任意数量的套接字句柄,管理多个 I/O请求。要做到这一点,需要调用 CreateIOCompletionPort 函数。该函数实际用于两个明显有别的目的:( 1 )创建一个完成端口对象;( 2)将一个句柄同完成端口关联到一起。函数原型:HANDLE CreateIOCompletionPort( HANDLE FileHandle, HANDLE ExistingCompletionPort, Dword CompletionKey, Dword NumberOfConcurrentThreads);

●FileHandle 表示要加入的 IO 句柄( socket 句柄); ExistingCompletionPort 表示已经存在的完成端口句柄; CompletionKey 表示与当前加入到完成端口的 IO 句柄相关的 per_handle_data ,在后续的相关函数调用过程当中只要是这个 IO 句柄引发的,就通过该数据结构来处理; NumberOfConcurrentThreads 表示完成端口上最大的并发线程数量, 0 表示线程数与处理器数相同。

Page 74: Winsock I/O 方法

74

完成端口模型

开始创建—个完成端口的时候,唯一感兴趣的参数便是 NumberOfConcurrentThreads ( 并发线程的数量 ) ;前面三个参数都会被忽略。NumberOfConcurrentThreads   参数定义了在一个完成端口上,同时允许执行的线程数量。理想情况下我们希望每个处理器各自负责—个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将该参数设为 0 ,表示系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码创建一个 I/O 完成端口: CompletionPort     = CreateIoCompletionPort( INVALID_HANDLE_VALUE , NULL , 0 , 0);

Page 75: Winsock I/O 方法

75

Completion Port

线程与完成端口 成功创建一个完成端口后,便可开始将套接字句柄与完成端口对象关联到一起。但在关联套接字之前,首先必须创建—个或多个“线程”,以便在 I/O请求投递给完成端口对象(即存放入 I/O 完成包队列)后,为完成端口提供服务。

Page 76: Winsock I/O 方法

76

ConcurrentThreads vs Worker ThreadsCreateIoCompletionPort 函数中有一个并发线程数参数NumberOfConcurrentThreads ,与完成端口编程里另外一个概念 --工作者线程,比较容易引起混淆。NumberOfConcurrentThreads 需要设置为多少,又需要创建多少个工作者线程才算合适?NumberOfConcurrentThreads 的数目最好和 CPU 数量一样,因为少了就无法利用多 CPU 的优势,而多了则会因为线程切换而造成性能下降。工作者线程的数量是不是也要一样多呢?当然不是,它的数量取决于应用程序的需要。如果工作者线程里没有阻塞操作,对于某些情况来说,一个工作者线程就足够了(在后面的例子中就只有一个工作者线程),否则最好创建多个工作者线程。

Page 77: Winsock I/O 方法

77

Completion Port— 旦在完成端口上拥有足够多的工作者线程来为 I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这要求我们在—个已存在的完成端口上再一次调用 CreateIoCompletionPort 函数,同时为前三个参数: FileHandle , ExistingCompletionPort 和 CompletionKey——提供套接字的信息。其中, FileHandle 参数指定—个要同完成端口关联在一起的套接字句柄。 ExistingCompletionPort 参数指定的是一个已有的完成端口。 Compl

etionKey 参数则指定要与某个特定套接字句柄关联在—起的“单句柄数据( Per-handle Data )”,在这个参数中,应用程序可保存与—个套接字对应的任意类型的信息。之所以把它叫作“单句柄数据”,是由于它只对应着与该套接字句柄关联在—起的数据。可将其作为指向一个数据结构的指针来保存套接字句柄,以及与该套接字有关的其它信息。为完成端口提供服务的例程可通过 Per-handle Data 参数,取得与该套接字句柄有关的信息。

Page 78: Winsock I/O 方法

78

Completion Port在完成端口编程中, Per-handle Data 和 Per-I/O Operation Data 也是两个比较重要但不同的概念。Per-handle Data 用来把客户端数据和对应的完成通知关联起来,这样每次我们处理完成通知的时候,就能知道它是哪个客户端的信息,并且可以根据客户端的信息作出相应的反应。Per-I/O Operation Data则不同,它记录了每次 I/O 通知的信息,比如接收报文时我们就可以从中读出报文的内容,即与 I/O 操作有关的信息都记录在 Per-I/O Operation Data 里面。

Per-handle-data :与每个套接字对应Per-IO-data :统一的 overlapped

Page 79: Winsock I/O 方法

79

创建一个 I/O 完成端口:CompletionPort=CreatIoCompletionPort(INVALTD_HANDLE_VALUE , NULL,0,0)创建一个或多个“工作者线程”,以便在 I/O请求投递给完成端口对象后,为完成端口提供服务。 CreateIoCompletionPort 函数的 NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一次允许多个工作者线程运行。一旦在完成端口上拥有足够多的工作者线程来为 I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这要求我们在一个现有的完成端口上,再次调用 CreateIoCompletionPort 函数,同时通过前三个参数, FileHandle ,ExistingCompletionPort 和 CompletionKey ,提供套接字的相关信息。其中, FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。 CompletionKey指定与该套接字对应的 per_handle_data 。

完成端口模型的使用过程

Page 80: Winsock I/O 方法

80

Completion Port以套接字句柄为基础,投递发送与接收等 IO请求,开始对 I/O请求的处理。使用 GetQueuedCompletionStatus (获取排队完成状态)函数,让一个或多个工作者线程在完成端口上等待。一个工作者线程从 GetQueuedCompletionStatus这个 API 调用接收到 I/O 完成通知后,在 lpCompletionKey 和 lpOverlapped参数中会包含一些必要的套接字信息。利用这些信息,即可通过完成端口,继续在一个套接字上的 I/O 处理。通过这些参数,可获得两方面重要的套接字数据:单句柄数据( Per-handle Data ) ,以及单 I/O 操作数据( Per-I/O Operation Data )。

Page 81: Winsock I/O 方法

81

Completion PortGetQueuedCompletionStatus 函数原型如下:GetQueuedCompletionStatus(    HANDLE CompletionPort,    LPDWORD lpNumberOfBytesTransferred,    LPDWORD lpCompletionKey,    LPOVERLAPPED *lpOverlapped,    DWORD dwMilliseconds );参数 1 为创建线程池时传递的参数 hCompletionPort ,参数 2提供一个DWORD 指针,用来接收当 I/O 完成时实际传输的字节数。参数 3即单句柄完成键,是原先传递进入 CreateIoCompletionPort 函数的套接字对应的“单句柄数据”。参数 4 为套接字分配的 (WSA)OVERLAPPED 结构,实际操作中往往提供一个 (WSA)OVERLAPPED扩展结构,这就是前面说的“单 I/O 数据”:typedef struct{   OVERLAPPED Overlapped;   WSABUF DataBuf;   CHAR Buffer[DATA_BUFSIZE];   BOOL   OperationType; } PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;DwMilliseconds 用于指定调用者希望等待一个完成数据包在完成端口上出现的时间。假如将其设为 INFINITE ,调用会无休止地等持下去。

Page 82: Winsock I/O 方法

82

工作者线程与完成端口示例 完成端口 I/O 程序流程:1) 创建一个完成端口。第四个参数设置为 0 ,指定在完成端口上,每个处理器对应一个工作者线程。2) 判断系统内到底安装了多少个处理器。3) 创建工作者线程,根据步骤 2 ) 得到的处理器信息,为每个处理器创建一个线程来在完成端口上为已完成的 I/O

请求提供服务。调用创建线程函数时,必须同时提供一个工作者例程,由线程在创建后执行。4) 准备好监听套接字,在端口 5150 上监听进入的连接请求。

Page 83: Winsock I/O 方法

83

工作者线程与完成端口示例5) 使用 Accept 函数,接受进入的连接请求。6) 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。7) 再次调用 CreateIoCompletionPort ,将自 Accept 返回的新套接字句柄同完成端口关联到一起。通过完成键( CompletionKey )参数,将单句柄数据结构传递给 CreateIoCompletionPort 。8) 开始在已接受的连接上进行 I/O 操作。在此,我们希望通过重叠 I/O机制,在新建的套接字上投递一个或多个异步WSARecv 或 WSASend请求。这些 I/O请求完成后,一个工作者线程会为 I/O请求提供服务,同时继续处理未来的 I/O请求。9) 重复步骤 5 ) ~ 8 ) ,直至服务器中止。

Page 84: Winsock I/O 方法

84

程序清单:完成端口的建立  WSADATA     wsaData;   // Initialize Windows Socket library  WSAStartup(MAKEWORD(2,2 ) , &wsaData);// Step 1: 创建一个完成端口 CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE , NULL , 0 , 0);// Step 2:判断有多少个处理器 GetSystemInfo(&SystemInfo);// Step 3://根据处理器的数量创建工作线程 .//本例当中,工作线程的数目和处理器数目相同 .for(i=0;   i   <   SystemInfo.dwNumberOfProcessers , i++) {  HANDLE ThreadHandle;  // 创建线程,并把完成端口作为参数传给线程,同时指定工作者例程 .  // NOTE: the ServerWorkerThread procedure is not defined in this listing.  ThreadHandle = CreateThread(NULL , 0 , ServerWorkerThread , CompletionPort , 0 , &ThreadID);  // 关闭线程句柄 (仅仅关闭句柄,并非关闭线程本身 )   CloseHandle(ThreadHandle);}

Page 85: Winsock I/O 方法

85

程序清单:完成端口的建立// Step 4: 创建监听套接字 Listen = WSASocket(AF_INET , SOCK_STREAM , 0 , NULL , 0 , WSA_FLAG_OVERLAPPED);InternetAddr.sin_family = AF_INET;InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);InternetAddr.sin_port = htons(5150);bind(Listen , (PSOCKADDR)&InternetAddr , sizeof(InternetAddr));

//准备监听套接字 listen(Listen , 5);

Page 86: Winsock I/O 方法

86

程序清单:完成端口的建立while(TRUE){  // Step 5: 接纳 Socket  Accept = WSAAccept(Listen , NULL , NULL , NULL , 0);

  // Step 6:  // 创建一个 perhandledata 结构,并与完成端口关联 PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR , sizeof(PER_HANDLE_DATA));

  printf("Socket number %d connected\n" , Accept);  PerHandleData->Socket = Accept;

  // Step 7: 将接纳到的套接字与完成端口关联 CreateIoCompletionPort((HANDLE)Accept , CompletionPort , (DWORD)PerHandleData , 0);

  // Step 8:  // 开始进行 I/O 操作,用重叠 I/O 发送WSASend() 和 WSARecv()   WSARecv(...)}

Page 87: Winsock I/O 方法

87

完成端口工作者线程 : DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID){  HANDLE CompletionPort = (HANDLE)CompletionID;  DWORD BytesTransferred;  LPOVERLOPPED Overlapped;  LPPER_HANDLE_DATA PerHandleData;  LPPER_IO_OPERATION_DATA PerIoData;  DWORD SendBytes , RecvBytes;  DWORD Flags;

  while(TRUE)  {    // Wait for I/O to complete on any socket associated with the completion port    GetQueuedCompletionStatus(CompletionPort , &BytesTransfferred , (LPDWORD)&PerHandleData , (LPOVERLAPPED *)&PerIoData , INFINITE);    // First check to see whether an error has occurred on the socket;    // if so , close the socket and clean up the per-handle data and     // per-I/O operation data associated with the socket

Page 88: Winsock I/O 方法

88

完成端口工作者线程 :  if(BytesTransferred==0 &&      (PerIoData->OperationType==RECV_POSTED ||       PerIoData->OperationType==SEND_POSTED))    {      // A zero Bytes Transferred indicates that the socket has been closed by the peer , // so you should close the socket. NOTE: Per-handle data was used to reference the socket associated with the I/O operation.      closesocket(PerHandleData->Socket);

      GlobalFree(PerHandleData);      GlobalFree(PerIoData);      continue;    }

Page 89: Winsock I/O 方法

89

完成端口工作者线程    // Service the completed I/O request. You can determine which I/O request    // has just completed by looking at the OperationType field contained    // in the per-I/O operation data.    if(PerIoData->OperationType==RECV_POSTED)    {      …… // Do something with the received data in PerIpData->Buffer    }    // Post another WSASend or WSARecv operation.    // As an example , we will post another WSARecv() I/O operation.    Flags = 0;    // Set up the per-I/O operation data for the next overlapped call    ZeroMemory(&(PerIoData->Overlapped) , sizeof(OVERLAPPED));    PerIoData->DataBuf.len = DATA_BUFSIZE;    PerIoData->DataBuf.buf = PerIoData->Buffer;    PerIoData->OperationType = RECV_POSTED;

    WSARecv(PerHandleData->Socket , &(PerIoData->DataBuf) , 1 , &RecvBytes , &Flags , &(PerIoData->Overlapped) , NULL);  }}

Page 90: Winsock I/O 方法

90

完成端口实例 2/////////////////////////////////////////////////// IOCPDemo.cpp文件 调试通过

#include "WSAInit.h"#include <stdio.h>#include <windows.h>

// 初始化Winsock库CWSAInit theSock;

#define BUFFER_SIZE 1024#define OP_READ 1#define OP_WRITE 2#define OP_ACCEPT 3

typedef struct _PER_HANDLE_DATA // per-handle 数据{ SOCKET s; // 对应的套节字句柄 sockaddr_in addr; // 客户方地址 char buf[BUFFER_SIZE]; // 数据缓冲区 int nOperationType; // 操作类型} PER_HANDLE_DATA, *PPER_HANDLE_DATA;

Page 91: Winsock I/O 方法

91

完成端口实例 2DWORD WINAPI ServerThread(LPVOID lpParam){ // 得到完成端口对象句柄 HANDLE hCompletion = (HANDLE)lpParam;

DWORD dwTrans; PPER_HANDLE_DATA pPerHandle; OVERLAPPED *pOverlapped; while(TRUE) { // 在关联到此完成端口的所有套节字上等待 I/O 完成 BOOL bOK = ::GetQueuedCompletionStatus(hCompletion, &dwTrans, (PULONG_PTR)&pPerHandle, &pOverlapped, WSA_INFINITE); if(!bOK) // 在此套节字上有错误发生 { ::closesocket(pPerHandle->s); ::GlobalFree(pPerHandle); ::GlobalFree(pOverlapped); continue; }

if(dwTrans == 0 && // 套节字被对方关闭 (pPerHandle->nOperationType == OP_READ || pPerHandle->nOperationType == OP_WRITE))

{ ::closesocket(pPerHandle->s); ::GlobalFree(pPerHandle); ::GlobalFree(pOverlapped); continue; }

Page 92: Winsock I/O 方法

92

完成端口实例 2switch(pPerHandle->nOperationType) // 通过 per-I/O 数据中的 nOperationType域查看什么 I/O请求完成了 { case OP_READ: // 完成一个接收请求 { pPerHandle->buf[dwTrans] = '\0'; printf(pPerHandle-> buf);

// 继续投递接收 I/O请求 WSABUF buf; buf.buf = pPerHandle->buf ; buf.len = BUFFER_SIZE; pPerHandle->nOperationType = OP_READ; DWORD nFlags = 0; ::WSARecv(pPerHandle->s, &buf, 1, &dwTrans, &nFlags, pOverlapped, NULL); } break; case OP_WRITE: // Process the data sent, etc. break; case OP_ACCEPT: break; } } return 0;}

Page 93: Winsock I/O 方法

93

完成端口实例 2void main(){ int nPort = 4567; // 创建完成端口对象,创建工作线程处理完成端口对象中事件 HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0); ::CreateThread(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0);

// 创建监听套节字,绑定到本地地址,开始监听 SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN si; si.sin_family = AF_INET; si.sin_port = ::ntohs(nPort); si.sin_addr.S_un.S_addr = INADDR_ANY; ::bind(sListen, (sockaddr*)&si, sizeof(si)); ::listen(sListen, 5);

Page 94: Winsock I/O 方法

94

完成端口实例 2 // 循环处理到来的连接 while(TRUE) { // 等待接受未决的连接请求 SOCKADDR_IN saRemote; int nRemoteLen = sizeof(saRemote); SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen); // 接受到新连接之后,为它创建一个 per-handle 数据,并将它们关联到完成端口对象。 PPER_HANDLE_DATA pPerHandle = (PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); pPerHandle->s = sNew; memcpy(&pPerHandle->addr, &saRemote, nRemoteLen); pPerHandle->nOperationType = OP_READ; ::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (ULONG_PTR)pPerHandle, 0); // 投递一个接收请求 OVERLAPPED *pol = (OVERLAPPED *)::GlobalAlloc(GPTR, sizeof(OVERLAPPED)); WSABUF buf; buf.buf = pPerHandle->buf; buf.len = BUFFER_SIZE; DWORD dwRecv; DWORD dwFlags = 0; ::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, pol, NULL); }}

Page 95: Winsock I/O 方法

95

本章小结Socket编程有阻塞和非阻塞两种,在操作系统 I/O 实现时又有几种 I/O 模型,包括 Select , WSAAsyncSelect , WSAEventSelect , IO 重叠模型,完成端口等。要学习基本的网络编程方法,可以选择从阻塞模式开始,而要开发真正实用的程序,就要进行非阻塞模式的编程(很难想象一个大型服务器采用阻塞模式进行网络通信)。在选择 I/O 模型时,建议初学者可以从 WSAAsyncSelect 模型开始,因为它比较简单,而且有一定的实用性。IOCP 完成端口是 Windows 下性能最好的 I/O 模型,同时它也是最复杂的内核对象。 主要用于网络通信方面,例如:大型 MMO游戏、大型 IM (即时通讯、实时传讯)系统、 网吧管理系统、企业管理系统等等具有大量并发用户请求的场合。

Page 96: Winsock I/O 方法

96

本章小结1. 客户机的开发 建议采用重叠 I/O 或 WSAEventSelect 模型,以便在

一定程度上提升性能。以Windows 为基础的应用程序,要进行窗口消息的管理, WSAAsyncSelect 模型是一种较好的选择,因为 WSAAsyncSelect本身便是从 Windows 消息模型借鉴来的。若采用这种模型,我们的程序一开始便具备了处理消息的能力。

2. 服务器的开发 若开发的是一个服务器应用,要在一个给定的时间,

同时控制多个套接字,建议采用重叠 I/O 模型。但是,如果预计到自己的服务器在任何给定的时间,都会为大量 I/O请求提供服务,便应考虑使用 I/O 完成端口模型,从而获得更好的性能。

Page 97: Winsock I/O 方法

97

课程设计 2设计一个基于某一种 I/O 方法的 socket (winsock) 通信程序,要求具备下列基本功能:

1. 系统配置界面:能通过界面配置通信双方的 IP地址、端口号

2. 通过界面输入待发送的数据,按“确定”键后发送

3. 通过界面显示收到的数据4. 设置一个退出按钮,按下该按钮后退出程序的运行

Page 98: Winsock I/O 方法

98

看来还得多看些参考书哟!