【网络编程】之十、重叠IO Overlapped IO

winsock2 中引入了重叠I/O(Overlapped I/O)的概念并且要求所有的传输协议提供者都支持这一功能。  他的功能高于前面我们提过的三种,但是最强悍的还是我们后面要说的完成端口。

基本原理:让应用程序使用一个重叠的数据结构,一次投递一个活多个winsock I/O请求,针对那些提交的清酒,在他们完成之后,应用程序可为他们提供服务。  应用程序可通过ReadFile和WriteFile两个函数执行I/O操作。 要注意:重叠I/O仅能在由WSASocket函数打开的套接字上使用。要想在一个套接字上使用重叠I/O,首先必须使用 WSA_FLAG_OVERLAPPED 这个标志。

SOCKET s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
当你创建套接字的时候,你使用socket函数,她会默认设置WSA_FLAG_OVERLAPPED 标志。


ok,现在我们已经成功的建立了一个套接字,同时将其他与一个本地接口绑定到一起后就可以开始进行重叠I/O操作了。在这里,我们以前用的send,recv等函数将被改变成为WSASend,WSARecv。

来看一下这些函数:

 WSASend             // sends data on a connected socket. 提供一个指向已填充的数据缓冲区的指针。
 WSASendTo        // sends data to a specific destination, using overlapped I/O where applicable.
 WSARecv        // receives data from a connected socket.
 WSARecvFrom      //receives data on a socket and stores the source address.  提供存放接收数据的缓冲区
 WSAIoctl      //allows for miscellaneous control of a socket.   还可以使用重叠I/O操作的延迟完成特性。
 AcceptEx   //accepts a new connection, returns the local and remote address, and receives the first block of data sent by the client application.
 TrnasmitFile   //transmits file data over a connected socket handle. uses the operating system's cache manager to retrieve the file data, and provides high-performance file data transfer over sockets.


这里面WS_IO_PENDING 是最常见的返回值,这是说明我们的重叠函数调用成功了,但是I/O操作还没有完成。



如果和一个WSAOVERLAPPED结构一起来调用这些函数,那么函数会立即完成并返回,无论套接字是否是阻塞模式。判断I/O请求是否成功的方法有两个,分别是:

1、等待   事件对象通知。

2、通过 完成例程。


我们谈第一个,事件通知:

在重叠函数的参数中都有一个参数是:Overlapped,我们可以假设是把我们的WSARecv这样的操作“绑定”到这个重叠结构上,提交一个请求,而不是将错做立即完成,其他的事情交给重叠结构去做,而其中的重叠结构又要与windows的事件对象“绑定”到一起,这样我们调用玩WSARecv以后就OK了,等到重叠操作完成后自然会有与之对应的时间来通知我们操作完成了,然后我们就可以来根据重叠操作的结果取得我们想要的数据了。


重叠I/O的事件通知方法要求将win32事件对象与WSOVERLAPPED结构关联在一起,当I/O操作完成后,时间的状态会变成“已传信”状态,也就是激发状态。

来看一下WSAOVERLAPPED结构:

typedef struct _WSAOVERLAPPED {
    DWORD    Internal;
    DWORD    InternalHigh;
    DWORD    Offset;
    DWORD    OffsetHigh;//上面参数都是由系统在内部使用,不是由应用程序直接进行处理或使用。
    WSAEVENT hEvent;//允许应用程序将一个事件对象句柄同一个套接字关联起来。
} WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;

typedef struct _WSAOVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
     struct {
       DWORD Offset;
       DWORD OffsetHigh;
     };    PVOID Pointer;
  };
  HANDLE hEvent;
} WSAOVERLAPPED,  *LPWSAOVERLAPPED;

当重叠I/O请求完成后,应用程序要负责取回重叠I/O操作的结果,一个重叠请求操作最终完成后,在事件通知方法中,winsock会更改与一个WSAOVERLAPPED结构对应的一个事件对象的事件传信状态, 将它从“未传信”变为“已传信”。    由于一个事件对象已分配给WSAOVERLAPPED结构,所以只需要简单滴调用WSAWaitForMultipleEvents函数,从而判断出一个重叠I/O调用在什么时候完成。


还有一个函数是取得重叠结构,他是WSAGetOverlappedResult函数,他是在发现一次重叠请求完成后才可以执行。用来判断这个重叠调用到底是成功还是失败。

BOOL WSAAPI WSAGetOverlappedResult(
  __in          SOCKET s,//指定在重叠操作开始的时候,与之对应的那个套接字
  __in          LPWSAOVERLAPPED lpOverlapped,//一个指针,对应于在重叠操作开始时。
  __out         LPDWORD lpcbTransfer,//一个指针,对应一个DWORD变量,负责接收一次重叠发送/接收操作时机传输的字节数。
  __in          BOOL fWait,//用于决定函数是否应该等待一次待解决的重叠操作完成。TRUE:除非操作完成,否则函数不会返回。 FALSE:并且函数处于待解决状态,那么函数返回FALSE,同时返回WSAIOINCOMPLETE错误。   目前,这个参数无论设置什么都没有任何效果。
  __out         LPDWORD lpdwFlags//一个指针,指向DWORD负责接收结果标志。
);
当函数调用成功那么就会返回TRUE, 表示重叠操作成功,并且lpcbTransfer参数指向的值已进行了更新。  

如果反回了FALSE,那么可能是由于某种原因造成了错误:

1、重叠I/O已经完成,但是又错误;

2、重叠I/O操作仍处于待解决状态;

3、重叠操作的完成状态不能判断,因为函数的参数存在错误。

当函数失败后,lpcbTransfer参数指向的值不会进行更新,而且我们的应用程序应调用WSAGerLastError函数来调查到底是什么原因造成的。


OK,下面来看一下重叠I/O的编程步骤:

1、创建套接字,开始在指定的端口上监听连接请求。

SOCKET         ListenSocket,             // 监听套接字
AcceptSocket;             // 与客户端通信的套接字
WSAOVERLAPPED  AcceptOverlapped;     // 重叠结构一个
WSABUF     DataBuf[DATA_BUFSIZE] ;       
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2),&wsaData);

ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  //创建TCP套接字

SOCKADDR_IN ServerAddr;                           //分配端口及协议族并绑定
ServerAddr.sin_family=AF_INET;                                
ServerAddr.sin_addr.S_un.S_addr  =htonl(INADDR_ANY);          
ServerAddr.sin_port=htons(8888);

bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字

listen(ListenSocket, 5);               

2、接受一个客户端进入的连接请求。

AcceptSocket = accept (ListenSocket, NULL,NULL) ; 
当然,这里是我偷懒,如果想要获得连入客户端的信息(记得论坛上也常有人问到),accept的后两个参数就不要用NULL,而是这样
SOCKADDR_IN ClientAddr;                   // 定义一个客户端得地址结构作为参数
int addr_length=sizeof(ClientAddr);
AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);
// 于是乎,我们就可以轻松得知连入客户端的信息了
LPCTSTR lpIP =  inet_ntoa(ClientAddr.sin_addr);      // IP
UINT nPort = ClientAddr.sin_port;                      // Port

3、为接受的套接字创建一个WSAOVERLAPPED结构,并给这个结构分配一个事件对象句柄,同时将该事件对象句柄分配给一个事件数组,以便稍后WSAWaitForMultipleEvents函数使用。

WSAEVENT  EventArray[WSA_MAXIMUM_WAIT_EVENTS];  
DWORD     dwEventTotal = 0,            // 程序中事件的总数
dwRecvBytes = 0,            // 接收到的字符长度
Flags = 0;                    // WSARecv的参数

#define DATA_BUFSIZE     4096          // 接收缓冲区大小

// 创建一个事件
// dwEventTotal可以暂时先作为Event数组的索引
EventArray[dwEventTotal] = WSACreateEvent();      

ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));      // 置零
AcceptOverlapped.hEvent = EventArray[dwEventTotal];            // 关联事件

char buffer[DATA_BUFSIZE];
ZeroMemory(buffer, DATA_BUFSIZE);
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;                          // 初始化一个WSABUF结构
dwEventTotal ++;                              // 总数加一

4、在套接字上投递一个异步WSARecv请求,指定参数为WSAOVERLAPPED结构,注意函数通常会以失败告终,返回SOCKET_ERROR错误状态WSA_IO_PENDING(I/O操作没有完成)。

if(WSARecv(AcceptSocket ,&DataBuf,1,&dwRecvBytes,&Flags,
& AcceptOverlapped, NULL) == SOCKET_ERROR)
{ 
// 返回WSA_IO_PENDING是正常情况,表示IO操作正在进行,不能立即完成
// 如果不是WSA_IO_PENDING错误,就大事不好了~~~~~~!!!
if(WSAGetLastError() != WSA_IO_PENDING)    
{
// 那就只能关闭大吉了
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwEventTotal]);
}
}

5、使用步骤3的事件数组,调用WSAWaitForMultipleEvents函数,并等待与重叠调用关联在一起的事件进入“已传信”状态。

6、WSAWaitForMultipleEvents函数返回后,针对“已传信”状态的事件,调用WSAResultEvent函数来重置事件,从而重设事件对象,并对完成的重叠请求进行处理。

WSAResetEvent(EventArray[dwIndex]);

DWORD dwIndex;
// 等候重叠I/O调用结束
// 因为我们把事件和Overlapped绑定在一起,重叠操作完成后我们会接到事件通知
dwIndex = WSAWaitForMultipleEvents(dwEventTotal, 
EventArray ,FALSE ,WSA_INFINITE,FALSE);
// 注意这里返回的Index并非是事件在数组里的Index,而是需要减去WSA_WAIT_EVENT_0
dwIndex = dwIndex – WSA_WAIT_EVENT_0;



7、使用WSAGetOverlappedResult函数来判断重叠调用的返回状态是什么。

DWORD dwBytesTransferred;
WSAGetOverlappedResult( AcceptSocket, AcceptOverlapped ,
&dwBytesTransferred, FALSE, &Flags);
// 先检查通信对方是否已经关闭连接
// 如果==0则表示连接已经,则关闭套接字
if(dwBytesTransferred == 0)
{
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwIndex]);    // 关闭事件
return;
}


8、在套接字上投递另一个重叠WSARecv请求。

9、重复5到8。


在上文中我们提到一个AcceptEx函数,这个函数是在重叠I/O模型中允许以一种重叠方式,事件对客户端连接。他位于Mswsock.h头文件以及Mswsock.lib库文件中。

这个函数和accept的区别是:我们必须提供接受的套接字,而不是让函数自动为我们创建。

BOOL AcceptEx(
  __in          SOCKET sListenSocket,//一个监听套接字
  __in          SOCKET sAcceptSocket,//指定另一个套接字,负责对进入连接请求的“接受”
  __in          PVOID lpOutputBuffer,//指定一个特殊的缓冲区,因为要负责三种数据的接收,服务器的本地地址,客户端的远程地址和新建连接上发送的第一个数据块。
  __in          DWORD dwReceiveDataLength,//以字节为单位,指定在lpOutputBuffer缓冲区中,保留多大的空间来接收数据。  如果传0,那么就不会再接收任何数据了。
  __in          DWORD dwLocalAddressLength,//
  __in          DWORD dwRemoteAddressLength,//和上一个参数以字节为单位指定在lpOutputBuffer缓冲区中,保留多大空间,在一个套接字被接受的时候,用于本地和远程地址信息的保存
  __out         LPDWORD lpdwBytesReceived,//用于返回接收到的实际数据量,以字节为单位。只有在以同步方式完成的前提下,才会设置这个参数。
  __in          LPOVERLAPPED lpOverlapped//对应的是一个OVERLAPPED结构,允许AcceptEx以一种异步方式工作。 前面说过,只有在一个重叠I/O应用中,这个函数才需要使用事件对象通知机制。
);
要知道AcceptEx函数只能由这里给大家说的“事件通知”方式获取异步I/O请求的结果,在"完成例程”中是无法使用的。


下面给出win32重叠io,来读取文件:

char buf[512*10000];
int readdata(void)
{
	BOOL bRet;   //返回值
	HANDLE hFile;   //文件指针
	DWORD numRead;   //读取数据长度
	OVERLAPPED overlapped;   //I/O结构
	
	hFile = CreateFile("..\\se.zip", GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 
	                   FILE_FLAG_OVERLAPPEN, NULL);
	
	if (INVALID_HANDLE_VALUE == hFile)
	{
		return -1;
	}
	
	memset(buf, 0, sizeof(char) * 512*10000);   //初始化缓冲区
	memset(&overlapped, 0, sizeof(overlapped));   //初始化overlapped
	overlapped.Offset = 0;    //读文件的位置
	
	bRet = ReadFile(hFile, buf, 512*10000, &numRead, &overlapped);
	
	if(TRUE == bRet)  //读取数据成功
	{
		//读取数据完毕
	}
	else  //重叠io操作
	{
		if (ERROR_IO_PENDING == GetLastError())  //读取操作等待中
		{
			WaitForSingleObject(hFile, INFINITE);   //等待文件句柄被激活
			//读取结果
			bRet = GetOverlappedResult(hFile, &overlapped, &numRead, TRUE);
			if(TRUE == bRet)
			{
			   //读取数据完毕
			   //处理数据
			}
			else
			{
			//处理错误
			}
		}
		else
		{
		//处理错误
		}
	}
	CloseHandle(hFile);
}

}



下面将讲述完成端口,那个更加高效,但是也更加困难;

2012/9/2

jofranks 于南昌


原文地址:https://www.cnblogs.com/java20130723/p/3211403.html