zoukankan      html  css  js  c++  java
  • 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模型,这样可以带来更高的性能。

    版权声明:

  • 相关阅读:
    eclipse折叠快捷键
    ubuntu下忘记用户名和密码的解决办法
    ubuntu屏幕分辨率问题
    CentOS 5.X安装LAMP最高版本环境
    CentOS 6.X安装LAMP最高版本环境
    Shell脚本升级CentOS php版本v
    运行yum报错Error: Cannot retrieve metalink for repository: epel. Please verify its path and try again
    批处理测试局域网网络连通性ping1-255
    批处理cmd背景颜色
    CentOS6.4安装LAMP环境
  • 原文地址:https://www.cnblogs.com/walccott/p/4957073.html
Copyright © 2011-2022 走看看