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

 

原文做者:aircrafthtml

原文連接:https://www.cnblogs.com/DOMLX/p/9662931.html前端

 

 

一.異步IO模型(asynchronous IO)python

(1)什麼是異步I/Oc++

異步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,機器學習深度學習之類有關於基礎編程,圖像處理和機器視覺開發的知識

相關文章
相關標籤/搜索