c++ 網絡編程(十) LINUX/windows 異步通知I/O模型與重疊I/O模型 附帶示例代碼


 

原文作者:aircraft

原文鏈接:https://www.cnblogs.com/DOMLX/p/9662931.html

 

 

一.異步IO模型(asynchronous IO)

(1)什么是異步I/O

異步I/O(asynchronous I/O)由POSIX規范定義。演變成當前POSIX規范的各種早起標准所定義的實時函數中存在的差異已經取得一致。一般地說,這些函數的工作機制是:告知內核啟動某個操作,並讓內核在整個操作(包括將數據從內核復制到我們自己的緩沖區)完成后通知我們。這種模型與前一節介紹的信號驅動模型的主要區別在於:信號驅動式I/O是由內核通知我們何時可以啟動一個I/O操作,而異步I/O模型是由內核通知我們I/O操作何時完成。

示意圖如下:

我們調用aio_read函數(POSIX異步I/O函數以aio_或lio_開頭),給內核傳遞描述符、緩沖區指針、緩沖區大小(與read相同的三個參數)和文件偏移(與lseek類似),並告訴內核當整個操作完成時如何通知我們。該系統調用立即返回,並且在等待I/O完成期間,我們的進程不被阻塞。本例子中我們假設要求內核在操作完成時產生某個信號,該信號直到數據已復制到應用進程緩沖區才產生,這一點不同於信號驅動I/O模型。

 

 

(2)運用到的函數講解--WSAEventSelect模型

在WSAEventSelect模型中,基本流程如下:
 1. 創建一個事件對象數組,用於存放所有的事件對象;
 2. 創建一個事件對象(WSACreateEvent);
 3. 將一組你感興趣的SOCKET事件與事件對象關聯(WSAEventSelect),然后加入事件對象數組;
 4. 等待事件對象數組上發生一個你感興趣的網絡事件(WSAWaitForMultipleEvents);
 5. 對發生事件的事件對象查詢具體發生的事件類型(WSAEnumNetworkEvents);
 6. 針對不同的事件類型進行不同的處理;
 7. 循環進行

  函數過程:

 

  1. 初始化網絡環境,創建一個監聽的socket,然后進行connect操作。接下來WSACreateEvent()創建一個網絡事件對象,其聲明如下:
    WSAEVENT WSACreateEvent(void); //返回一個手工重置的事件對象句柄
  2. 再調用WSAEventSelect,來將監聽的socket與該事件進行一個關聯,其聲明如下:
    int WSAEventSelect( SOCKET s, //套接字 WSAEVENT hEventObject, //網絡事件對象 long lNetworkEvents //需要關注的事件 ); 

    我們客戶端只關心FD_READ和FD_CLOSE操作,所以第三個參數傳FD_READ | FD_CLOSE。

  3. 啟動一個線程調用WSAWaitForMultipleEvents等待1中的event事件,其聲明如下:
    復制代碼
    DWORD WSAWaitForMultipleEvents(    
      DWORD cEvents,                  //指定了事件對象數組里邊的個數,最大值為64 const WSAEVENT FAR *lphEvents, //事件對象數組 BOOL fWaitAll, //等待類型,TRUE表示要數組里全部有信號才返回,FALSE表示至少有一個就返回,這里必須為FALSE DWORD dwTimeout, //等待的超時時間 BOOL fAlertable //當系統的執行隊列有I/O例程要執行時,是否返回,TRUE執行例程返回,FALSE不返回不執行,這里為FALSE ); 
    復制代碼

    由於我們是客戶端,所以只等待一個事件。

  4. 當事件發生,我們需要調用WSAEnumNetworkEvents,來檢測指定的socket上的網絡事件。其聲明如下:
    int WSAEnumNetworkEvents ( SOCKET s, //指定的socket WSAEVENT hEventObject, //事件對象 LPWSANETWORKEVENTS lpNetworkEvents //WSANETWORKEVENTS<span style="font-family:Arial, Helvetica, sans-serif;">結構地址</span> ); 

    當我們調用這個函數成功后,它會將我們指定的socket和事件對象所關聯的網絡事件的信息保存到WSANETWORKEVENTS這個結構體里邊去,我們來看下這個結構體的聲明:

    typedef struct _WSANETWORKEVENTS { long lNetworkEvents;<span style="white-space:pre"> </span>//指定了哪個已經發生的網絡事件 int iErrorCodes[FD_MAX_EVENTS];<span style="white-space:pre"> </span>//錯誤碼 } WSANETWORKEVENTS, *LPWSANETWORKEVENTS; 

    根據這個結構體我們就可以判斷是否是我們所關注的網絡事件已經發生了。如果是我們的讀的網絡事件發生了,那么我們就調用recv函數進行操作。若是關閉的事件發生了,就調用closesocket將socket關掉,在數組里將其置零等操作。

 

  整個模型的流程圖如下:

 

 

 (3)實現服務端代碼:

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

SOCKET g_sClient[WSA_MAXIMUM_WAIT_EVENTS] = {INVALID_SOCKET};  //client socket數組
WSAEVENT g_event[WSA_MAXIMUM_WAIT_EVENTS];                     //網絡事件對象數組
SOCKET g_sServer = INVALID_SOCKET;                             //server socket 
WSAEVENT g_hServerEvent;                                       //server 網絡事件對象
int iTotal = 0;                                                //client個數
/*
@function OpenTCPServer             打開TCP服務器
@param _In_ unsigned short Port     服務器端口
@param  _Out_ DWORD* dwError        錯誤代碼
@return  成功返回TRUE 失敗返回FALSE
*/
BOOL OpenTCPServer( _In_ unsigned short Port, _Out_ DWORD* dwError)
{
    BOOL bRet = FALSE;
    WSADATA wsaData = { 0 };
    SOCKADDR_IN ServerAddr = { 0 };
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = htons(Port);
    ServerAddr.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)
            {
                g_sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
                g_hServerEvent = WSACreateEvent();                    //創建網絡事件對象
                WSAEventSelect(g_sServer, g_hServerEvent, FD_ACCEPT);//為server socket注冊網絡事件 
                if (g_sServer != INVALID_SOCKET)
                {
                    if (SOCKET_ERROR != bind(g_sServer, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)))
                    {
                        if (SOCKET_ERROR != listen(g_sServer, SOMAXCONN))
                        {
                            bRet = TRUE;
                            break;
                        }
                        *dwError = WSAGetLastError();
                        closesocket(g_sServer);
                    }
                    *dwError = WSAGetLastError();
                    closesocket(g_sServer);
                }
                *dwError = WSAGetLastError();
            }
            *dwError = WSAGetLastError();

        }
        *dwError = WSAGetLastError();
    } while (FALSE);
    return bRet;
}

//接受client請求線程
unsigned int __stdcall ThreadAccept(void* lparam)
{
    WSANETWORKEVENTS networkEvents; //網絡事件結構
    while (iTotal < WSA_MAXIMUM_WAIT_EVENTS)  //這個值是64
    {
        if (0 == WSAEnumNetworkEvents(g_sServer, g_hServerEvent, &networkEvents))
        {
            if (networkEvents.lNetworkEvents & FD_ACCEPT) //如果等於FD_ACCEPT,相與就為1
            {
                if (0 == networkEvents.iErrorCode[FD_ACCEPT_BIT])  //檢查有無網絡錯誤
                {
                    //接受請求
                    SOCKADDR_IN addrServer = { 0 };
                    int iaddrLen = sizeof(addrServer);
                    g_sClient[iTotal] = accept(g_sServer, (SOCKADDR*)&addrServer, &iaddrLen);
                    if (g_sClient[iTotal] == INVALID_SOCKET)
                    {
                        printf("accept failed with error code: %d\n", WSAGetLastError());
                        return 1;
                    }
                    //為新的client注冊網絡事件
                    g_event[iTotal] = WSACreateEvent();
                    WSAEventSelect(g_sClient[iTotal], g_event[iTotal], FD_READ | FD_WRITE | FD_CLOSE);
                    iTotal++;
                    printf("accept a connection from IP: %s,Port: %d\n", inet_ntoa(addrServer.sin_addr), htons(addrServer.sin_port));
                }
                else  //錯誤處理
                {
                    int iError = networkEvents.iErrorCode[FD_ACCEPT_BIT];
                    printf("WSAEnumNetworkEvents failed with error code: %d\n", iError);
                    return 1;
                }
            }
        }
        Sleep(100);
    }
    return 0;
}

//接收數據
unsigned int __stdcall ThreadRecv(void* lparam)
{
    char* buf = (char*)malloc(sizeof(char) * 128);
    while (1)
    {
        if (iTotal == 0)
        {
            Sleep(100);
            continue;
        }
        //等待網絡事件
        DWORD dwIndex = WSAWaitForMultipleEvents(iTotal, g_event, FALSE, 1000, FALSE); 
        //當前的事件對象
        WSAEVENT curEvent = g_event[dwIndex];
        //當前的套接字
        SOCKET sCur = g_sClient[dwIndex];
        //網絡事件結構
        WSANETWORKEVENTS networkEvents;
        if (0 == WSAEnumNetworkEvents(sCur, curEvent, &networkEvents))
        {
            if (networkEvents.lNetworkEvents & FD_READ)  //有數據可讀
            {
                if (0 == networkEvents.iErrorCode[FD_READ_BIT])
                {
                    memset(buf, 0, sizeof(buf));
                    int iRet = recv(sCur, buf, sizeof(buf), 0);  //接收數據
                    if (iRet != SOCKET_ERROR)
                    {
                        if (strlen(buf) != 0)
                            printf("Recv: %s\n", buf);
                    }
                }
                else //錯誤處理
                {
                    int iError = networkEvents.iErrorCode[FD_ACCEPT_BIT];
                    printf("WSAEnumNetworkEvents failed with error code: %d\n", iError);
                    break;
                }
            }
            else if (networkEvents.lNetworkEvents & FD_CLOSE)  //client關閉
                printf("%d downline\n", sCur);
        }
        Sleep(100);
    }
    if (buf)
        free(buf);
    return 0;
}

int main()
{
    DWORD dwError = 0;
    if (OpenTCPServer(18000, &dwError))
    {
        _beginthreadex(NULL, 0, ThreadAccept, NULL, 0, NULL);
        _beginthreadex(NULL, 0, ThreadRecv, NULL, 0, NULL);
    }
    Sleep(100000000);
    closesocket(g_sServer);
    WSACleanup();
    return 0;
}

 

 

 

二.重疊IO模型

 

1-重疊模型的優點

 

1可以運行在支持Winsock2的所有Windows平台,而不像完成端口只支持NT系統

 

2比起阻塞,select,WSAAsyncSelect以及WSAEventSelect等模型,重疊I/O(Overlapped I/O)模型使應用程序能達到更加系統性能

 

因為他和其他4種模型不同的是,使用重疊模型的應用程序通知緩沖區收發系統直接使用數據,也就是說,如果應用程序

 

投遞了一個10kb大小的緩沖區來接收數據,而數據已經到達套接字,則將該數據直接拷貝到投遞的緩沖區,

 

而4種模型中,數據達到並拷貝到單套接字接收緩沖區,此時應用程序會被告知可以讀入的容量,當應用程序調用

 

接收函數之后,數據才從單套接字緩沖區拷貝應用程序到緩沖區,差別就體現了。

 

 

 

2-重疊模型的基本原理

 

重疊模型是讓應用程序使用重疊數據結構(WSAOVERLAPPED),一次投遞一個或多個Winsock I/O請求,針對這些提交的

 

請求,在他們完成之后,應用程序會收到通知,於是就可通過自己的代碼來處理這些數據了。

 

使用事件通知的方法來實現重疊IO模型,基於事件的話,就要求將Win事件與WSAOVERLAPPED結構關聯在一起,

 

使用重疊結構,常用的send,sendto,recv,recvform也被WSASend,WSARecv等替換掉,

 

OVERLAPPER SOCKET(重疊Socket)上進行重疊發送的操作,(簡單的理解就是異步send,recv)

 

他們的參數中都有一個Overlapped參數,就是說所有的重疊Socket都要綁定到這個重疊結構體上,

 

提交一個請求,其他的事情就交給重疊結構去操心, 而其中重疊結構要與Windows事件綁定在一起, 

 

在樣,我們調用完WSARecv后.等重疊操作完成,就會有對應的事件來同意我們操作完成,

 

3-重疊模型的函數詳解

(1)創建套接字

     要使用重疊I/O模型,在創建套接字時,必須使用WSASocket函數,設置重疊標志。

 

  

 

The WSASocket function creates a socket that is bound to a specific transport-service provider.

SOCKET WSASocket(
  __in          int af,
  __in          int type,
  __in          int protocol,//前三個參數與socket函數相同
  __in          LPWSAPROTOCOL_INFO lpProtocolInfo,  //指定下層服務提供者   ,可以是NULL
  __in          GROUP g,    //保留
  __in          DWORD dwFlags //指定套接字屬性。要使用重疊I/O模型,必須指定WSA_FLAG_OVERLAPPED
);

由於要用到重疊模型來提交我們的操作,所以原來的recv、send、sendto、recvfrom等函數都要被替換為WSARecv、WSASend、WSASendto、WSARecvFrom函數來代替。

 

(2)傳輸數據

     在重疊I/O模型中,傳輸數據的函數是WSASend\WSARecv(TCP)和WSASendTo、WSARecvFrom等,下面是WSASend的定義:

    

The WSASend function sends data on a connected socket.

int WSASend(
  __in          SOCKET s,
  __in          LPWSABUF lpBuffers,
  __in          DWORD dwBufferCount,
  __out         LPDWORD lpNumberOfBytesSent,
  __in          DWORD dwFlags,
  __in          LPWSAOVERLAPPED lpOverlapped,
  __in          LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

參數

s:標識一個已連接套接口的描述字。
lpBuffers:一個指向WSABUF結構數組指針。每個WSABUF結構包含緩沖區的指針和緩沖區的大小。
dwBufferCount:lpBuffers數組中WSABUF結構的數目。
lpNumberOfBytesSent:如果發送操作立即完成,則為一個指向所發送數據字節數的指針。
dwFlags:標志位。
lpOverlapped:指向WSAOVERLAPPED結構的指針(對於非重疊套接口則忽略)。
lpCompletionRoutine:一個指向發送操作完成后調用的完成例程的指針。(對於非重疊套接口則忽略)
 

返回值

若無錯誤發生且發送操作立即完成,則WSASend()函數返回0。這時,完成例程(Completion Routine)應該已經被調度,一旦調用線程處於alertable狀態時就會調用它。否則,返回SOCKET_ERROR 。通過WSAGetLastError獲得詳細的錯誤代碼。WSA_IO_PENDING 這個錯誤碼(其實表示沒有錯誤)表示重疊操作已經提交成功(就是異步IO的意思了),稍后會提示完成(這個完成可不一定是發送成功,沒准出問題也不一定)。其他的錯誤代碼都代表重疊操作沒有正確開始,也不會有完成標志出現。
 
        

   可以異步接收連接請求的函數是AcceptEX。這是一個Mincrosoft擴展函數,它接受一個新的連接,返回本地和遠程地址,取得客戶程序發送的第一塊數據,函數定義如下:

 
        

The AcceptEx function accepts a new connection, returns the local and remote address, and receives the first block of data sent by the client application.

 
        

Note  This function is a Microsoft-specific extension to the Windows Sockets specification.

 
        
BOOL AcceptEx(
  __in          SOCKET sListenSocket,
  __in          SOCKET sAcceptSocket,
  __in          PVOID lpOutputBuffer,
  __in          DWORD dwReceiveDataLength,
  __in          DWORD dwLocalAddressLength,
  __in          DWORD dwRemoteAddressLength,
  __out         LPDWORD lpdwBytesReceived,
  __in          LPOVERLAPPED lpOverlapped
);


參數

sListenSocket
[in]偵聽套接字。服務器應用程序在這個套接字上等待連接。
sAcceptSocket
[in]將用於連接的套接字。此套接字必須不能已經綁定或者已經連接。
lpOutputBuffer
[in]指向一個緩沖區,該緩沖區用於接收新建連接的所發送數據的第一個塊、該服務器的本地地址和客戶端的遠程地址。接收到的數據將被寫入到緩沖區0偏移處,而地址隨后寫入。 該參數必須指定,如果此參數設置為NULL,將不會得到執行,也無法通過GetAcceptExSockaddrs函數獲得本地或遠程的地址。
dwReceiveDataLength
[in]lpOutputBuffer字節數,指定接收數據緩沖區lpOutputBuffer的大小。這一大小應不包括服務器的本地地址的大小或客戶端的遠程地址,他們被追加到輸出緩沖區。如果dwReceiveDataLength是零,AcceptEx將不等待接收任何數據,而是盡快建立連接。
dwLocalAddressLength
[in]為本地地址信息保留的字節數。此值必須比所用傳輸協議的最大地址大小長16個字節。
dwRemoteAddressLength
[in]為遠程地址的信息保留的字節數。此值必須比所用傳輸協議的最大地址大小長16個字節。 該值不能為0。
dwBytesReceived
[out]指向一個DWORD用於標識接收到的字節數。此參數只有在同步模式下有意義。如果函數返回ERROR_IO_PENDING並在遲些時候完成操作,那么這個DWORD沒有意義,這時你必須獲得從完成通知機制中讀取操作字節數。
lpOverlapped
[in]一個OVERLAPPED結構,用於處理請求。此參數必須指定,它不能為空。
返回值
如果沒有錯誤發生,AcceptEx函數成功完成並返回TRUE。 [1] 
如果函數失敗,AcceptEx返回FALSE。可以調用WSAGetLastError函數獲得擴展的錯誤信息。如果WSAGetLastError返回ERROR_IO_PENDING,那么這次行動成功啟動並仍在進行中。

AcceptEX函數將幾個套接字函數的功能集合在一起。如果它投遞的請求成功完成,則執行了如下3個操作:

(1)接受了新的連接

(2)新連接的本地地址和遠程地址都會返回

(3)接收到了遠程主機發來的第一塊數據

AcceptEX和大家熟悉的accept函數有很大的不同就是AcceptEX函數需要調用者提供兩個套接字,一個指定了在哪個套接字上監聽,另一個指定了在哪個套接字上接受連接,也就是說,AcceptEX不會像accept函數一樣為新的連接創建套接字。

   如果提供了新的緩沖區,AcceptEX投遞的重疊操作直到接受到連接並且讀到數據之后才會返回。以SO_CONNECT_TIME為參數調用getsockopt函數可以檢查到是否接受了連接,如果接受了連接,這個調用還可以取得連接已經建立了多長時間。

  AcceptEX函數是從Mswsock.lib庫中導出的,為了能夠直接調用它,而不用鏈接到Mswsock.lib庫,需要使用WSAIoctl函數將AcceptEX函數加載到內存,WSAIoctl函數是ioctlsocket函數的擴展,它可以使用重疊I/O。函數的第3個到第6個參數是輸入和輸出緩沖區,在這里傳遞AcceptEX函數的指針


(4)接收傳輸結果

當重疊I/O請求最終完成以后,以之關聯的事件對象受信,等待函數返回,應用程序可以使用WSAGetOverlappedResult函數取得重疊操作的結果,函數用法如下:

The WSAGetOverlappedResult function retrieves the results of an overlapped operation on the specified socket.

BOOL WSAAPI WSAGetOverlappedResult(
  __in          SOCKET s,
  __in          LPWSAOVERLAPPED lpOverlapped,
  __out         LPDWORD lpcbTransfer,
  __in          BOOL fWait,
  __out         LPDWORD lpdwFlags
);
參數:
s:標識套接口。這就是調用重疊操作(WSARecv()WSARecvFrom()、WSASend()、WSASendTo() 或 WSAIoctl())時指定的那個套接口
lpOverlapped:指向調用重疊操作時指定的WSAOVERLAPPED結構。
lpcbTransfer:指向一個32位變量,該變量用於存放一個發送或接收操作實際傳送的字節數,或WSAIoctl()傳送的字節數。
fWait:指定函數是否等待掛起的重疊操作結束。若為真TRUE則函數在操作完成后才返回。若為假FALSE且函數掛起,則函數返回FALSE,WSAGetLastError()函數返回 WSA_IO_INCOMPLETE。
lpdwFlags:指向一個32位變量,該變量存放完成狀態的附加標志位。如果重疊操作為 WSARecv()或WSARecvFrom(),則本參數包含lpFlags參數所需的結果。
返回值:
如果函數成功,則返回值為真TRUE。它意味着重疊操作已經完成,lpcbTransfer所指向的值已經被刷新。應用程序可調用WSAGetLastError()來獲取重疊操作的錯誤信息
如果函數失敗,則返回值為假FALSE。它意味着要么重疊操作未完成,要么由於一個或多個參數的錯誤導致無法決定完成狀態。失敗時,lpcbTransfer指向的值不會被刷新。應用程序可用WSAGetLastError()來獲取失敗的原因。

4-重疊模型的實例代碼:
//完成例程實現重疊io模型偽代碼
SOCKET acceptSock;
WSABUF dataBuf;

void main()
{
    WSAOVERLAPPED overlapped;
    //1.初始化
    //...

    //2.接收連接請求
    acceptSock=accept(listenSock,NULL,NULL);

    //3.初始化重疊結構
    UINT flag=0;
    ZeroMemory(&overlapped,sizeof(WSAOVERLAPPED));
    dataBuf.len=DATA_BUFSIZE;
    dataBuf.buf=buf;

    if (WSARecv(acceptSock,&dataBuf,1,&recvBytes,&flag,&overlapped,workroutine)==SOCKET_ERROR)//最后一個參數時回調函數地址
    {
        if(WSAGetLastError()!=WSA_IO_PENDING)
        {
            printf("WSARecv() failed with error %d\n",WSAGetLastError());
            return;
        }
    }
    
    //創建事件
    eventArray[0]=WSACreateEvent();
    while (true)
    {
        int index=WSAWaitForMultipleEvents(1,eventArray,FALSE,WSA_INFINITE,TRUE);//最后一個參數最好為true
        if (index==WAIT_IO_COMPLETION)//io請求完成
        {
            break;
        }
        else//io請求出錯
        {
            return;
        }
    }
    //調用回調函開始進行處理
}

void CALLBACK WorkRoutine(DWORD error,DWORD bytesTransferred,LPWSAOVERLAPPED overlapped,DWORD inflag)
{
    DWORD sendBytes,recvBytes;
    DWORD flags;

    if(error!=0||bytesTransferred==0)
    {
        closesocket(acceptSock);
        return;
    }

    flags=0;

    ZeroMemory(&overlapped,sizeof(WSAOVERLAPPED));
    dataBuf.len=DATA_BUFSIZE;
    dataBuf.data=buf;

    if (WSARecv(acceptSock,&dataBuf,1,&recvBytes,&flag,&overlapped,workroutine)==SOCKET_ERROR)//最后一個參數時回調函數地址
    {
        if(WSAGetLastError()!=WSA_IO_PENDING)
        {
            printf("WSARecv() failed with error %d\n",WSAGetLastError());
            return;
        }
    }
}

 

 最后說一句啦。本網絡編程入門系列博客是連載學習的,有興趣的可以看我博客其他篇。。。。c++ 網絡編程課設入門超詳細教程 ---目錄


參考博客:https://www.cnblogs.com/Dreamcaihao/archive/2012/11/14/2770293.html
參考博客:https://www.cnblogs.com/tanguoying/p/8506821.html
參考博客:https://blog.csdn.net/wxf2012301351/article/details/73332588
參考書籍:《TCP/IP網絡編程 ---尹聖雨》

 

若有興趣交流分享技術,可關注本人公眾號,里面會不定期的分享各種編程教程,和共享源碼,諸如研究分享關於c/c++,python,前端,后端,opencv,halcon,opengl,機器學習深度學習之類有關於基礎編程,圖像處理和機器視覺開發的知識


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM