1. 程式人生 > >重疊I/O之完成例程

重疊I/O之完成例程

這個模型中有兩個函式可以交換著用,那就是WSAWaitForMultipleEvents()和SleepEx()函式,前者需要一個事件驅動,後者則不需要。是不是聽起來後者比較厲害,當然不是,簡單肯定是拿某種效能換來的,那就是當多client同時發出請求的時候,SleepEx如果等候時間設定成比較大的話,會造成client連線不上的現象。具體可以執行一下示例程式碼體會一下。
示例程式碼1(WSAWaitForMultipleEvents()版本)
示例程式碼2(SleepEx()版本)

使用該模型的步驟如下:
一、開啟伺服器(和事件通知那裡一樣)

二、建立ThreadAccept執行緒

這裡要先建立一個事件物件,然後把該事件物件作為引數傳入ThreadBind執行緒中。之後就不斷的等待client的請求,一有新的請求立即用WSASetEvent函式將該事件物件狀態設定為有訊號。

虛擬碼如下:

create a event object;  //WSACreateEvent()
...
call ThreadAccept and use this event object as param;
...
while(1)
{
accept new client request;
...
set the event has single; //WSASetEvent()
}

如圖:

三、建立ThreadBind執行緒

這個主要用來為new client繫結一個完成例程,然後再投遞一個WSARecv。

虛擬碼如下:

while(1)
{
    while(1)
    {
        Wait for accept() to signal an event and also process CompletionRoutine ;//WSAWaitForMultipleEvents()
        ...
        reset the event object;//WSAResetEvent()
        ...
        alloc a global mem for
save client information;//GlobalAlloc() ... post a WSARecv; } }

如圖:

四、建立完成例程函式(回撥函式)

其實這個就相當於是寫一個自定義的回撥函式給系統呼叫。
該函式的引數一定,不能更改。名字隨便起。
如下:

void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, 
LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
    ......
}

其主要功能如下所描述
虛擬碼:

get the client information;
...
error handle;
...
data handle;
...
post a WSARecv

如圖:

SleepEx版本的基本差不多,就是把事件去掉,改為用一個變數判斷有無new client以及用SleepEx等待完成例程的操作。
如圖:

示例程式碼1

#include <WinSock2.h>
#include <process.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")

#define PORT 18000
#define MAXBUF 128

//自定義一個存放socket資訊的結構體,用於完成例程中對OVERLAPPED的轉換
typedef struct _SOCKET_INFORMATION {
    OVERLAPPED Overlapped;        //這個欄位一定要放在第一個,否則轉換的時候,資料的賦值會出錯              
    SOCKET Socket;                //後面的欄位順序可打亂並且不限制欄位數,也就是說你還可以多定義幾個欄位
    CHAR Buffer[MAXBUF];
    WSABUF wsaBuf;
} SOCKET_INFORMATION, *LPSOCKET_INFORMATION;

SOCKET g_sClient;                 //不斷新加進來的client

//開啟伺服器
BOOL OpenServer(SOCKET* sServer)
{
    BOOL bRet = FALSE;
    WSADATA wsaData = { 0 };
    SOCKADDR_IN addrServer = { 0 };
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = htons(PORT);
    addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    do
    {
        if (!WSAStartup(MAKEWORD(2, 2), &wsaData))
        {
            if (LOBYTE(wsaData.wVersion) == 2 || HIBYTE(wsaData.wVersion) == 2)
            {
                //在套接字上使用重疊I/O模型,必須使用WSA_FLAG_OVERLAPPED標誌建立套接字
                //g_sServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
                *sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
                if (*sServer != INVALID_SOCKET)
                {
                    if (SOCKET_ERROR != bind(*sServer, (SOCKADDR*)&addrServer, sizeof(addrServer)))
                    {
                        if (SOCKET_ERROR != listen(*sServer, SOMAXCONN))
                        {
                            bRet = TRUE;
                            break;
                        }
                        closesocket(*sServer);
                    }
                    closesocket(*sServer);
                }
            }
        }

    } while (FALSE);

    return bRet;
}

//完成例程
void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
    DWORD dwSendBytes, dwRecvBytes;
    DWORD dwFlag;

    //強制轉換為我們自定義的結構,這裡就解釋了為什麼第一個欄位要是OVERLAPPED
    //因為轉換後首地址肯定會相同,讀取的資料一定會是Overlapped的資料
    //所以要先把Overlapped的資料儲存下來,接下來記憶體中的資料再由系統分配到各個欄位中
    LPSOCKET_INFORMATION pSi = (LPSOCKET_INFORMATION)Overlapped;

    if (dwError != 0)  //錯誤顯示
        printf("I/O operation failed with error %d\n", dwError);
    if (dwBytesTransferred == 0)
        printf("Closing socket %d\n\n", pSi->Socket);
    if (dwError != 0 || dwBytesTransferred == 0) //錯誤處理
    {
        closesocket(pSi->Socket);
        GlobalFree(pSi);
        return;
    }

    //如果已經發送完成了,接著投遞下一個WSARecv
    printf("Recv%d:%s\n", pSi->Socket, pSi->wsaBuf.buf);
    dwFlag = 0;
    ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
    pSi->wsaBuf.len = MAXBUF;
    pSi->wsaBuf.buf = pSi->Buffer;
    if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlag, &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
    {
        if (WSAGetLastError() != WSA_IO_PENDING)
        {
            printf("WSARecv() failed with error %d\n", WSAGetLastError());
            return;
        }
    }
}

//把client和完成例程繫結起來
unsigned int __stdcall ThreadBind(void* lparam)
{
    DWORD dwFlags;
    LPSOCKET_INFORMATION pSi;
    DWORD dwIndex;
    DWORD dwRecvBytes;
    WSAEVENT eventArry[1];
    eventArry[0] = (WSAEVENT)lparam;
    while (1)
    {
        //等待一個完成例程返回
        while (TRUE)
        {
            dwIndex = WSAWaitForMultipleEvents(1, eventArry, FALSE, WSA_INFINITE, TRUE);
            if (dwIndex == WSA_WAIT_FAILED)
            {
                printf("WSAWaitForMultipleEvents() failed with error %d\n", WSAGetLastError());
                return FALSE;
            }
            if (dwIndex != WAIT_IO_COMPLETION)
                break;
        }
        //重設事件
        WSAResetEvent(eventArry[0]);
        //為SOCKET_INFORMATION分配一個全域性記憶體空間,相當於全域性變量了
        //這裡為什麼要分配全域性的呢?因為我們要在完成例程中引用socket的資料
        if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
        {
            printf("GlobalAlloc() failed with error %d\n", GetLastError());
            return 1;
        }

        //填充各個欄位
        pSi->Socket = g_sClient;
        ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
        pSi->wsaBuf.len = MAXBUF;
        pSi->wsaBuf.buf = pSi->Buffer;

        dwFlags = 0;
        //投遞一個WSARecv
        if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
            &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
        {
            if (WSAGetLastError() != WSA_IO_PENDING)
            {
                printf("WSARecv() failed with error %d\n", WSAGetLastError());
                return 1;
            }
        }
        printf("Socket %d got connected...\n", g_sClient);
    }
    return 0;
}

//接受client請求執行緒
unsigned int __stdcall ThreadAccept(void* lparam)
{
    SOCKET sServer = *(SOCKET*)lparam;
    WSAEVENT event = WSACreateEvent();
    _beginthreadex(NULL, 0, ThreadBind, event, 0, NULL);
    while (TRUE)
    {
        g_sClient = accept(sServer, NULL, NULL);
        if (g_sClient != INVALID_SOCKET)
            WSASetEvent(event);
    }
    return 0;
}


int main(int argc, char **argv)
{
    SOCKET sServer = INVALID_SOCKET;
    if (OpenServer(&sServer))
        _beginthreadex(NULL, 0, ThreadAccept, &sServer, 0, NULL);
    Sleep(10000000);
    return 0;
}

示例程式碼2
因為只是ThreadAccept和ThreadBind有變動,所以只貼出這兩段程式碼

//接受client請求執行緒
unsigned int __stdcall ThreadAccept(void* lparam)
{
    SOCKET sServer = *(SOCKET*)lparam;
    while (TRUE)
    {
        g_sClient = accept(sServer, NULL, NULL);
        if (g_sClient != INVALID_SOCKET)
            g_bNewClient = TRUE;
    }
    return 0;
}

//把client和完成例程繫結起來
unsigned int __stdcall ThreadBind(void* lparam)
{
    DWORD dwFlags;
    LPSOCKET_INFORMATION pSi;
    DWORD dwIndex;
    DWORD dwRecvBytes;

    while (1)     
    {               
        //等待一個完成例程返回
        while (TRUE)
        {
            dwIndex = SleepEx(10, TRUE);     //這裡等待10ms,如果等待時間越大,越容易出現記憶體讀取錯誤
            if (dwIndex == WSA_WAIT_FAILED)  //如果用事件通知,則不用考慮這個,不會衝突的。
            {
                printf("SleepEx() failed with error %d\n", WSAGetLastError());
                return 1;
            }
            if (dwIndex != WAIT_IO_COMPLETION)
                break;   //有新的client加入,跳出迴圈,繼續為新的client繫結例程
        }
        if (g_bNewClient)//如果有new client 才為它繫結一個完成例程
        {
            //為SOCKET_INFORMATION分配一個全域性記憶體空間,相當於全域性變量了
            //這裡為什麼要分配全域性的呢?因為我們要在完成例程中引用socket的資料
            if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
            {
                printf("GlobalAlloc() failed with error %d\n", GetLastError());
                return 1;
            }

            //填充各個欄位
            pSi->Socket = g_sClient;
            ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
            pSi->wsaBuf.len = MAXBUF;
            pSi->wsaBuf.buf = pSi->Buffer;

            dwFlags = 0;
            //投遞一個WSARecv
            if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
                &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
            {
                if (WSAGetLastError() != WSA_IO_PENDING)
                {
                    printf("WSARecv() failed with error %d\n", WSAGetLastError());
                    return 1;
                }
            }
            printf("Socket %d got connected...\n", g_sClient);
            g_bNewClient = FALSE;
        }
    }
    return 0;
}

關於完成例程如何同時投遞WSARecv和WSASend下一篇講。