socket的IO模型

在设计网络通信程序时,需要通过一种机制来确定网络中某些事件的发生。例如,当主机A向主机B发送数据时,在主机B接收到数据时需要让应用程序读取数据,那么应用程序何时读取数据呢?也就是说,应用程序如何确定网络中何时有数据需要接收呢?这就需要在设计网络应用程序时选择一个I/O模型。在Windows操作系统中,I/O模型主要有6种,下面分别介绍。

1.Select模型

Select模型是套接字中一种常见的I/O模型,通过使用select函数来确定套接字的状态。在网络应用程序
中,通过一个线程来设计一个循环,不停地调用select函数,判断套接字上是否存在数据,或者是否能够向
套接字写入数据等。
Select模型就像是用户等待邮件的到来,用户无法预先确定邮件何时到来,只能每隔一段时间查看一下邮箱,看看是否有新邮件。
Select函数实现模式的选择。
语法:
int select (int nfds,                       //无实际意义,只是为了和UNIX下的套接字兼容

fd_set FAR * readfds,                 //表示一组被检查可读的套接字

fd_set FAR * writefds,                  //表示一组被检查可写的套接字

fd_set FAR * exceptfds,                //被检查有错误的套接字

const struct timeval FAR * timeout  //表示函数的等待时间

);

返回值:如果函数调用成功,在readfds、writefds或exceptfds参数中将存储满足条件的套接字元素,并且函数返回值为满足条件的套接字数量;如果函数调用超出了timeout设置的时间,返回值为0;如果函数调用失败,返回值为SOCKET_ERROR。
为了方便用户对fd_set类型的参数进行操作,Visual C++ 提供了4个宏,分别介绍如下:
 FD_CLR(s, *set):从集合set中删除套接字s。
 FD_ISSET(s,*set):判断套接字s是否为集合set中的一员。如果是,返回值为非零,否则为零。
 FD_SET(s,*set):向集合中添加套接字s。
 FD_ZERO(*set):将集合set初始化为NULL。

下面通过一段代码来判断套接字上是否有数据可读。

fd_set fdRead; //定义一个fd_set对象
FD_ZERO(&fdRead); //初始化fdRead
FD_SET(clientSock, &fdRead); //将套接字clientSock添加到fdRead集合中
if (select(0, &fdRead, NULL, NULL, NULL) > 0) //调用select函数
{
//如果select函数调用成功,则判断clientSock是否仍为fdRead中的一员
//如果是,则表明clientSock可读
if (FD_ISSET(clientSock, &fdRead))
{
//从套接字中读取数据
}
}

2.WSAAsyncSelect模型

WSAAsyncSelect模型是Windows系统提供的一种基于消息的网络事件通知模型。当网络中有事件发生时,用户发出了连接请求,则应用程序中指定的窗口将收到一个消息,用户可以通过消息处理函数来对网络中的事件进行处理,例如接受客户的连接请求、接收套接字中的数据等。WSAAsyncSelect模型类似于邮箱中的通知消息,当邮箱中有新邮件时,会提示用户有新邮件了,这样
用户就不必定时查看邮箱了。
WSAAsyncSelect函数用来设置网络事件通知模型。
语法:
int WSAAsyncSelect (SOCKET s,//表示套接字

                                HWND hWnd,//表示接收消息的窗口句柄

                                unsigned int wMsg,//表示窗口接收来自套接字中的消息

                                long lEvent//表示用户感兴趣的网络事件集合

                                );


网络事件               事 件 类 型 事 件 描 述
FD_READ         套接字中有数据读取时发送消息
FD_WRITE       当输出缓冲区可用时发出消息
FD_OOB          套接字中有外带数据读取时发送消息

FD_ACCEPT    有连接请求时发出消息

FD_CONNECT 当连接完成后发出消息
FD_CLOSE      套接字关闭时发出消息


FD_WRITE事件通常会在以下情况下发生:
1.当客户端连接成功后会收到FD_WRITE事件。
2.如果当前套接字发送缓冲区已满,在发送操作完成之后,发送缓冲区有可用空间时将触发FD_WRITE事件,通知用户当前可以进行    写操作。


下面通过一段代码来描述WSAAsyncSelect模型。
(1)自定义一个消息,代码如下:
#define WM_SOCKET WM_USER + 20
(2)添加一个消息处理函数,用于处理网络中的事件,代码如下:
LRESULT CDialogDlg::OnSocket(WPARAM wParam, LPARAM lParam)
{
int nEvent = WSAGETSELECTEVENT (lParam); //读取网络事件
int nError = WSAGETSELECTERROR (lParam); //读取错误代码
switch (nEvent)
{
case FD_CONNECT:
{
TRACE("连接完成!");
break;
}
}
return 0;
}
(3)添加消息映射宏,将自定义消息与消息处理函数OnSocket关联,代码如下:
ON_MESSAGE(WM_SOCKET, OnSocket)
(4)调用WSAAsyncSelect函数设置套接字WSAAsyncSelect模型,代码如下:
int nRet = WSAAsyncSelect(clientSock, m_hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CONNECT);
if (nRet != 0)
{
TRACE("设置WSAAsyncSelect模型失败");
}
这样,当网络中有FD_READ、FD_WRITE或FD_CONNECT事件发生时将向窗口发送WM_SOCKET消
息,进而调用OnSocket方法。


3.WSAEventSelect模型

WSAEventSelect模型与WSAAsyncSelect模型有些类似,只是它是以事件对象为基础描述网络事件的发生。在使用WSAEventSelect模型时,首先需要使用WSACreateEvent函数创建一个事件对象。该函数的语法如下:
WSAEVENT WSACreateEvent (void);
返回值:如果函数调用成功,返回值表示一个人工重置事件对象,初始状态为无信号状态;如果函数调用失败,返回值为NULL。
在创建完事件对象后,需要调用WSAEventSelect函数将事件对象与套接字关联在一起,并注册感兴趣的网络事件。
WSAEventSelect函数用于实现将事件对象与套接字关联在一起。
语法:
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
s:表示套接字。
hEventObject:表示事件对象。
lNetworkEvents:表示注册的网络事件。
这样,当网络中的套接字有事件发生时,与其关联的事件对象将变为有信号状态。
此外,在程序中还需要使用WSAWaitForMultipleEvents函数等待网络中触发网络事件的事件对象。
WSAWaitForMultipleEvents函数用于实现等待网络事件的发生。
语法:
DWORD WSAWaitForMultipleEvents(

DWORD cEvents, //表示事件对象数组lphEvents的元素数量,最大值为WSA_MAXIMUM_WAIT_EVENTS,即64

const WSAEVENT* lphEvents,//表示用于检测的事件对象数组
BOOL fWaitAll, //为TRUE,则lphEvents数组中的所有元素有信号时返回;为FALSE,则数组中的任意一个事件对象有信号时返回

DWORD dwTimeout, //用于设置超时时间,单位是毫秒。如果为0,则函数立即返回;为WSA_INFINITE,则函数从不超时

BOOL fAlertable//表示当系统将一个I/O完成例程放入队列中以供执行时函数是否返回。为TRUE,则函数返回且执行完成例程;为                            //FALSE,函数不返回,不执行完成例程

                                                      );


返 回 值 : 造成函数返回的事件对象。为了获取事件对象对应的套接字, 需要将返回值减去WSA_WAIT_EVENT_0以获得事件对象在lphEvents数组中的索引值。如果函数执行失败,返回值为WSA_WAIT_FAILED。
在获取了网络中触发事件的套接字和事件对象后,应用程序中还需要判别网络事件的类型,这需要使用WSAEnumNetworkEvents函数来实现。
WSAEnumNetworkEvents函数实现判别网络事件的类型。
语法:
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents);
 s:网络套接字。
 hEventObject:一个可选项,可以为NULL,如果不为NULL,则表示一个事件对象,函数执行后会将事件对象设置为无信号状态。
 lpNetworkEvents:一个WSANETWORKEVENTS结构数组,WSANETWORKEVENTS结构记录了套接字的网络事件和错误代码。    其中,lNetworkEvents用于表示网络中发生的所有事件;iErrorCode表示一个错误代码数组,同lNetworkEvents表示的各个事件关      联。例如,如果lNetworkEvents中包含有FD_READ事件类型,则该事件的错误代码在iErrorCode数组中的索引位置为                   FD_READ_BIT,即在网络事件后添加“_BIT”作为后缀以表示事件的错误代码在数组中的索引。
该结构的定义如下:

typedef struct _WSANETWORKEVENTS
{
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

下面介绍一下WSAEventSelect模型的使用方法。
(1)创建一个事件对象,代码如下:
SOCKET sockList[WSA_MAXIMUM_WAIT_EVENTS];
sockList[0] = clientSock;
int nCount = 1;
sockaddr_in Addr;
int nAddrSize = 0;
BOOL m_bTerminated = FALSE;
HANDLE hEvent = WSACreateEvent();
HANDLE EventList[WSA_MAXIMUM_WAIT_EVENTS];
EventList[0] = hEvent;


(2)将事件对象与套接字关联在一起,代码如下:

WSAEventSelect(clientSock, EventList[0], FD_READ | FD_CLOSE);
( 3 ) 在单独的线程中以循环的方式调用WSAWaitForMultipleEvents 函数等待网络事件, 调用WSAEnumNetworkEvents获取网络事件,代码如下:

while (!m_bTerminated)
{
	DWORD dwIndex = WSAWaitForMultipleEvents(nCount, &EventList[0], FALSE, WSA_INFINITE, FALSE);
	WSANETWORKEVENTS wsaEvents;
	memset(&wsaEvents, 0, sizeof(WSANETWORKEVENTS));
	WSAEnumNetworkEvents(sockList[dwIndex - WSA_WAIT_EVENT_0], EventList[dwIndex - WSA_WAIT_EVENT_0],
		&wsaEvents);
	if (wsaEvents.lNetworkEvents & FD_READ)
	{
		if (wsaEvents.iErrorCode[FD_READ_BIT] == 0) //有数据接收
		{
			char *pBuffer = new char[1024];
			memset(pBuffer, 0, 1024);
			recv(clientSock, pBuffer, 1024, 0); //接收数据
			//进行其他处理
			delete[] pBuffer;
		}
	}
	else if (wsaEvents.lNetworkEvents & FD_ACCEPT) //连接请求
	{
		if (wsaEvents.iErrorCode[FD_ACCEPT_BIT] == 0) //接受连接
		{
			SOCKET sock = accept(clientSock, (sockaddr*)&Addr, &nAddrSize);
			if (nCount > WSA_MAXIMUM_WAIT_EVENTS)
			{
				closesocket(sock);
				continue;
			}
			hEvent = WSACreateEvent();
			EventList[nCount] = hEvent;
			sockList[nCount] = sock;
			WSAEventSelect(sockList[nCount], EventList[nCount], FD_READ | FD_ACCEPT);
			nCount++;
			//...
		}
	}
}



4.Overlapped I/O 事件通知模型

与前3个模型相比,Overlapped模型可以使应用程序达到最佳的性能。这是因为在Overlapped模型中,只
要用户提供了一个数据缓冲区,当套接字中有数据到达时系统就会将数据直接写入该缓冲区中。而在前面
的几个模型中,当套接字中有数据到达时,系统会将数据复制到接收缓冲区中,然后通知应用程序有数据
达到,用户再使用接收函数将数据读取到自己的缓冲区中。由此可见,Overlapped模型效率更高一些。
Overlapped模型的主要原理就是使用一个重叠的数据结构WSAOVERLAPPED,一次投递一个或多个I/O请
求。针对这些提交的请求,在它们完成之后,应用程序会收到通知。这样,在应用程序中就可以处理数据了。
有两种方式可以实现Overlapped模型,即使用事件通知和使用完成例程。下面来讨论Overlapped I/O 事
件通知模型。
由于Overlapped模型需要使用WSAOVERLAPPED结构,因此在使用套接字函数时需要采用WSARecv、
WSASend 之类的函数代替recv 、send 函数。这些函数有一个共同的特征, 就是参数中需要一个
WSAOVERLAPPED结构指针作为参数,而WSAOVERLAPPED结构与Overlapped模型的关系非常紧密。该
结构定义如下:
typedef struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
其中,Internal、InternalHigh 、Offset和OffsetHigh是系统保留的,我们只要关心hEvent成员就可以了。
该成员是一个事件对象。当应用程序需要接收或发送数据时,需要使用WSARecv或WSASend函数将该重叠
操作(发送或接收数据)绑定到WSAOVERLAPPED结构上。这样,当操作完成时,WSAOVERLAPPED结
构中的hEvent事件对象将处于有信号状态(利用WaitForMultipleObjects函数等待事件变为有信号状态,表示
重叠操作已完成)。为了获得重叠操作的状态,程序中需要调用WSAGetOverlappedResult函数。
语法:

BOOL WSAGetOverlappedResult( SOCKET s, LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags)

s 表示套接字
lpOverlapped 表示关联重叠操作的数据结构
lpcbTransfer 表示发送或接收数据的字节数
fWait 表示函数是否等待正在进行的重叠操作完成后才返回。如果为TRUE,表示函数不返回,直到重叠操作完成,为FALSE,函数            立即返回FALSE,错误代码为WSA_IO_INCOMPLETE
lpdwFlags  是一组标记值,如果重叠操作是由WSARecv或WSASend函数引发的,则函数的lpFlags参数值将传
                 递到lpdwFlags中
下面通过代码简要描述Overlapped I/O事件通知模型的实现过程。

(1)定义一组变量,代码如下:

#define BUF_LEN 1024 //接收缓冲区大小
SOCKET mainSock; //本地套接字
SOCKET AcceptSock[BUF_LEN] = {0}; //接收连接的套接字
WSABUF SockBuf[BUF_LEN]; //套接字数据缓冲区
WSAOVERLAPPED Overlapped[BUF_LEN]; //重叠结构
WSAEVENT EventList[WSA_MAXIMUM_WAIT_EVENTS]; //事件对象数组
DWORD dwRecvCount = 0; //接收数据的字节数
int nFlags = 0; // WSARecv的参数
DWORD dwEventCount = 0; //事件对象的数量


(2)创建、绑定并监听套接字,代码如下:

WSADATA wsaData; //定义WSADATA对象
WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化库函数
mainSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
NULL, NULL, WSA_FLAG_OVERLAPPED); //定义本地套接字
SOCKADDR_IN localAddr; //定义套接字地址对象
localAddr.sin_family = AF_INET;
localAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
localAddr.sin_port = htons(8100); //设置端口
bind(mainSock, (LPSOCKADDR)&localAddr, sizeof(localAddr));//绑定地址
listen(mainSock, 15); //监听套接字


(3)设计一个单独的线程函数,实现接受客户端的连接请求,代码如下:
int nIndex = 0;
SOCKADDR_IN remoteAddr;
int nAddrSize = sizeof(remoteAddr);
while (true) //在线程中开始一个循环
{
	if (AcceptSock[nIndex] == 0) //当前套接字元素没有被使用
	{
		//接受客户端连接
		AcceptSock[nIndex] = accept(mainSock, (SOCKADDR*)&localAddr, &nAddrSize);
		if (AcceptSock[nIndex] != INVALID_SOCKET) //接收连接成功
		{
			EventList[nIndex] = WSACreateEvent(); //创建事件对象
			dwEventCount++; //增加事件计数
			memset(&Overlapped[nIndex], 0, sizeof(WSAOVERLAPPED));
			Overlapped[nIndex].hEvent = EventList[nIndex];
			//开始接收数据,检测网络状态
			char* pBuffer = new char[BUF_LEN]; //分配数据缓冲区
			memset(pBuffer, 0, BUF_LEN);
			SockBuf[nIndex].buf = pBuffer;
			SockBuf[nIndex].len = BUF_LEN;
			int nRet = WSARecv(AcceptSock[nIndex], &SockBuf[nIndex], dwEventCount, &dwRecvCount,
				&nFlags, &Overlapped[nIndex], NULL); //投递一个重叠请求
			if (nRet == SOCKET_ERROR) //发生错误
			{
				int nErrorCode = WSAGetLastError(); //读取错误代码
				if (nErrorCode != WSA_IO_PENDING) //错误未知
				{
					closesocket(AcceptSock[nIndex]); //关闭套接字
					AcceptSock[nIndex] = 0;
					delete[] SockBuf[nIndex].buf; //释放缓冲区
					SockBuf[nIndex].buf = NULL;
					SockBuf[nIndex].len = 0;
					continue;
				}
			}
			nIndex = (nIndex + 1) % WSA_MAXIMUM_WAIT_EVENTS;
		}
	}
}


(4)再设计一个单独的线程函数,实现套接字数据的接收,代码如下:

DWORD WINAPI ReceiveData(LPVOID lpParameter)
{
	int nIndex = 0;
	while (true)
	{
		nIndex = WSAWaitForMultipleEvents(dwEventCount, EventList, FALSE, 1000, FALSE);
		if (nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
			continue;
		nIndex = nIndex - WSA_WAIT_EVENT_0; //计算有信号事件在EventList数组中的索引
		WSAResetEvent(EventList[nIndex]); //恢复事件为无信号状态
		DWORD dwAffectSize;
		//由于之前成功地调用了WSAWaitForMultipleEvents函数获取了套接字关联的事件对象有信号,因此
		//WSAGetOverlappedResult函数的调用通常都会成功;但是如果dwAffectSize参数为0,
		//则表示对方关闭了套接字,此时可以关闭本地对应的套接字
		WSAGetOverlappedResult(AcceptSock[nIndex], &Overlapped[nIndex], &dwAffectSize,
			FALSE, &nFlags); //读取操作结果
		if (dwAffectSize == 0) //对方套接字关闭
		{
			closesocket(AcceptSock[nIndex]); //关闭套接字
			AcceptSock[nIndex] = NULL;
			delete[] SockBuf[nIndex].buf; //释放缓冲区
			SockBuf[nIndex].buf = NULL;
			SockBuf[nIndex].len = 0;
		}
		else //数据也接收,可以直接使用数据
		{
			//...数据存储在DataBuf[nIndex].buf中,用户可以访问之中的数据
			//在数据使用后,重新初始化数据缓冲区
			memset(SockBuf[nIndex].buf, 0, BUF_LEN);
			//开始一个新的重叠请求
			if (WSARecv(AcceptSock[nIndex], &SockBuf[nIndex], dwEventCount, &dwRecvCount,
				&nFlags, &Overlapped[nIndex], NULL) == SOCKET_ERROR) //发生了错误
			{
				if (WSAGetLastError() != WSA_IO_PENDING) //错误代码不是操作正在进行中
				{
					closesocket(AcceptSock[nIndex]); //关闭套接字
					AcceptSock[nIndex] = NULL;
					delete[] SockBuf[nIndex].buf; //释放缓冲区
					SockBuf[nIndex].buf = NULL;
					SockBuf[nIndex].len = 0;
				}
			}
		}
	}
	return 0;
}


5.Overlapped I/O 完成例程模型

Overlapped I/O 完成例程模型与Overlapped I/O 事件通知模型的原理基本相同,但是Overlapped I/O 事件通知模型仅限于一个线程最多管理64个连接(这是由于它采用事件通知的原因,在WSAWaitForMultipleEvents函数中目前最多可以支持64个事件对象,因此只能在线程中最多管理64个连接),而Overlapped I/O 完成例程模型在一个线程中可以管理上千个连接,而且保持较高的性能(因为它不使用事件对象作为网络事件的通知消息,而是以一个完成例程也就是一个函数来响应网络事件的发生)。在Overlapped I/O 完成例程模型中,当网络中有事件发生时,将调用用户指定的一个完成例程。
Overlapped I/O完成例程模型与异步过程调用(Asynchronous Procedure Call, APC)的原理是相同的。我们知道,每个线程关联一个APC队列,当APC队列中有APC函数时,如果线程处于警告等待状态,则线程按先进先出(FIFO)的原则会执行APC队列中的APC函数。在线程中调用WaitForSingleObjectEx、WaitForMultipleObjectEx、SleepEx、SignalObjectAndWait MsgWaitForMultipleObjectEx等函数时,线程将进入警告等待状态。下图描述了异步过程调用的原理。


注意: APC 队列中完成例程的执行是在调用线程中进行的,而不是在额外的线程或线程池中异步执行。
在完成例程执行完毕后,将恢复线程的正常状态。
在Overlapped I/O 完成例程模型中,当异步I/O操作完成后,系统将完成例程放入线程的APC队列,当
线程进入警告等待状态时将调用APC队列中的完成例程。下面介绍Overlapped I/O 完成例程模型在程序中的
实现。
在WSARecv、WSARecvFrom、WSASend等套接字函数中都包含一个表示完成例程的参数。
WSARecv用于实现字符串的接收。
语法:
int WSARecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED
lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
说明:最后一个参数lpCompletionRoutine 就表示一个完成例程,也就是一个函数指针。
语法:
void CALLBACK CompletionRoutine(IN DWORD dwError, //表示重叠操作的状态

IN DWORD cbTransferred,//表示重叠操作实际接收的字节数
IN LPWSAOVERLAPPED lpOverlapped, //表示最初传递到I/O调用时的一个WSAOVERLAPPED结构

IN DWORD dwFlags//表示WSARecv函数中lpFlags参数信息

);

下面简要描述Overlapped I/O 完成例程模型的实现过程。
(1)定义一个数据结构,用于描述提交异步操作的参数信息,代码如下:

struct IO_INFORMATION
{
OVERLAPPED Overlapped; //IO重叠结果
SOCKET Sock; //套接字
WSABUF RecBuf; //数据缓冲区
DWORD dwSendLen; //发送数据长度
DWORD dwRecvLen; //接收数据长度
};

(2)创建、绑定并监听套接字,代码如下:

#define BUF_LEN 1024 //接收缓冲区大小
SOCKET mainSock; //本地套接字
DWORD nFlags = 0; // WSARecv的参数
SOCKET mainSock; //本地套接字
WSADATA wsaData; //定义WSADATA对象
WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化库函数
mainSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
	NULL, NULL, WSA_FLAG_OVERLAPPED); //定义本地套接字
SOCKADDR_IN localAddr; //定义套接字地址对象
localAddr.sin_family = AF_INET;
localAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
localAddr.sin_port = htons(8100); //设置端口
bind(mainSock, (LPSOCKADDR)&localAddr, sizeof(localAddr)); //绑定地址
listen(mainSock, 15); //监听套接字



(3)设计一个完成例程,读取数据,并开始新的重叠操作请求,代码如下:

//定义一个完成例程
void CALLBACK RecvData(IN DWORD dwError, IN DWORD cbTransferred,
	IN LPWSAOVERLAPPED lpOverlapped, IN DWORD dwFlags)
{
	//利用C语言的技巧,IO_INFORMATION结构的第一个成员为OVERLAPPED,
	//在调用WSARecv函数时传递的是&pIOInfo->Overlapped,也就表示IO_INFORMATION结构的首地址
	IO_INFORMATION *pIOInfo = (IO_INFORMATION*)lpOverlapped;
	if (dwError != 0 || cbTransferred == 0) //有错误发生或者对方断开连接
	{
		closesocket(pIOInfo->Sock); //关闭套接字
		delete[] pIOInfo->RecBuf.buf; //释放缓冲区
		delete pIOInfo; //释放IO_INFORMATION对象
	}
	else //读取数据,重新提交重叠操作
	{
		//...读取数据pIOInfo->RecBuf.buf
		//在读取数据后初始化缓冲区
		memset(pIOInfo->RecBuf.buf, 0, pIOInfo->RecBuf.len);
		if (WSARecv(pIOInfo->Sock, &pIOInfo->RecBuf, 1, &pIOInfo->dwRecvLen,
			&nFlags, &pIOInfo->Overlapped, RecvData) == SOCKET_ERROR) //有错误发生
		{
			int nError = WSAGetLastError(); //获取错误代码
			if (nError != WSA_IO_PENDING) //如果没有错误代码,则表示重叠操作正在进行中
			{
				closesocket(pIOInfo->Sock); //关闭套接字
				delete[] pIOInfo->RecBuf.buf; //释放缓冲区
				delete pIOInfo; //释放IO_INFORMATION对象
			}
		}
	}
}



(4)开始一个辅助线程,在线程函数中利用循环接受客户端连接,并提交重叠操作请求,代码如下:

DWORD WINAPI AcceptConnect(LPVOID lpParameter)
{
	SOCKET mainSock; //本地套接字
	WSADATA wsaData; //定义WSADATA对象
	WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化库函数
	mainSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
		NULL, NULL, WSA_FLAG_OVERLAPPED); //定义本地套接字
	SOCKADDR_IN localAddr; //定义套接字地址对象
	localAddr.sin_family = AF_INET;
	localAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	localAddr.sin_port = htons(8100); //设置端口
	bind(mainSock, (LPSOCKADDR)&localAddr, sizeof(localAddr));//绑定地址
	listen(mainSock, 15); //监听套接字
	SOCKADDR_IN remoteAddr;
	int nAddrSize = sizeof(remoteAddr);
	while (true)
	{
		SOCKET clientSock = accept(mainSock, (SOCKADDR*)&remoteAddr, &nAddrSize);
		if (clientSock != INVALID_SOCKET) //accept调用成功
		{
			IO_INFORMATION *pIOInfo = new IO_INFORMATION; //构建一个IO_INFORMATION对象
			memset(pIOInfo, 0, sizeof(IO_INFORMATION)); //对pIOInfo进行初始化
			pIOInfo->Sock = clientSock;
			pIOInfo->RecBuf.len = BUF_LEN;
			char *pBuffer = new char[BUF_LEN]; //创建一个缓冲区
			memset(pBuffer, 0, BUF_LEN); //初始化缓冲区
			pIOInfo->RecBuf.buf = pBuffer;
			if (WSARecv(pIOInfo->Sock, &pIOInfo->RecBuf, 1, &pIOInfo->dwRecvLen,
				&nFlags, &pIOInfo->Overlapped, RecvData) == SOCKET_ERROR) //有错误发生
			{
				int nError = WSAGetLastError(); //获取错误代码
				if (nError != WSA_IO_PENDING) //错误代码表示没有重叠操作正在进行中
				{
					closesocket(pIOInfo->Sock); //关闭套接字
					delete[] pIOInfo->RecBuf.buf; //释放缓冲区
					delete pIOInfo; //释放IO_INFORMATION对象
				}
			}
		}
		SleepEx(1000, TRUE); //延时1000毫秒
	}
	return 0;
}

在上述代码中注意“SleepEx(1000, TRUE);”语句,该语句的作用是延时1000毫秒,延时的目的是为了让内核有机会调用完全例程。与Sleep函数不同的是,SleepEx会在以下几种情况下返回。
(1)I/O完成回调函数被调用。
(2)一个APC(Asynchronous Procedure Call,异步调用过程)被放入线程。
(3)超过指定的时间。
在线程函数中调用SleepEx函数使线程处于一种可警告的等待状态,使得重叠I/O完成操作后内核有机会
调用完成例程。

6.IOCP模型

IOCP模型又称完成端口模型。在介绍IOCP模型之前,先来介绍一下完成端口。完成端口是Windows系统中的一种内核对象,在其内部提供了线程池的管理机制,这样在进行异步重叠IO操作时可以避免反复创建线程的系统开销。
IOCP模型的主要设计思路是创建一个完成端口对象,并将其绑定到套接字上,然后开启几个用户线程。当重叠I/O操作完成时,系统会将I/O完成包放入I/O完成包队列中。这样,用户线程通过调用GetQueuedCompletionStatus函数可以检测队列中的I/O完成包。如果函数成功等待到I/O完成包,会获取完成端口的键值(该键值是在创建完成端口时指定的,通常使用该键值描述一个自定义的数据结构,包含套接字、数据缓冲区、重叠结构的信息)。
下面通过代码来描述IOCP模型的实现过程。
(1)定义一组变量,代码如下:

#define BUF_LEN 1024 //接收缓冲区大小
SOCKET mainSock; //本地套接字
DWORD nFlags = 0; //WSARecv的参数
(2)定义一个枚举类型,用于表示网络事件,代码如下:
enum NetEvent{NE_REC, NE_SEND, NE_POST, NE_CLOSE};
(3)自定义一个I/O重叠操作的数据结构,用于在进行I/O操作时传递数据,代码如下:
typedef struct IO_INFORMATION
{
	OVERLAPPED Overlapped; //IO重叠结果
	SOCKET Sock; //套接字
	char Buffer[BUF_LEN]; //用户数据缓冲区
	WSABUF RecBuf; //数据缓冲区
	DWORD dwSendLen; //发送数据长度
	DWORD dwRecvLen; //接收数据长度
	NetEvent neType; //网络事件
} *LPIO_INFORMATION;


(4)定义一个线程函数,调用GetQueuedCompletionStatus函数等待I/O完成数据包。在成功获取I/O完成
数据包后,读取自定义的IO_INFORMATION结构信息,根据事件类型neType执行不同的操作。最后还需要
重新开始一个重叠I/O操作请求,代码如下:

DWORD WINAPI UserThread(LPVOID CompletionPortID)
{
	HANDLE hCompPort = (HANDLE)CompletionPortID; //获取完成端口
	while (TRUE)
	{
		DWORD dwTransferred = 0;
		LPIO_INFORMATION pInfo = NULL;
		LPWSAOVERLAPPED Overlapped = NULL;
		//等待获取I/O完成数据包
		if (GetQueuedCompletionStatus(hCompPort, &dwTransferred,
			(LPDWORD)&pInfo, &Overlapped, INFINITE))
		{
			if (dwTransferred == 0 && pInfo->neType != NE_CLOSE) //连接意外终止
			{
				closesocket(pInfo->Sock); //关闭套接字
				delete pInfo; //释放pInfo对象
				continue;
			}
			switch (pInfo->neType)
			{
			case NE_REC: //接收数据
			{
				//...访问pInfo->Buffer中的数据
				break;
			}
			case NE_SEND: //发送数据
			{
				//...调用WSASend发送数据
				break;
			}
			case NE_CLOSE: //套接字连接关闭
			{
				//让线程退出
				return FALSE;
			}
			default:
				break;
			}
			//开始一个新的重叠I/O请求
			pInfo->neType = NE_POST; //设置网络事件
			pInfo->RecBuf.buf = pInfo->Buffer; //设置缓冲区
			pInfo->RecBuf.len = BUF_LEN; //设置缓冲区长度
			WSARecv(pInfo->Sock, &pInfo->RecBuf, 1, &pInfo->dwRecvLen,
				&nFlags, &pInfo->Overlapped, NULL);
		}
	}
	return FALSE;
}


(5)再定义一个线程函数,实现接受客户端连接。然后根据CPU数量开启多个用户线程,等待I/O完成
数据包。这样,就简单构建了一个IOCP模型,代码如下:

DWORD WINAPI AcceptConnect(LPVOID lpParameter)
{
	HANDLE hCompPort; //定义一个完成端口对象
	if ((hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,
		NULL, 0, 0)) == NULL) //创建完成端口对象
	{
		return 0;
	}
	SOCKET mainSock; //本地套接字
	WSADATA wsaData; //定义WSADATA对象
	WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化库函数
	mainSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
		NULL, NULL, WSA_FLAG_OVERLAPPED); //定义本地套接字
	SOCKADDR_IN localAddr; //定义套接字地址对象
	localAddr.sin_family = AF_INET;
	localAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	localAddr.sin_port = htons(8100); //设置端口
	bind(mainSock, (LPSOCKADDR)&localAddr, sizeof(localAddr));//绑定地址
	listen(mainSock, 15); //监听套接字
	SOCKADDR_IN remoteAddr;
	int nAddrSize = sizeof(remoteAddr);
	SYSTEM_INFO SystemInfo;
	GetSystemInfo(&SystemInfo); //获取系统信息
	DWORD dwThreadID;
	for (UINT i = 0; i<SystemInfo.dwNumberOfProcessors * 2; i++) //创建CPU数*2个用户线程
	{
		HANDLE hThread = NULL;
		if ((hThread = CreateThread(NULL, 0, UserThread, hCompPort, 0, &dwThreadID)) == NULL)
		{
			return FALSE;
		}
		CloseHandle(hThread); //关闭线程句柄
	}
	while (true)
	{
		//接受客户端连接
		SOCKET clientSock = accept(mainSock, (SOCKADDR*)&remoteAddr, &nAddrSize);
		if (clientSock != INVALID_SOCKET) //accept调用成功
		{
			IO_INFORMATION *pIOInfo = new IO_INFORMATION; //构建一个IO_INFORMATION对象
			memset(pIOInfo, 0, sizeof(IO_INFORMATION)); //初始化pIOInfo
			memset(pIOInfo->Buffer, 0, BUF_LEN); //初始化缓冲区
			memset(&pIOInfo->Overlapped, 0, sizeof(OVERLAPPED)); //初始化重叠结构
			pIOInfo->Sock = clientSock;
			pIOInfo->RecBuf.len = BUF_LEN; //设置缓冲区长度
			pIOInfo->RecBuf.buf = pIOInfo->Buffer; //设置数据缓冲区
			pIOInfo->neType = NE_REC; //设置网络事件
			if (CreateIoCompletionPort((HANDLE)pIOInfo->Sock, hCompPort,
				(DWORD)pIOInfo, 0) == NULL) //绑定套接字和完成端口
			{
				return FALSE;
			}
			if (WSARecv(pIOInfo->Sock, &pIOInfo->RecBuf, 1, &pIOInfo->dwRecvLen,
				&nFlags, &pIOInfo->Overlapped, NULL) == SOCKET_ERROR) //有错误发生
			{
				int nError = WSAGetLastError(); //获取错误代码
				if (nError != WSA_IO_PENDING) //没有错误代码表示重叠操作正在进行中
				{
					closesocket(pIOInfo->Sock); //关闭网络套接字
					delete pIOInfo; //释放pIOInfo对象
				}
			}
		}
	}
	return 0;
}


对于套接字的6种I/O模型,在此是按照从简单到复杂的顺序进行介绍的。对于网络编程的初学者,只要掌握前3种I/O模型就可以了。只有在需要管理数百乃至上千个套接字时,才需要使用重叠I/O模型,这样可以带来更高的性能。

版权声明:

posted on 2015-04-27 11:11  moffis  阅读(468)  评论(0编辑  收藏  举报

导航