完成端口與AcceptEx

關於IOCP網上到處都是資料,說的也很詳細。我在這裏就不再多說了,這只是本人在學習IOCP時的筆記,和配合AcceptEx寫的一個極小的服務端程序。由於剛剛接觸ICOP加上本人剛畢業不到一年,所以裏面的理解或觀點可能有誤,還請大家多多批評!

     VC6.0開發,旨在體現IOCP的架構,忽略細節,服務程序的功能只是接收客戶連接,接着接收到客戶數據,然後原封不動的返回給客戶!

下面這段話,如果不感興趣,可以跳過去,直接看代碼...

先說IOCP,其實思路很清晰:

     1.聲明一個數據結構,用來存放客戶套接字和客戶信息

     2.聲明一個包含OVERLAPPED字段的I/O結構

     3.創建完成端口

     4.創建服務線程

     5.接收客戶端連接請求

     6.關聯這個套接字到完成端口中

     7.服務線程中不斷的等待I/O結果,在結果中提供服務和根據需要發起另一個異步操作。

     按照這個思路很快的寫出了一個服務程序,但是遇到了下面的問題:

     1.WSAGetLastError()返回10045,找了半天才發現發起重疊操作時候,WSARecv中flag參數沒有初始化,需要初始化賦值爲0。

     2.在GetQueuedCompletionStatus中,沒有錯誤,但總是返回讀取的字數爲0。I/O重疊結構中也收不到任何字符。這時候我就在這裏用了一下recv()函數,在recv中卻可以收到來自客戶端發送的數據。難道每次都要自己recv()?肯定不是!如果那樣還用擴展的I/O結果何用。一定是哪裏指定了接收的數目,而自己不小心指定爲0了,所以沒有接收數據。找了半天果然如此。在發起重疊操作時候,擴展的I/O中WSABUF的賦值有問題。

     我的錯誤:wsaBuf.len = (I/O結構).len;

     改爲:       wsaBuf.len = (I/O結構).len = DATABUF_SIZE;

     修改之後終於可以接收和發送數據了。

爲什麼要用AcceptEx?

     在學習IOCP時,看到一位大神寫的文章,他用客戶端開了3W個線程同時連接服務端和發送數據,我好奇就也開了3W個線程去同時連接服務端,結果很多都printf連接失敗的信息!再看看大神的文章,再搜一下AcceptEx。對比accept,覺得AcceptEx確實很強大。AcceptEx和accept主要的區別就在於接收套接字:

     accept函數是等待客戶連接進來之後才創建套接字,雖然在我們看到的就是一個socket函數,但是在函數背後,系統應該會消耗不少資源,因爲它要打通一個和外界通訊的路。如果大量套接字併發接入,難免有的套接字不能及時創建和接收。

     AcceptEx則是事先創建好套接字,坐等客戶端的連接就行了。

     但是,AcceptEx相比accept確實複雜了很多。原來一句accept就可以解決的,現在卻要爲AcceptEx做很多服務,但是隻要理清思路,這個做起來也是很從容的。

     1.創建一個監聽套接字

     2.將監聽套接字關聯到完成端口中

     3.對監聽套接字調用bind()、listen()

     4.通過WSAIoctl獲取AcceptEx、GetAcceptExSockaddrs函數的指針

     5.創建一個用於接收客戶連接的套接字

     6.用獲取到的AcceptEx函數指針發起用於接收連接的異步操作

     7.服務器接收到連接的套接字,設置一下它的屬性(有人說沒有必要)。用這個接收到的套接字去發起重疊的I/O操作。

     8.多次重複5,6就是多次發起接收連接的異步操作的過程。

     對於第4步,爲什麼要獲取AcceptEx的指針,而不是直接就調用AcceptEx這個函數呢?網上找到的資料是這麼說的:

     Winsock2的其他供應商不一定會實現AcceptEx函數。同樣情況也包括的其他Microsoft的特定APIs如TransmitFile,GetAcceptExSockAddrs以及其他Microsoft將在以後版本的windows裏。

在運行WinNT和Win2000的系統上,這些APIs在Microsoft提供的DLL(mswsock.dll)裏實現,可以通過鏈接mswsock.lib或者通過WSAioctl的SIO_GET_EXTENSION_FUNCTION_POINTER操作動態調用這些擴展APIs.

未獲取函數指針就調用函數(如直接連接mswsock.lib並直接調用AcceptEx)的消耗是很大的,因爲AcceptEx實際上是存在於Winsock2結構體系之外的。每次應用程序常試在服務提供層上(mswsock之上)調用AcceptEx時,都要先通過WSAIoctl獲取該函數指針。如果要避免這個很影響性能的操作,應用程序最好是直接從服務提供層通過WSAIoctl先獲取這些APIs的指針。

這樣一來,大家就不覺得這個複雜的函數WSAloctl那麼讓人心煩了吧!至於調用失敗後所返回的錯誤代碼,百度百科中介紹的很詳細!

使用AcceptEx後:

     在使用AcceptEx後,併發2000個套接字去連接客戶端,不再出現連接失敗的消息了。

     但是,你肯定會說人家3W個,你這2000個不能說明問題。開始我也一直在嘗試同時併發3W個線程,可是發現公司機器最多時候也就1573個連接,家裏筆記本差不多2000個。這是怎麼會事呢?於是搜資料查到一個進程最多可以開啓的理論線程數是2048個線程,而且實際情況下通常小於這個值,這樣在一個進程裏面怎麼可能有3W個連接啊!忍不住好奇就下了http://blog.csdn.net/piggyxp/article/details/6922277大神的IOCP客戶端demo,發現並不是同時併發3W個,用任務管理器看併發最多時候線程數並沒有超過1K(無意冒犯大神,只是個人的愚見,我學習IOCP也是大部分都是從大神的文章中學習到的,所以先要感謝大神的奉獻,同時如果(不是如果,是肯定)我的理解有錯誤,希望大家不吝賜教,多多批評,鄙人一定感激萬分)。

     爲了驗證IOCP是否有那麼強的能力,我的客戶端沒有做成連接到服務端一個套接字,再創建一個線程,傳遞套接字到線程的方式。而是,主線程直接創建2000個線程,在每個線程中去連接服務器(覺得這樣更能體現併發連接),多開幾個客戶端,每個客戶端的連接數爲最大線程數,服務端同時處理的連接數爲12562(開更多的線程連接數更多,有興趣的可以試一下)。下面是360的流量管理下面的截圖:

我註釋掉了接收數據後printf接收到的數據,因爲發現如果連接過多,一直printf服務器就掛掉了,不知道改成mfc會不會好點...

     下面是服務器代碼:

[cpp] view plain copy

#include "stdafx.h"  
#include <Afx.h>  
#include <Windows.h>  
#include <Winsock2.h>  
#pragma comment(lib, "WS2_32.lib")  

#include <mswsock.h>    //微軟擴展的類庫  

#define DATA_BUFSIZE 100  
#define READ   0  
#define WRITE  1  
#define ACCEPT 2  

DWORD g_count = 0;  
//擴展的輸入輸出結構  
typedef struct _io_operation_data  
{  
    OVERLAPPED  overlapped;  
    WSABUF      databuf;  
    CHAR        buffer[DATA_BUFSIZE];  
    BYTE        type;  
    DWORD       len;  
    SOCKET      sock;  
}IO_OPERATION_DATA, *LP_IO_OPERATION_DATA;  

//完成鍵  
typedef struct _completion_key  
{  
    SOCKET sock;  
    char   sIP[30];     //本機測試,IP都是127.0.0.1,沒啥意思,實際寫時候這個值填的是端口號  
}COMPLETION_KEY, *LP_COMPLETION_KEY;  

///////////////////////////////////////////////////  
//完成端口句柄  
HANDLE g_hComPort = NULL;  
BOOL   g_bRun = FALSE;  

BOOL AcceptClient(SOCKET sListen);      //發起接收連接操作  
BOOL Recv(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIO); //發起接收操作  
BOOL Send(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIO); //發起發送操作  

//處理重疊結果  
BOOL ProcessIO(IO_OPERATION_DATA *pIOdata, COMPLETION_KEY *pComKey);  

//////////////////////////////////////////////////  
//服務線程  
DWORD WINAPI ServerWorkerThread( LPVOID pParam );  

//////////////////////////////////////////////////  
LPFN_ACCEPTEX lpfnAcceptEx = NULL;                   //AcceptEx函數指針  
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;  //加載GetAcceptExSockaddrs函數指針  

///////////////////////////////////////////////////  
//監聽套接字,其實也不一定要是全局的。用於接收到連接後繼續發起等待連接操作。  
SOCKET g_sListen;  

int main(int argc, char* argv[])  
{  
    g_bRun = TRUE;  

    //創建完成端口  
    g_hComPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );  
    if( g_hComPort == NULL )  
    {  
        printf("Create completionport error!   %d\n", WSAGetLastError() );  
        return 0;  
    }  

    //創建服務線程  
    SYSTEM_INFO sysInfor;  
    GetSystemInfo( &sysInfor );  
    int i=0;  
    for(i = 0; i < sysInfor.dwNumberOfProcessors * 2; i++)  
//  if(true)  
    {  
        HANDLE hThread;  
        DWORD  dwThreadID;  

        hThread = CreateThread( NULL, 0, ServerWorkerThread, g_hComPort, 0, &dwThreadID );  
        CloseHandle( hThread );  
    }  

    //加載套接字庫  
    WSADATA wsData;  
    if( 0 != WSAStartup( 0x0202, &wsData ) )  
    {  
        printf("加載套接字庫失敗!   %d\n", WSAGetLastError() );  
        g_bRun = FALSE;  
        return 0;  
    }  

    ////////////////////////////////////////////////////////////////////  
    //等待客戶端連接  

    //先創建一個套接字用於監聽  
    SOCKET sListen = WSASocket( AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED );  
    g_sListen = sListen;  

    //將監聽套接字與完成端口綁定  
    LP_COMPLETION_KEY pComKey;      //完成鍵  
    pComKey = (LP_COMPLETION_KEY) GlobalAlloc ( GPTR, sizeof(COMPLETION_KEY) );  
    pComKey->sock = sListen;  
    CreateIoCompletionPort( (HANDLE)sListen, g_hComPort, (DWORD)pComKey, 0 );  

    //監聽套接字綁定監聽  
    SOCKADDR_IN serAdd;  
    serAdd.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  
    serAdd.sin_family = AF_INET;  
    serAdd.sin_port = htons( 6000 );  

    bind( sListen, (SOCKADDR*)&serAdd, sizeof(SOCKADDR) );  
    listen( sListen, 5 );  
    if( sListen == SOCKET_ERROR )  
    {  
        goto STOP_SERVER;  
    }  

    /////////////////////////////////////////////////////////////////////  
    //使用WSAIoctl獲取AcceptEx函數指針  
    if( true )  
    {  
        DWORD dwbytes = 0;  
        //Accept function GUID  
        GUID guidAcceptEx = WSAID_ACCEPTEX;  

        if( 0 != WSAIoctl( sListen, SIO_GET_EXTENSION_FUNCTION_POINTER,   
            &guidAcceptEx, sizeof(guidAcceptEx),   
            &lpfnAcceptEx, sizeof(lpfnAcceptEx),   
            &dwbytes, NULL, NULL) )  
        {  
            //百度百科,有關該函數的所有返回值都有!  
        }  

        // 獲取GetAcceptExSockAddrs函數指針,也是同理  
        GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;  
        if( 0 != WSAIoctl( sListen, SIO_GET_EXTENSION_FUNCTION_POINTER,   
            &guidGetAcceptExSockaddrs,  
            sizeof(guidGetAcceptExSockaddrs),   
            &lpfnGetAcceptExSockaddrs,   
            sizeof(lpfnGetAcceptExSockaddrs),     
            &dwbytes, NULL, NULL) )    
        {    
        }    
    }  

    //發起接收的異步操作  
    for(i=0; i<2000; i++ )  
    {  
        AcceptClient(sListen);    
    }  

    //不讓主線程退出  
    while( g_bRun )  
    {  
        Sleep(1000);  
    }  

STOP_SERVER:  
    closesocket( sListen );  
    g_bRun = FALSE;  
    WSACleanup();  

    return 0;  
}  

/////////////////////////////////////////////////////////////////////////  
//服務線程  
DWORD WINAPI ServerWorkerThread( LPVOID pParam )  
{  
    HANDLE  completionPort = (HANDLE)pParam;  
    DWORD   dwIoSize;  

    COMPLETION_KEY        *pComKey;     //完成鍵  
    LP_IO_OPERATION_DATA  lpIOoperData;     //I/O數據  

    //用於發起接收重疊操作  
    BOOL bRet;  

    while( g_bRun )  
    {  
        bRet = FALSE;  

        dwIoSize = -1;  
        pComKey = NULL;  
        lpIOoperData = NULL;  

        bRet = GetQueuedCompletionStatus( g_hComPort, &dwIoSize, (LPDWORD)&pComKey, (LPOVERLAPPED*)&lpIOoperData,INFINITE );  

        if( !bRet )  
        {  
            DWORD dwIOError = GetLastError();  
            if( WAIT_TIMEOUT == dwIOError )  
            {  
                continue;  
            }  
            else if( NULL != lpIOoperData )  
            {  
                CancelIo( (HANDLE)pComKey->sock );   //取消等待執行的異步操作  
                closesocket(pComKey->sock);                
                GlobalFree( pComKey );        
            }  
            else  
            {  
                g_bRun = FALSE;  
                break;  
            }  
        }  
        else  
        {         
            if( 0 == dwIoSize  && (READ==lpIOoperData->type || WRITE==lpIOoperData->type) )  
            {  
                printf("客戶斷開了連接!\n");  
                CancelIo( (HANDLE)pComKey->sock );   //取消等待執行的異步操作  
                closesocket(pComKey->sock);                
                GlobalFree( pComKey );  
                GlobalFree( lpIOoperData );  
                continue;  
            }  
            else   
            {  
                ProcessIO( lpIOoperData, pComKey );  
            }  
        }  
    }  

    return 0;  
}  

BOOL ProcessIO(IO_OPERATION_DATA *pIOoperData, COMPLETION_KEY *pComKey)  
{  
    if( pIOoperData->type == READ )  
    {  
        //打印接收到的內容  
    //  char ch[100] = { 0 };  
    //  sprintf(ch, "%s  :  %s", pComKey->sIP, pIOoperData->buffer);  
    //  printf( ch );  
        Send( pComKey, pIOoperData );   //將接收到的內容原封不動的發送回去  
    }  
    else if( pIOoperData->type == WRITE )  
    {  
        Recv( pComKey, pIOoperData );   //發起接收操作  
    }  
    else if( pIOoperData->type == ACCEPT )  
    {   //使用GetAcceptExSockaddrs函數 獲得具體的各個地址參數.  
        printf("accept sucess!\n");  
        setsockopt( pIOoperData->sock, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char*)&(pComKey->sock), sizeof(pComKey->sock) );  

        LP_COMPLETION_KEY pClientComKey = (LP_COMPLETION_KEY) GlobalAlloc ( GPTR, sizeof(COMPLETION_KEY) );  
        pClientComKey->sock = pIOoperData->sock;  

        SOCKADDR_IN *addrClient = NULL, *addrLocal = NULL;  
        int nClientLen = sizeof(SOCKADDR_IN), nLocalLen = sizeof(SOCKADDR_IN);  

        lpfnGetAcceptExSockaddrs(pIOoperData->buffer, 0,   
            sizeof(SOCKADDR_IN)+16, sizeof(SOCKADDR_IN)+16,   
            (LPSOCKADDR*)&addrLocal, &nLocalLen,  
            (LPSOCKADDR*)&addrClient, &nClientLen);  

        sprintf(pClientComKey->sIP, "%d", addrClient->sin_port ); //cliAdd.sin_port );  
        printf(pClientComKey->sIP );  

        CreateIoCompletionPort( (HANDLE)pClientComKey->sock, g_hComPort, (DWORD)pClientComKey, 0 );  //將監聽到的套接字關聯到完成端口  

        Recv( pClientComKey, pIOoperData );  

    //  char s[30] = {0};  
    //  sprintf( s, "%d\n", g_count++ );  
    //  printf(s);  
        //接收到一個連接,就再發起一個異步操作!  
        AcceptClient( g_sListen );    
    }  

    return TRUE;  
}  

BOOL AcceptClient(SOCKET sListen)  
{     
    DWORD dwBytes;  

    LP_IO_OPERATION_DATA pIO;  
    pIO = (LP_IO_OPERATION_DATA) GlobalAlloc (GPTR, sizeof(IO_OPERATION_DATA));   
    pIO->databuf.buf = pIO->buffer;  
    pIO->databuf.len = pIO->len = DATA_BUFSIZE;  
    pIO->type = ACCEPT;  

    //先創建一個套接字(相比accept有點就在此,accept是接收到連接才創建出來套接字,浪費時間. 這裏先準備一個,用於接收連接)  
    pIO->sock = WSASocket( AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED );  

    //調用AcceptEx函數,地址長度需要在原有的上面加上16個字節  
    //向服務線程投遞一個接收連接的的請求  
    BOOL rc = lpfnAcceptEx( sListen, pIO->sock,  
        pIO->buffer, 0,   
        sizeof(SOCKADDR_IN)+16, sizeof(SOCKADDR_IN)+16,   
        &dwBytes, &(pIO->overlapped) );  

    if( FALSE == rc )  
    {  
        if( WSAGetLastError() != ERROR_IO_PENDING )  
        {  
            printf("%d", WSAGetLastError() );  
            return false;  
        }  
    }  

    return true;  
}  

BOOL Recv(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIOoperData)  
{  
    DWORD flags = 0;  
    DWORD recvBytes = 0;  
    ZeroMemory( &pIOoperData->overlapped, sizeof(OVERLAPPED) );  

    pIOoperData->type = READ;      
    pIOoperData->databuf.buf = pIOoperData->buffer;  
    pIOoperData->databuf.len = pIOoperData->len = DATA_BUFSIZE;  

    if( SOCKET_ERROR == WSARecv( pComKey->sock, &pIOoperData->databuf, 1, &recvBytes, &flags,  &pIOoperData->overlapped, NULL) )  
    {  
        if( ERROR_IO_PENDING != WSAGetLastError() )  
        {  
            printf("發起重疊接收失敗!   %d\n", GetLastError() );  
            return FALSE;  
        }  
    }  
    return TRUE;  
}  

BOOL Send(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIOoperData)  
{  
    DWORD flags = 0;  
    DWORD recvBytes = 0;  
    ZeroMemory( &pIOoperData->overlapped, sizeof(OVERLAPPED) );  

    pIOoperData->type = WRITE;  
    pIOoperData->databuf.len = 100;        

    if( SOCKET_ERROR == WSASend( pComKey->sock, &pIOoperData->databuf, 1, &recvBytes, flags,  &pIOoperData->overlapped , NULL) )  
    {  
        if( ERROR_IO_PENDING != WSAGetLastError() )  
        {  
            printf("發起發送重疊接收失敗!\n");  
            return FALSE;  
        }  
    }  

    return TRUE;  
}  

對於客戶端就更簡單了,只是創建線程,請求連接,發送數據,接收數據
[cpp] view plain copy

#include "stdafx.h"  
#include <Afx.h>  
#include <Windows.h>  
#include <Winsock2.h>  
#pragma comment(lib, "WS2_32.lib")  

DWORD WINAPI Thread(LPVOID lParam);  

int main(int argc, char* argv[])  
{  
    WSADATA dwData;  
    WSAStartup( 0x0202, &dwData );  

    for( int i = 0; i < 2000; i++ )  
    {  
        HANDLE hThread = NULL;  
        hThread = CreateThread(NULL, 0, Thread, NULL, 0, 0);  
        CloseHandle(hThread);  
        hThread = NULL;  
    }  

    while( true )  
    {  
        Sleep(100);  
    }  

    return 0;  
}  

DWORD WINAPI Thread(LPVOID lParam)  
{  
    SOCKET sock = socket( AF_INET, SOCK_STREAM, 0 );  

    SOCKADDR_IN serAddr;  
    serAddr.sin_family = AF_INET;  
    serAddr.sin_port = htons(6000);  
    serAddr.sin_addr.S_un.S_addr = inet_addr(_T("127.0.0.1"));  

    int reVal = connect( sock, (SOCKADDR*)&serAddr, sizeof(SOCKADDR) );  
    if( reVal==SOCKET_ERROR )  
    {  
        printf("cannot client SERVER!   %d\n", WSAGetLastError());  
        return 0;  
    }     

    int i=0;  
    char buf[100] = _T("光陰的故事!\n");  
    while( true )  
    {             
        if( SOCKET_ERROR == send( sock, buf, 100, 0 ) )  
        {  
            printf("cannot SEND message to server!   %d\n", WSAGetLastError());  
            break;  
        }  

        memset( buf, 0, strlen(buf) );  //清空一下,體現是接收到的數據  

        if(  SOCKET_ERROR == recv( sock, buf, 100, 0 ) )  
        {  
            printf("cannot RECV message to server!   %d\n", WSAGetLastError());  
            break;  
        }  

    //  printf( buf );  
        Sleep(3000);          
    }  

    closesocket(sock);  
    return 0;  
}  

[cpp] view plain copy

將代碼貼到編譯器中即可,也可以下載這個demo http://download.csdn.net/detail/u010025913/7250965
代碼漏洞百出,希望大家多多批評指教!

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