gh0st的IOCP模型分析

xianjian_x發表於2016-05-14

在分析了那麼多IOCP相關api之後想把IOCP模型分析下,本人菜鳥一個,高手勿笑。

gh0st是單文件型別的程式框架。 文件型別的都是從theApp開始的。theApp是一個全域性變數。 那我們就先看一下CGh0stApp這個類的初始化函式 BOOL CGh0stApp::InitInstance()
下面很大一部分是生成的框架。我給大家指出來,就沒必要再看這些了

直到

if (!ProcessShellCommand(cmdInfo)) 
                return FALSE; 

都是框架。不去看。分析下面的。

((CMainFrame*) m_pMainWnd)->Activate(nPort, nMaxConnection); 

這句是呼叫CMainFrame類的Activate函式。 m_pMainWnd是單文件類的主介面指標,也是框架類指標。就是CMainFrame類 ,接下來我們就去Activate函式裡面看看 。

    m_iocpServer = new CIOCPServer;   /// 這裡呼叫了IOCPserver建構函式
    // 開啟IOCP伺服器, 初始化例程   
    if (m_iocpServer->Initialize(NotifyProc, this, 100000, nPort))

進入 Initialize 函式看下:

bool CIOCPServer::Initialize(NOTIFYPROC pNotifyProc, CMainFrame* pFrame, int nMaxConnections, int nPort)
{
    //// 建立套接字
    m_socListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

    // 建立事件   處理網路IO
    m_hEvent = WSACreateEvent();

    /// 在 m_socListen 套接字上接收 FD_ACCEPT 事件,關聯事件 和套接字 
    int nRet = WSAEventSelect(m_socListen,m_hEvent,FD_ACCEPT);  

    // 繫結 套接字
    nRet = bind(m_socListen, (LPSOCKADDR)&saServer, sizeof(struct sockaddr));

    // Set the socket to listen
    nRet = listen(m_socListen, SOMAXCONN);

    /// 開啟監聽執行緒 ListenThreadProc
    m_hThread = (HANDLE)_beginthreadex(NULL, 0,  ListenThreadProc,  (void*) this, 0, &dwThreadId);
    if (m_hThread != INVALID_HANDLE_VALUE)
    {
        //// 初始化完成埠
        InitializeIOCP();
    }
}

讓我們看下 監聽執行緒 ListenThreadProc 和 InitializeIOCP 函式都做了什麼。

首先看監聽執行緒:

unsigned CIOCPServer::ListenThreadProc(LPVOID lParam)
{
    while(1)
    {
        DWORD dwRet;

        /// 在這裡阻塞等待客戶端連線
        dwRet = WSAWaitForMultipleEvents(1, &pThis->m_hEvent, FALSE,100, FALSE);
        /// 列舉發生的事件
        int nRet = WSAEnumNetworkEvents(pThis->m_socListen, pThis->m_hEvent, &events);

        ///處理accept 事件
        if (events.lNetworkEvents & FD_ACCEPT)
        {
            if (events.iErrorCode[FD_ACCEPT_BIT] == 0)
                pThis->OnAccept();
        }
    } // while....
    return 0; // Normal Thread Exit Code...
}
void CIOCPServer::OnAccept()
{
    SOCKADDR_IN SockAddr;
    SOCKET      clientSocket;   
    int         nRet;
    int         nLen;

    /// 接收新的socket 描述符
    nLen = sizeof(SOCKADDR_IN);
    clientSocket = accept(m_socListen, (LPSOCKADDR)&SockAddr,&nLen); 

    // 建立ClientContext 結構體 來和完成埠繫結
    ClientContext* pContext = AllocateContext();
    pContext->m_Socket = clientSocket;
    pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;
    pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);

   /// 注意這裡把 通過 accept 得到的客戶端套接字 SockAddr 與 完成埠結合 
    AssociateSocketWithCompletionPort(clientSocket, m_hCompletionPort, (DWORD) pContext)

    /// 這裡觸發第一個 IO 完成請求 
    OVERLAPPEDPLUS  *pOverlap = new OVERLAPPEDPLUS(IOInitialize);

    BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);

    /// 空操作
    m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_CONNECT);

    //  投遞一個 recv 接收請求 ,到客戶端套接字
    PostRecv(pContext);
}

至此 ListenThreadProc 一個迴圈已經走完,接著看下InitializeIOCP 函式都做了些什麼

bool CIOCPServer::InitializeIOCP(void)
{

    SOCKET s;
    DWORD i;
    UINT  nThreadID;
    SYSTEM_INFO systemInfo;


    /// 建立被所有執行緒使用的完成埠,注意這裡是完成埠。
    /// 跟前面的建立的事件來接受 accept 還不一樣
    m_hCompletionPort = CreateIoCompletionPort( (HANDLE)s, NULL, 0, 0 );

    /// 這裡我們建立兩倍於 處理器的執行緒數量,因為每個執行緒不是時時刻刻都在工作,
    ///  還有處於阻塞狀態,所以執行緒個數最好比處理器個數多一些
    for ( i = 0; i < nWorkerCnt; i++ ) 
    {
        hWorker = (HANDLE)_beginthreadex(NULL,0,ThreadPoolFunc, (void*) this,0, &nThreadID);        
    }
    return true;
} 

  可以看出這個函式主要是建立一個完成埠,建立兩倍於處理器數量的 工作執行緒。再看下工作執行緒池 ThreadPoolFunc 都做些什麼(只分析主幹,細枝末節略過):

unsigned CIOCPServer::ThreadPoolFunc (LPVOID thisContext)    
{
    HANDLE hCompletionPort = pThis->m_hCompletionPort;

    for (BOOL bStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; ) 
    {
        // Get a completed IO request.
        BOOL bIORet = GetQueuedCompletionStatus(hCompletionPort, &dwIoSize, (LPDWORD) &lpClientContext, &lpOverlapped, INFINITE);

        DWORD dwIOError = GetLastError();
        pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, m_ol);

        if (!bError)
        {
            if(bIORet && NULL != pOverlapPlus && NULL != lpClientContext) 
            {
                try
                {
                    pThis->ProcessIOMessage(pOverlapPlus->m_ioType, lpClientContext, dwIoSize);
                }
                catch (...) {}
            }
        }
    }
    return 0;
} 

可以看出這個函式首先呼叫 GetQueuedCompletionStatus 阻塞一直 等到從完成埠取出一個成功I/O操作的完成包,然後呼叫 ProcessIOMessage 處理。

如果把巨集定義:

    BEGIN_IO_MSG_MAP()
        IO_MESSAGE_HANDLER(IORead, OnClientReading)
        IO_MESSAGE_HANDLER(IOWrite, OnClientWriting)
        IO_MESSAGE_HANDLER(IOInitialize, OnClientInitializing)
    END_IO_MSG_MAP()

展開就會發現上面其實就是通過 m_ioType 來區分分別呼叫 OnClientReading ,OnClientWriting 的。

bool CIOCPServer::OnClientReading(ClientContext* pContext, DWORD dwIoSize)
{
    CLock cs(CIOCPServer::m_cs, "OnClientReading");
    {
        if (dwIoSize == 0)                ///判斷是否是斷開
        {
            /// 移除客戶端
            RemoveStaleClient(pContext, FALSE);
            return false;
        }

        /// 判斷是否 是"gh0st" 標誌 
        if (dwIoSize == FLAG_SIZE && memcmp(pContext->m_byInBuffer, m_bPacketFlag, FLAG_SIZE) == 0)
        {
            // 重新傳送
            Send(pContext, pContext->m_ResendWriteBuffer.GetBuffer(), pContext->m_ResendWriteBuffer.GetBufferLen());
            // 必須再投遞一個接收請求
            PostRecv(pContext);
            return true;
        }

        /// 寫入資料到緩衝區
        pContext->m_CompressionBuffer.Write(pContext->m_byInBuffer,dwIoSize);

        /// 通知主框架處理 NC_RECEIVE 
        m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_RECEIVE);

        // Check real Data
        while (pContext->m_CompressionBuffer.GetBufferLen() > HDR_SIZE)
        {
                ///資料包 解壓縮
                if (nRet == Z_OK)     /// 資料包正確
                {
                    ///通知 主框架處理 NC_RECEIVE_COMPLETE
                    m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_RECEIVE_COMPLETE);
                }
        }
        // 投遞接收
        PostRecv(pContext);
    }
    return true;
}

看下 OnClientWriting 函式:

bool CIOCPServer::OnClientWriting(ClientContext* pContext, DWORD dwIoSize)
{
    try
    {
        if (pContext->m_WriteBuffer.GetBufferLen() == 0)    ///資料是否傳送完畢
        {
            pContext->m_WriteBuffer.ClearBuffer();
            // Write complete
            SetEvent(pContext->m_hWriteComplete);
            return true;            // issue new read after this one
        }
        else
        {
            OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite);

            ///投遞 傳送 請求
            int nRetVal = WSASend(pContext->m_Socket, 
                            &pContext->m_wsaOutBuffer,
                            1,
                            &pContext->m_wsaOutBuffer.len, 
                            ulFlags,
                            &pOverlap->m_ol, 
                            NULL);
        }
    }catch(...){}
    return false;           // issue new read after this one
}

至此大體框架分析完畢。

  對於IOCP要使用WSASocket建立支援重疊IO的套接字,對於這種套接字 要用WSAAccept 來等待客戶端的連線,這個函式是阻塞的。但gh0st裡面沒有用 WSAAccept 而是選擇了事件模型。

  每當 accept 到一個客戶端套接字的時候,就會呼叫函式 CreateIoCompletionPort把完成埠 hCompletionPort 與accept返回的套接字和CompletionKey(完成鍵)associate然後執行緒池所有執行緒在等待這個 hCompletionPort ,後面 WSASend ,WSARecv 操作的都是accept 到的那個套接字。

  如果很多客戶端連線過來之後,完成埠 hCompletionPort 會與很多個套接字associate,作業系統會維持他們之間的關係,當有一個套接字上面有IO事件之後,GetQueuedCompletionStatus就會返回,從 lpOverlapped 結構體知道是一次讀還是寫事件。
關於這一點MSDN文件上有說明:

Multiple file handles can be associated with a single I/O completion port by calling CreateIoCompletionPort multiple times with the same I/O completion port handle in the ExistingCompletionPort parameter and a different file handle in the FileHandle parameter each time

  在服務端起初呼叫的 WSARecv ,投遞一個讀請求 是很有用的,否則 完成埠佇列沒有請求,以後對完成埠 hCompletionPort 的請求都不會返回。

  對於服務端的 投遞 傳送請求 WSASend ,客戶端即便沒有接受 recv, 這個函式也會觸發 GetQueuedCompletionStatus。正如 IOCP知識點及疑惑 文中分析的一樣 ,這只是一個本地的過程。 但是對於 WSARecv ,需要客戶端send 之後才會返回 ,這是個CS互動的過程。

關於IOCP兩篇很好的參考文章:

IOCP知識點及疑惑 這篇文章分析的很詳細,深入。

理解I/O完成埠模型

碼字不容易,覺得好請打賞下:
這裡寫圖片描述

相關文章