gh0st的IOCP模型分析

在分析了那麼多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完成端口模型

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

發佈了43 篇原創文章 · 獲贊 21 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章