您的位置:首页 > 其它

socket的IO模型

2015-04-27 11:11 375 查看
在设计网络通信程序时,需要通过一种机制来确定网络中某些事件的发生。例如,当主机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模型,这样可以带来更高的性能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: