Windows socket之重疊IO:事件通知

轉載自:http://blog.csdn.net/ithzhang/article/details/8496232


Windows socket重疊IO模型開發

利用套接字重疊IO模型,應用程序能一次投遞一個或多個IO請求,當系統完成IO操作後通知應用程序。該模型以win32異步IO機制爲基礎。與前面介紹的所有IO模型相比較,該模型是真正意義上的異步IO模型,它能使Windows socket應用程序達到更高的性能。

關於異步IO機制可以參考:《Windows核心編程系列》十談談同步設備IO與異步設備IO之異步設備IO

Windows socket重疊IO延續了win32 IO模型。從發送和接收的角度來看,重疊IO模型與前面介紹的Select模型、WSAAsyncSelect模型和WSAEventSelect模型都不同。因爲在這三個模型中IO操作還是同步的,例如:在應用程序調用recv函數時,都會在recv函數內阻塞,直到接收數據完畢後才返回。而重疊IO模型會在調用recv後立即返回。等數據準備好後再通知應用程序。

系統嚮應用程序發送通知的形式有兩種:一是事件通知。二是完成例程。後面將會介紹這兩種形式。

注意:套接字的重疊IO屬性不會對套接字的當前工作模式產生影響。創建具有重疊屬性的套接字執行重疊IO操作,並不會改變套接字的阻塞模式。套接字的阻塞模式與重疊IO操作不相關。重疊IO模型僅僅對WSASend和WSARecv的行爲有影響。 對listen,如果是阻塞模式,直到有客戶請求到達時纔會返回。這點要特別注意。

與其他模型的區別

在Windows socket中,接收數據的過程可以分爲:等待數據和將數據從系統複製到用戶空間兩個階段。各種IO模型的區別在於這兩個階段上。

前三個模型的第一個階段的區別: select模型利用select函數主動檢查系統中套接字是否滿足可讀條件。WSAAsyncSelect模型和WSAEventSelect模型則被動等待系統的通知。

第二個階段的區別:此階段前三個模型基本相同,在將數據從系統複製到用戶緩衝區時,線程阻塞。而重疊IO與它們都不同,應用程序在調用輸入函數後,只需等待接收系統完成的IO操作完成通知。

套接字重疊IO模型主要有一下相關函數:

 WSASocket()             //創建套接字。 
 WSASend 和 WSASendTo    //發送數據。
 WSARecv 和 WSARecvFrom  //接收數據。
 WSAIoctl                //控制套接字模式。
 AcceptEx                //接受連接。

創建套接字

要在套接字上使用重疊IO模型,在創建套接字時,必須使用WSA_FLAG_OVERLAPPED標誌創建。

SOCKET s = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);

當使用socket函數創建套接字時,會默認設置WSA_FLAG_OVERLAPPED標誌。

接收數據

應用程序調用WSARecv和WSARecvFrom函數接收數據。

WSARecv函數 介紹如下:

int WSARecv(
     SOCKET s,
     LPWSABUF lpBuffers,
     DWORD dwBufferCount,
     LPDWORD lpNumberOfBytesRecvd,
     LPDWORD lpFlags,
     LPWSWOVERLAPPED lpOverlapped,
     LPWSOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUNTINE);
  1. s :爲接收套接字。
  2. lpBuffers :接受緩衝區。
  3. dwBufferCount :爲接收緩衝區數組元素的個數。如果就一個緩衝區,就指定爲1.
  4. lpNumberOfBytesRecvd :如果接收操作立即完成,該參數指明接收數據的字節數。
  5. lpFlags :標誌位,一般爲0.
  6. lpOverlapped :指向WSAOVERLAPPED結構指針。
  7. lpCompletionROUTINE :完成例程。

該函數可以使用一個或多個緩衝區接收數據。如果接收操作未能立即完成。應用程序利用完成例程或是事件對象獲得通知。

如果操作立即完成,該函數返回值爲0。lpNumberOfBytesRecvd參數指明接收數據的字節數。如果未能立即完成,函數返回SOCKET_ERROR值。WSAGetLastError返回WSA_IO_PENDING。

如果lpOverlapped和lpCompletionROUTINE都爲NULL,則該套接字作爲同步IO套接字使用。

如果lpCompletionRoutine參數爲NULL,當接收完成時,lpOverlapped參數的事件變爲觸發狀態。在應用程序中可以調用WSAWaitForMultipleEvents或者是WSAGetOverlappedResult等待該事件。

重疊IO採用事件或者是完成例程通知程序異步操作已完成。這可以通過調用WSASend或WSARecv來實現。之所以前面首先介紹WSARecv是因爲:我們並不知道客戶發送的數據何時到來。當在有客戶請求進來時,我們就調用了WSARecv函數,並採用重疊IO模型,並且在收到數據後,再次調用WSARecv,這樣在任何時刻只要此客戶的數據到來我們就會收到通知。如果應用程序調用WSARecv的時間先於數據到達的時間,則數據到達時會直接放入用戶的接收緩衝區。這就避免了從系統緩衝區向用戶緩衝區的複製操作。對於提高性能很有幫助。

WSBUF定義如下:

typedef struct _WSABUF
{

    u_long len;  //緩衝區長度。 
    har *buf;    //緩衝區。

} WSABUF, *LPWSABUF;

WSAOVERLAPPED結構。

WSARecv函數使用WSAOVERLAPPED結構作爲參數,將事件對象和重疊IO關聯在一起。該結構聲明如下:

typdef struct _WSAOVERLAPPED
{
    DWROD Internal;     //錯誤代碼。  
    DWORD InternalHigh; //已傳輸字節數
    DWORD Offset;       //低32位文件偏移。
    DWORD OffsetHigh;   //高32位文件偏移
    WSAEVENT hEvent;    //事件對象句柄。
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

應用程序可以執行一下步驟將一個事件對象與套接字關聯起來:

  1. 調用WSACreateEvent創建事件對象。
  2. 將該事件賦值給WSAOVERLAPPED結構的hEvent字段。
  3. 使用該重疊結構,調用WSASend或WSARecv函數。

當重疊操作完成時,重疊IO結構的事件對象變爲已觸發狀態。可以調用WSAWaitForMultipleEvents函數等待該事件發生。關於該函數,前面在介紹WSAEventSelect模型時有詳細的介紹,此處不再贅述。注意它最多隻能等待64個事件對象。

WSAGetOverlappedResult函數。

該函數返回在套接字重疊IO的結構:

bool WSAGetOverlappedResult(
      SOCKET s,
      LPWSAOVERLAPPED lpOverlapped,
      LPDWORD lpcbTransfer,
      BOOL fWait,
      LPDWORD lpdwFlags);
  1. s爲發起重疊操作的套接字。
  2. lpOverlapped發起重疊操作的WSAOVERLAPPED結構指針。
  3. lpcbTransfer:實際發送或接收的字節數。
  4. fWait:函數返回的方式。如果爲TRUE,該函數直到重疊IO完成時才返回。當爲false時,如果操作仍然處於等待執行狀態,則函數返回false。錯誤代碼爲WSA_IO_INCOMPLELE。
  5. lpdwFlags:接收完成狀態的附加標誌。

當函數返回TRUE時,重疊IO已經完成,lpOverlapped指明實際返回的數據。當函數返回false時,重疊IO還未完成。

注意:由於微軟已經公佈了WSAOVERLAPPED和OVERLAPPED結構的個字節的意義。Internal表示錯誤代碼。InternalHigh表示已傳輸字節數。因此在調用WSAWaitForMultipleEvents等待事件對象返回後,得到此事件對應的WSAOVERLAPPED結構後,就可以根據這兩個字段判斷異步IO的執行結果。無需再調用GetOverlappedResult結構。具體請參考《Windows核心編程》。

發送數據

應用程序調用WSASend或者WSASendTo發送數據。

WSASend函數聲明如下:

int WSASend(
      SOCKET s,
      LPWSABUF lpBuffers,
      DWORD dwBufferCount,
      LPDWORD lpNumberOfBytesSent,
      DWORD dwFlags,
      LPWSAOVERLAPPED lpOverlapped,
      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE);
  1. s:爲發送套接字。
  2. lpBuffer:指向WSABUF結構指針,用於發送數據。
  3. dwBufferCount : lpBuffers數組中元素數量。
  4. lpNumberOfBytesSent:如果發送立即完成,該參數指明發送字節數。
  5. lpFlags:標誌位。
  6. lpOverlapped : 指向WSAOVERLAPPED結構指針。
  7. lpCompletionROUTINE:完成例程。

事件通知

套接字重疊IO的事件通知方法要求事件對象與WSAOVERLAPPED結構關聯在一起。當IO操作完成後,該事件對象從未觸發狀態變爲觸發狀態。在應用程序中先調用WSAWaitForMultipleEvents函數等待該事件的發生。獲得該事件對象對應的WSAOVERLAPPED結構後可以根據Internal和InternalHigh字段(也可以調用WSAGetOverlappedResult函數)判斷IO完成的情況。

分爲以下步驟:

  1. 創建具有WSAOVERLAPPED標誌的套接字。如果調用socket()函數,則默認創建具有WSAOVERLAPPED標誌的套接字。如果調用WSASocket函數,需要指定WSAOVERLAPPED標誌。
  2. 爲套接字定義WSAOVERLAPPED結構,並清零。
  3. 調用WSACreateEvent函數創建事件對象,並將該事件句柄分配給WSAOVERLAPPED結構的hEvent字段。
  4. 調用輸入或者輸出函數。
  5. 調用WSAWaitForMultipleEvents函數等待與重疊IO關聯在一起的事件變爲已觸發狀態。
  6. WSAWaitForMultipleEvents返回後,調用WSAResetEvent函數,將該事件對象恢復爲未觸發態。
  7. 調用WSAGetOverlappedResult函數判斷重疊IO的完成狀態。

下面一個實例演示了使用socket 重疊IO模型開發服務程序的步驟。該程序設計兩個線程,接收線程用於接受客戶端連接請求,初始化重疊IO操作。服務線程用於重疊IO處理。

CClient:該類在前面各模型的介紹中都涉及過。它主要用於管理連接後的客戶端。

管理客戶端鏈表:存儲CClient*,用於管理連接的客戶端。
接受線程:


#include"iostream"
#include"Client.h"
#pragma  comment(lib,"WS2_32.lib")

SOCKET sListenSocket;
UINT totalEvent;  //事件數組元素個數。
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];  //事件對象數組。

bool InitSocket()
{
    WSAData wsa;
    WSAStartup(MAKEWORD(2,2),&wsa);
    //ServSocket = socket(AF_INET,SOCK_STREAM,0);
    sListenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if(sListenSocket == INVALID_SOCKET)
    {
        return false;
    }

    sockaddr_in  addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.100");
    addr.sin_port = htons(4000);
    int ret = bind(sListenSocket, (SOCKADDR*)&addr, sizeof(addr));
    if(ret == SOCKET_ERROR)
    {
        return false;
    }

    ret = listen(sListenSocket,10);
    if(ret == SOCKET_ERROR)
    {
        return false;
    }
    return true;
}

DWORD WINAPI AcceptThread(PVOID ppram)
{
    SOCKET newSocket;
    while(true)
    {
        newSocket = accept(sListenSocket, NULL, NULL);

        if(totalEvent > WSA_MAXIMUM_WAIT_EVENTS)
        {
            return -1;
        }

        if(WSA_INVALID_EVENT == ([totalEvent] = WSACreateEvent()))
        {
            return -1;
        }

        CClient* pClient = new CClient(newSocket, eventArray[totalEvent]);
        //m_list.add(newSocket)//新接受的套接字加入套接字鏈表,用以對客戶端的管理。
        totalEvent++;
    }
    //DeleteAllNode();
    closesocket(sListenSocket);
    WSACleanup();
    return 0;
}

服務線程:


DWORD WINAPI ServiceThread(PVOID pparam)
{
    int dwIndex;
    while(true)
    {
        if((dwIndex = WSAWaitForMultipleEvents(totalEvent, eventArray, false, WSA_INFINITE, false)) == WSA_WAIT_FAILED)
        {
            return -1;
        }

        WSAEVENT h = eventArray[dwIndex-WSA_WAIT_EVENT_0];
        WSAResetEvent(h);
        CClient* pClient;
        //獲得與該套接字對應的CClient指針。
        pClient = GetClient(h);
        //判斷IO操作完成情況。
        if(pClient->m_ol.InternalHigh == 0)//發生錯誤或客戶端關閉了連接。
        {
            //將此節點從客戶端鏈表中刪除。
            //將此節點對應的事件對象從事件對象數組中刪除。
        }
        else
        {
            //1,數據已經到達緩衝區。
            //2,處理數據。
            //3,投遞接收數據重疊IO請求,確保任何數據到來前都有一個異步接收IO請求。
            pClient->RecvData();
        }
    }
    return 0;
}

如有紕漏,請不吝賜教!!!
2013.1.12于山西大同

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章