windows网络模型之重叠IO的使用

时间:2023-03-08 17:17:46

大部分内容转载自https://blog.****.net/piggyxp/article/details/114883

目录:

1. 重叠模型的优点

2. 重叠模型的基本原理

3. 关于重叠模型的基础知识

4. 重叠模型的实现步骤

一. 重叠模型的优点

1.      可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。

2.      比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。

因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。

  而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。

3.      从《windows网络编程》中提供的试验结果中可以看到,在使用了P4 1.7G Xero处理器(CPU很强啊)以及768MB的回应服务器中,最大可以处理4万多个SOCKET连接,在处理1万2千个连接的时候CPU占用率才40% 左右 ―― 非常好的性能,已经直逼完成端口了^_^

二.  重叠模型的基本原理

说了这么多的好处,你一定也跃跃欲试了吧,不过我们还是要先提一下重叠模型的基本原理。

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

需要注意的是,有两个方法可以用来管理重叠IO请求的完成情况(就是说接到重叠操作完成的通知):

1.      事件对象通知(event object notification)

2.      完成例程(completion routines) ,注意,这里并不是完成端口

而本文只是讲述如何来使用事件通知的的方法实现重叠IO模型,完成例程的方法准备放到下一篇讲 :) (内容太多了,一篇写不完啊) ,如没有特殊说明,本文的重叠模型默认就是指的基于事件通知的重叠模型。

既然是基于事件通知,就要求将Windows事件对象与WSAOVERLAPPED结构关联在一起(WSAOVERLAPPED结构中专门有对应的参数),通俗一点讲,就是。。。。对了,忘了说了,既然要使用重叠结构,我们常用的send, sendto, recv, recvfrom也都要被WSASend, WSASendto, WSARecv, WSARecvFrom替换掉了, 它们的用法我后面会讲到,这里只需要注意一点,它们的参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作“绑定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完WSARecv以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要德数据了。

三.关于重叠模型的基础知识

下面来介绍并举例说明一下编写重叠模型的程序中将会使用到的几个关键函数。

1.      WSAOVERLAPPED结构

这个结构自然是重叠模型里的核心,它是这么定义的

struct _WSAOVERLAPPED {

  DWORD Internal;

  DWORD InternalHigh;

  DWORD Offset;

  DWORD OffsetHigh;

  WSAEVENT hEvent;      // 唯一需要关注的参数,用来关联    WSAEvent对象
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

  我们需要把WSARecv等操作投递到一个重叠结构上,而我们又需要一个与重叠结构“绑定”在一起的事件对象来通知我们操作的完成,看到了和hEvent参数,不用我说你们也该知道如何来来把事件对象绑定到重叠结构上吧?大致如下:

WSAEVENT event;                   // 定义事件
WSAOVERLAPPED AcceptOverlapped ; // 定义重叠结构
event = WSACreateEvent(); // 建立一个事件对象句柄
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 初始化重叠结构
AcceptOverlapped.hEvent = event; // Done !!

2.  WSARecv系列函数

            int WSARecv(
SOCKET s, // 当然是投递这个操作的套接字
LPWSABUF lpBuffers, // 接收缓冲区,与Recv函数不同
// 这里需要一个由WSABUF结构构成的数组
DWORD dwBufferCount, // 数组中WSABUF结构的数量
LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回函数调用
// 所接收到的字节数
LPDWORD lpFlags, // 说来话长了,我们这里设置为0 即可
LPWSAOVERLAPPED lpOverlapped, // “绑定”的重叠结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
// 完成例程中将会用到的参数,我们这里设置为 NULL
);

  

返回值:
WSA_IO_PENDING : 最常见的返回值,这是说明我们的WSARecv操作成功了,但是I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成

3.      WSAWaitForMultipleEvents函数

这个函数与线程中常用的WaitForMultipleObjects函数有些地方还是比较像的,因为都是在等待某个事件的触发嘛。因为我们需要事件来通知我们重叠操作的完成,所以自然需要这个等待事件的函数与之配套。

                        DWORD WSAWaitForMultipleEvents(
DWORD cEvents, // 等候事件的总数量
const WSAEVENT* lphEvents, // 事件数组的指针
BOOL fWaitAll, // 这个要多说两句:
// 如果设置为 TRUE,则事件数组中所有事件被传信的时候函数才会返回
// FALSE则任何一个事件被传信函数都要返回
// 我们这里肯定是要设置为FALSE的
DWORD dwTimeout, // 超时时间,如果超时,函数会返回 WSA_WAIT_TIMEOUT
// 如果设置为0,函数会立即返回
// 如果设置为 WSA_INFINITE只有在某一个事件被传信后才会返回
// 在这里不建议设置为WSA_INFINITE,因为。。。后面再讲吧..-_-b
BOOL fAlertable // 在完成例程中会用到这个参数,这里我们先设置为FALSE
);
返回值:
WSA_WAIT_TIMEOUT :最常见的返回值,我们需要做的就是继续Wait
WSA_WAIT_FAILED : 出现了错误,请检查cEvents和lphEvents两个参数是否有效
如果事件数组中有某一个事件被传信了,函数会返回这个事件的索引值,但是这个索引值需要减去预定义值 WSA_WAIT_EVENT_0才是这个事件在事件数组中的位置。
WSAWaitForMultipleEvents函数只能支持由WSA_MAXIMUM_WAIT_EVENTS对象定义的一个最大值,是 64,就是说WSAWaitForMultipleEvents只能等待64个事件,
如果想同时等待多于64个事件,就要 创建额外的工作者线程,就不得不去管理一个线程池,这一点就不如下一篇要讲到的完成例程模型了。

4.      WSAGetOverlappedResult函数


既然我们可以通过WSAWaitForMultipleEvents函数来得到重叠操作完成的通知,那么我们自然也需要一个函数来查询一下重叠操作的结果,定义如下

 BOOL WSAGetOverlappedResult(
SOCKET s, // SOCKET,不用说了
LPWSAOVERLAPPED lpOverlapped, // 这里是我们想要查询结果的那个重叠结构的指针
LPDWORD lpcbTransfer, // 本次重叠操作的实际接收(或发送)的字节数
BOOL fWait, // 设置为TRUE,除非重叠操作完成,否则函数不会返回
// 设置FALSE,而且操作仍处于挂起状态,那么函数就会返回FALSE
// 错误为WSA_IO_INCOMPLETE
// 不过因为我们是等待事件传信来通知我们操作完成,所以我们这里设
// 置成什么都没有作用…..-_-b 别仍鸡蛋啊,我也想说得清楚一些…
LPDWORD lpdwFlags // 指向DWORD的指针,负责接收结果标志
);
需要注意一下的就是如果WSAGetOverlappedResult完成以后,第三个参数返回是 0 ,则说明通信对方已经关闭连接,我们这边的SOCKET, Event之类的也就可以关闭了。
下面是一个完整的代码例子,关于使用两个线程的问题.

一个用来循环监听端口,接收请求的连接,然后给在这个套接字上配合一个WSAOVERLAPPED结构投递第一个WSARecv请求,然后进入第二个线程中等待操作完成。

第二个线程用来不停的对WSAEVENT数组WSAWaitForMultipleEvents,等待任何一个重叠操作的完成,然后根据返回的索引值进行处理,处理完毕以后投递WSARecv请求继续接受这个套接字信息。

#include "tchar.h"
#include <winsock2.h>
#include <Windows.h> #pragma comment(lib,"ws2_32.lib")
#define PORT 6000
int main(int argc, char * argv[])
{
//初始化网络环境
WSADATA wsa;
if (WSAStartup(MAKEWORD(, ), &wsa) != )
{
printf("WSAStartup failed\n");
return -;
}
// 初始化完成,创建一个TCP的socket
SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sServer == INVALID_SOCKET)
{
printf("socket failed\n");
return -;
}
//指定连接的服务端信息
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(PORT);
//客户端只需要连接指定的服务器地址,127.0.0.1是本机的回环地址
addrServ.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 服务器Bind 客户端是进行连接
int ret = connect(sServer, (SOCKADDR*)&addrServ, sizeof(SOCKADDR));//开始连接
if (SOCKET_ERROR == ret)
{
printf("socket connect failed\n");
WSACleanup();
closesocket(sServer);
return -;
}
//连接成功后,就可以进行通信了
char szBuf[];
memset(szBuf, , sizeof(szBuf));
sprintf_s(szBuf, sizeof(szBuf), "Hello server");
//当服务端是recv的时候,客户端就需要send,若两端同时进行收发则会卡在这里,因为recv和send是阻塞的
ret = send(sServer, szBuf, strlen(szBuf), );
if (SOCKET_ERROR == ret)
{
printf("socket send failed\n");
closesocket(sServer);
return -;
} ret = recv(sServer, szBuf, sizeof(szBuf), );
if (SOCKET_ERROR == ret)
{
printf("socket recv failed\n");
closesocket(sServer);
return -;
}
printf("%s\n", szBuf);
closesocket(sServer);
WSACleanup();
return ;
}