基于完成例程的重叠I/O网络模型

初步了解完成例程

1、必须指明一点,完成例程不是完成端口,而是属于重叠I/O(这里说的都是网络重叠I/O模型)的一种,通常来说重叠I/O模型有两种,分别是基于网络事件响应(下一篇文章会讲到这个模型)与基于完成例程。
2、基于完成例程的重叠I/O模型相对于事件选择I/O模型的优越之处在于,重叠I/O模型完全解决了recv的阻塞问题,以前的模型当中,recv只是接收到数据到来的通知,之后依旧要自己去内核拷贝数据到用户空间中,如今recv过程全部交由操作系统完成,减去了数据等待与将数据从内核拷贝到程序缓冲区的时间,并且这个期间不占用程序自身的时间片。详细见下图:

重叠I/O之完成例程异步模型:
完成例程
事件选择网络模型:
事件选择

3、完成例程相比基于事件响应的重叠I/O模型的优越之处在于,完成例程并没有64个事件的上限,而是操作系统调用完成例程(也就是一个由操作系统调用的回调函数)对接收到的数据进行处理。

模型的相关API解析:

关于网络事件对象的相关API与解释在上一篇WSAEventSelect模型中有讲到,关于文中提到的原子锁我在Windows多线程中也有讲到,这里就不一一赘述。

WSARecv

int WSARecv(
  _In_     SOCKET s,          //连接的客户端套接字
  _Inout_  LPWSABUF lpBuffers,//接收数据的WSABUF缓冲区地址
  _In_     DWORD dwBufferCount,//缓冲区的个数
  _Out_    LPDWORD lpNumberOfBytesRecvd, //接收的字节数
  _Inout_  LPDWORD lpFlags,  // 用于修改WSARecv函数调用行为的标志的指针。这个值置0即可
  _In_     LPWSAOVERLAPPED lpOverlapped, //指向重叠结构Overlapped指针
  _In_     LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine  //下面有介绍到 
);

功能:给指定的套接字投递一个异步的IO请求,请求后的处理由操作系统内核完成。
返回值:
If no error occurs and the receive operation has completed immediately, WSARecv returns zero. In this case, the completion routine will have already been scheduled to be called once the calling thread is in the alertable state. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError. The error code WSA_IO_PENDING indicates that the overlapped operation has been successfully initiated and that completion will be indicated at a later time. Any other error code indicates that the overlapped operation was not successfully initiated and no completion indication will occur.
大概意思为:成功返回0,标识已经操作已经完成。但是一般不会返回0,毕竟接收数据并不会那么及时,所以操作未完成就会返回SOCKET_ERROR,通过WSAGetLastError来检测错误的信息,其中有个常见的错误信息为WSA_IO_PENDING(标识重叠操作已经启动,并将在稍后的时间内表示完成),其他的错误信息表示重叠操作未成功启动
详解参数lpCompletionRoutine
If lpCompletionRoutine is not NULL, the hEvent parameter is ignored and can be used by the application to pass context information to the completion routine. A caller that passes a non- NULL lpCompletionRoutine and later calls WSAGetOverlappedResult for the same overlapped I/O request may not set the fWait parameter for that invocation of WSAGetOverlappedResult to TRUE. In this case the usage of the hEvent parameter is undefined, and attempting to wait on the hEvent parameter would produce unpredictable results.
一个指向完成例程的指针,此时情况下对于重叠结构(倒数第二个参数)的事件对象是不会有重置信号的操作的,所以要进行等待重叠结构中的事件的话,将不会有结果。如果不使用完成例程,可以置为NULL(基于事件通知的重叠I/O模型),如果重叠操作完成,此时重叠结构(倒数第二个参数)绑定的事件对象将会变为有信号状态,可以通过WSAWaitForMultipleEvents检测事件信号和WSAGetOverlappedResult接收重叠结果。


WSASend

int WSASend(
  _In_   SOCKET s,// 连接的套接字
  _In_   LPWSABUF lpBuffers,//  发送的缓冲区
  _In_   DWORD dwBufferCount,// 缓冲区的个数
  _Out_  LPDWORD lpNumberOfBytesSent,// 发送成功的字节数
  _In_   DWORD dwFlags,// 置0即可
  _In_   LPWSAOVERLAPPED lpOverlapped,// 指向重叠结构的指针
  _In_   LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine// 见WSARecv
);

功能:投递一个异步的发送,由操作系统完成
返回值:基本和WSARecv相同,就不多赘述了


完成例程函数原型CompeletRoutine

void CALLBACK CompeletRoutine(
DWORD dwError,   //指定重叠操作的完成状态,如lpOverlapped所示
DWORD dwBytesTransferred, //在重叠操作期间传输的字节数
LPWSAOVERLAPPED Overlapped, //指向的地址与传递给WSARecv的WSAOVERLAPPED结构体地址相同
DWORD dwFlags) //返回操作可能已经完成的标志(例如来自WSARecv等),如果接收操作已经立即完成。此函数不返回值。

函数相关释义:当使用WSAWaitForMultipleEvents时,所有等待完成例程在可警告的线程等待满足WSA_IO_COMPLETION的返回码之前被调用。可以以任何顺序调用完成例程,而不一定按照重叠操作完成相同顺序。但是,保留填写的缓冲区的顺序与指定的相同。


相关的其他函数

HGLOBAL WINAPI GlobalAlloc(
  _In_  UINT uFlags,    //分配堆的方式
  _In_  SIZE_T dwBytes  //分配的字节数
);

功能:申请一个全部内存空间
返回值:成功返回新分配的内存对象的句柄,失败返回NULL

HGLOBAL WINAPI GlobalFree(
  _In_  HGLOBAL hMem    // 分配的内存对象的句柄
);

功能:释放一个全局内存空间

LPVOID HeapAlloc(
  HANDLE hHeap,  // 需要分配堆的句柄,通过HeapCreate或GetProcessHeap获得
  DWORD dwFlags, // 堆分配的可选参数,HEAP_ZERO_MEMORY进行清零、HEAP_NO_SERIALIZE 不使用序列化存储(连续存储)
  DWORD dwBytes // 需要分配的堆的字节数
);

功能:用来在指定的堆上分配内存,并且分配后的内存不可移动。它分配的内存不能超过4MB。
返回值:成功返回新分配的堆空间首地址指针,失败返回NULL

BOOL HeapFree(
  HANDLE hHeap,   // 需要释放的堆句柄,为HeapCreate或GetProcessHeap的返回值
  DWORD dwFlags, // 释放的参数,不好解释,一般置0
  LPVOID lpMem   // 需要释放的堆空间
);

功能:从一个堆中释放内存块


关于WSAWaitForMultipleEvents中bAlertable参数的解释

在MSDN众多解释中抠的一段,消息不够请MSDN
The completion routine follows the same rules as stipulated for Windows file I/O completion routines. The completion routine will not be invoked until the thread is in an alertable wait state such as can occur when the function WSAWaitForMultipleEvents with the fAlertable parameter set to TRUE is invoked.
大概意思是:完成例程遵循Windows文件I/O完成例程相同的规则(底层都是ReadFile和WriteFile),直到线程处于可警告状态的时候才会调用完成例程函数,例如WSAWaitForMultipleEvents的fAlertable参数设置为TRUE并被调用时就会发生。也就是说如果bAlertable设置为FALSE并发生I / O完成回调,则不会执行I / O完成例程。
重点来了,在很多资料上只说了在使用完成例程的时候WSAWaitForMultipleEvents的
fAlertable设置为TRUE,而dwTimeout设置为WSA_INFINITE的原因。


实现过程

总体实现的流程图
总体流程图

1、初始化网络,接收连接请求

bool OverlapdIO::InitNet(NetCallBack func, UINT nSocketPOrt, LPCSTR lpszSocketAddr /*= ADDR_ANY*/)
{
    if (!WinSockInit(nSocketPOrt, lpszSocketAddr))
    {
        printf("WinSockInit failed:%d\n", WSAGetLastError());
        return false;
    }

    NetProc = func; // 初始化数据处理函数

    wsaEvent = WSACreateEvent();
    if (WSA_INVALID_EVENT == wsaEvent)
    {
        printf("WSACreateEvent failed:%d\n", WSAGetLastError());
        return false;
    }

    // begin accept thread
    _beginthreadex(0, 0, AcceptProc, this, 0, 0);

    SOCKET tmpSock = 0;
    SOCKADDR_IN tmpAddr;
    int nLen = sizeof(SOCKADDR_IN);
    while (true)
    {
        memset((void*)&tmpAddr, 0, nLen);
        tmpSock = accept(m_sListen, (struct sockaddr*)&tmpAddr, &nLen);
        if (INVALID_SOCKET != m_sClient)
        {
            while (InterlockedExchange(&bTarget, TRUE) == TRUE)// 获取原子锁
            {
                Sleep(0);//等待锁这段时间...你可以做其他事
            }
            memcpy(&m_cAddr, &tmpAddr, nLen);
            m_sClient = tmpSock;
            InterlockedExchange(&bTarget, FALSE);// 释放原子锁

            WSASetEvent(wsaEvent);
        }                   
    }

    return true;
}

2、接收连接的线程,将接收到的连接进行判断并投递一个异步操作(WSARecv)

unsigned int __stdcall OverlapdIO::AcceptProc(void* lparam)
{
    OverlapdIO* pThis = (OverlapdIO*)lparam;
    WSAEVENT eventArry[1] = { 0 };
    eventArry[0] = pThis->wsaEvent;
    DWORD dwIndex = 0;
    DWORD dwRet = 0;
    LPSOCKET_OVERLAPD sockOvlp;

    while (true)
    {
        while (true)
        {
            //等待连接的事件信号到来,最后一个参数为TRUE:将线程置于可警告的等待状态
            //SleepEx()函数也可将线程设置为可警告的等待状态
            dwIndex = WSAWaitForMultipleEvents(1, eventArry, FALSE, WSA_INFINITE, TRUE);
            if (WSA_WAIT_FAILED == dwIndex) //failed
            {
                printf("WSAWaitForMultipleEvent failed:%d\n", WSAGetLastError());
                continue;
            }
            if (WAIT_IO_COMPLETION != dwIndex) //is not incomplete
                break;
        }

        WSAResetEvent(eventArry[dwIndex - WSA_WAIT_EVENT_0]); //reset event
        // 在全局区申请内存,此处申请程序默认堆内存,所以注释
//      sockOvlp = (LPSOCKET_OVERLAPD)GlobalAlloc(GPTR, sizeof(SOCKET_OVERLAPD));
//      if (NULL == sockOvlp)
//      {
//          printf("GlobalAlloc() failed:%d\n", GetLastError());
//          GlobalFree(sockOvlp);
//          continue;
//      }
//      ZeroMemory((void*)&(sockOvlp->overlap), sizeof(WSAOVERLAPPED));
        sockOvlp = (LPSOCKET_OVERLAPD)HeapAlloc(// 在程序的默认堆上申请内存
            GetProcessHeap(),
            HEAP_ZERO_MEMORY,       
            sizeof(SOCKET_OVERLAPD));
        if (NULL == sockOvlp)
        {
            printf("HeapAlloc() failed:%d\n", GetLastError());
            HeapFree(GetProcessHeap(), 0, sockOvlp);
            continue;
        }

        sockOvlp->wsaBuf.len = MAXSIZE;
        sockOvlp->wsaBuf.buf = sockOvlp->szMsg;
        sockOvlp->Flags = 0;

        while (InterlockedExchange(&bTarget, TRUE) == TRUE) // 获取原子锁,保证m_sClient与m_cAddr值
        {       
            Sleep(0);
        }
        sockOvlp->sock = pThis->m_sClient;
        sockOvlp->addr = pThis->m_cAddr;
        InterlockedExchange(&bTarget, FALSE);   

        if (SOCKET_ERROR == WSARecv(sockOvlp->sock, &(sockOvlp->wsaBuf), 1, &(sockOvlp->dwRecvdBytes),
            &(sockOvlp->Flags), &(sockOvlp->overlap), (LPWSAOVERLAPPED_COMPLETION_ROUTINE)CompeletRoutine))
        {
            dwRet = WSAGetLastError();
            if (WSA_IO_PENDING != dwRet) // WSA_IO_PENDING 重叠IO操作成功,等待稍后完成
            {
                HeapFree(GetProcessHeap(), 0, sockOvlp);
                printf("WSARecv failed:%d\n", dwRet);
            };          
        }
    }
    return 0;
}

3、完成例程,就相当于一个系统的回调函数 ,在数据接收完成的时候自动被系统调用

void CALLBACK OverlapdIO::CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
    DWORD dwRet = 0;
    LPSOCKET_OVERLAPD pOvl = (LPSOCKET_OVERLAPD)Overlapped;

    if (dwError != 0 || dwBytesTransferred == 0)
    {
        if (dwError != 0)
            printf("CompeletRoutine dwError:%d\n", dwError);
        if (dwBytesTransferred == 0)
            printf("[%s:%d]--->log off\n", inet_ntoa(pOvl->addr.sin_addr),htons(pOvl->addr.sin_port));
        closesocket(pOvl->sock);
        HeapFree(GetProcessHeap(), 0, pOvl);    // 释放堆空间
        //GlobalFree(pOvl);
        return;
    }

    NetProc((void *)pOvl);  // 处理函数(用于处理接收的数据)

    ZeroMemory((void *)&(pOvl->overlap), sizeof(WSAOVERLAPPED));// 清空填0
    pOvl->Flags = 0;
    pOvl->wsaBuf.len = MAXSIZE;
    ZeroMemory((void*)pOvl->szMsg, MAXSIZE);
    pOvl->wsaBuf.buf = pOvl->szMsg;

    if (SOCKET_ERROR == WSARecv(pOvl->sock, &(pOvl->wsaBuf), 1, &(pOvl->dwRecvdBytes),
        &(pOvl->Flags), &(pOvl->overlap), (LPWSAOVERLAPPED_COMPLETION_ROUTINE)CompeletRoutine))
    {
        dwRet = WSAGetLastError();
        if (WSA_IO_PENDING != dwRet) // 重叠IO操作成功,等待稍后完成
            printf("WSARecv failed:%d\n", dwRet);
    }
}

4、进行数据发送的异步投递

bool OverlapdIO::WSASendToClient(const SOCKET_OVERLAPD& sockOvlp, void* lparam)
{
    DWORD dwRet = 0;
    char* buf = (char*)lparam;
    SOCKET_OVERLAPD sendOvlp = sockOvlp;

    memcpy(sendOvlp.szMsg, buf, strlen(buf));
    sendOvlp.Flags = 0;

    if (SOCKET_ERROR == WSASend(sendOvlp.sock, &(sendOvlp.wsaBuf), 1, &(sendOvlp.dwSendBytes), sendOvlp.Flags, &(sendOvlp.overlap), NULL))
    {
        dwRet = WSAGetLastError();
        if (dwRet != WSA_IO_PENDING)
        {
            printf("WSASend failed:%d\n", dwRet);
            return false;
        }
    }

    return true;
}

完整代码,供大家学习:http://pan.baidu.com/s/1i5qxyHv
下一篇:基于事件通知的重叠I/O模型

希望大家指出不足,共同进步

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
重叠IO模型之OverLapped完成例程模型WSACompletionRoutineServer VS2010 基础入门 客户端与服务器端 客户端向服务器端发送数据 可接收多个客户端 #include #include #pragma comment (lib, "ws2_32.lib") #define PORT 8088 #define MSG_SIZE 1024 SOCKET g_sConnect; bool g_bConnect = false; typedef struct { WSAOVERLAPPED overLap; WSABUF wsaBuf; char chMsg[MSG_SIZE]; DWORD nRecvNum; DWORD nFlags; SOCKET sClient; }PRE_IO_OPERATION_DATA, *LP_PER_IO_OPERATION_DATA; void CALLBACK CompletionRoutine(DWORD dwError, DWORD dwTrans, LPWSAOVERLAPPED lpOverlap, DWORD nFlags); DWORD WINAPI workThread(LPVOID lp) { LP_PER_IO_OPERATION_DATA lpData; while(TRUE) { if (g_bConnect) // 有新的连接 { // 为lpData分配空间并初始化 lpData = (LP_PER_IO_OPERATION_DATA)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PRE_IO_OPERATION_DATA)); lpData->wsaBuf.len = MSG_SIZE; lpData->wsaBuf.buf = lpData->chMsg; lpData->sClient = g_sConnect; WSARecv(lpData->sClient, &lpData->wsaBuf, 1, &lpData->nRecvNum, &lpData->nFlags, &lpData->overLap, CompletionRoutine); g_bConnect = false; // 处理完毕 } SleepEx(1000, TRUE); } return 0; } // 系统在WSARecv收到信息后,自动调用此函数,并传入参数--回调函数 void CALLBACK CompletionRoutine(DWORD dwError, DWORD dwTrans, LPWSAOVERLAPPED lpOverlap, DWORD nFlags) { LP_PER_IO_OPERATION_DATA lpData = (LP_PER_IO_OPERATION_DATA)lpOverlap; if (0 != dwError) // 接收失败 { printf("Socket %d Close!\n", lpData->sClient); closesocket(lpData->sClient); HeapFree(GetProcessHeap(), 0, lpData); } else // 接收成功 { lpData->chMsg[dwTrans] = '\0'; send(lpData->sClient, lpData->chMsg, dwTrans, 0); printf("Socket:%d MSG: %s \n", lpData->sClient, lpData->chMsg); memset(&lpData->overLap, 0, sizeof(WSAOVERLAPPED)); lpData->wsaBuf.len = MSG_SIZE; lpData->wsaBuf.buf = lpData->chMsg; // 继续接收来自客户端的数据 实现 WSARecv与CompletionRoutine循环 WSARecv(lpData->sClient, &lpData->wsaBuf,1, &lpData->nRecvNum, &lpData->nFlags, &lpData->overLap, CompletionRoutine); } } int main() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); SOCKET sListen; sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addrListen; addrListen.sin_family = AF_INET; addrListen.sin_port = htons(PORT); addrListen.sin_addr.S_un.S_addr = htonl(ADDR_ANY); int nErrorCode = 0; nErrorCode = bind(sListen, (sockaddr*)&addrListen, sizeof(sockaddr)); nErrorCode = listen(sListen, 5); DWORD nThreadID; CreateThread(NULL, 0, workThread, NULL, 0, &nThreadID); sockaddr_in addrConnect; int nAddrLen = sizeof(sockaddr_in); printf("Server Started!\n"); while(TRUE) { g_sConnect= accept(sListen, (sockaddr*)&addrConnect, &nAddrLen); if (INVALID_SOCKET == g_sConnect) { return -1; } g_bConnect = true; // 连接成功 printf("Accept Client :%s -- PORT:%d\n", inet_ntoa(addrConnect.sin_addr), htons(addrConnect.sin_port)); } return 0; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值