CAtlHttpClient的一個嚴重bug

     我寫的一個程序要從http服務器下載xml文件,就用了CAtlHttpClient這個http客戶端類。在xml文件比較小的時候一切都順利,
但當xml文件超過1M後,問題就時不時出現:不能下載xml文件了!
    什麼原因呢?
    只能在本機調試了。爲了儘快重現bug,我把xml文件增大到了3M多,下載的週期也由原來的2分鐘縮短到30秒...經過20多個週期
問題又重現了:不能下載xml文件,陷在了下載中....
    在vc2003的工具欄中點擊"全部中斷",打開"線程"窗口就呈現出所有的線程來。我們只關心用戶線程。
    是不是死鎖了?
    我挨個挨個線程的打開,然後觀查調用棧,沒有發現死鎖,所有打開的線程都可以按F10運行....但卻發現下載xml文件的線程
一直在ZEvtSyncSocket::Read()函數中(CAtlHttpClient實際是typedef CAtlHttpClientT<ZEvtSyncSocket> CAtlHttpClient),跳
不出來。難道在Read裏發生死鎖了?試着按F10卻是可以調試運行的,看來不是死鎖。那是什麼原因讓它一直陷在Read中?
    先大概說一下調用棧:
    我的DownloadHttp("http地址")函數調用CAtlHttpClientT<TSocketClass>::Navigate()->......->CAtlHttpClientT<TSocketClass>::ReadBody...
    CAtlHttpClientT<TSocketClass>::ReadBody中有這樣一個循環,寫過tcp通信的朋友都很熟悉(//pgp是我加我註釋):

   //pgp begin
    
//
循環的功能:不斷的從網絡底層緩存區中讀數據,直到完成指定數量才跳出循環
    
//
nContentLen:xml文件的長度,從http頭中獲得
    
//
nCurrentBodyLen:已讀完的xml文件長度,即當前讀的進度.
    
//當nCurrentBodyLen >= nContentLen時,跳出循環,即讀完了xml文件的內容

    while (nCurrentBodyLen < nContentLen)
    
{
    dwRead 
= dwReadBuffSize;//
pgp:緩衝區大小
    
//pgp:從ZEvtSyncSocket::Read()中讀數據

    if (!Read(readbuff, &dwRead))
        
return false
;
    
// notify user

    if (m_pNavData)
    
{
       
if (m_pNavData->
pfnReadStatusCallback)
        
if (!m_pNavData->pfnReadStatusCallback(dwRead, m_pNavData->
m_lParamRead))
            
return false
;
    }

    
//pgp:累加進度
    nCurrentBodyLen += dwRead;
    
//pgp:把一次讀出的數據保存起來

    if (!m_current.Append((LPCSTR)(BYTE*)readbuff, dwRead))
    
{
        ATLASSERT(
0
);
        
return false// error!

    }

    m_pEnd 
= ((BYTE*)(LPCSTR)m_current) + m_current.GetLength();
    }

    線程就是陷在這個循環中,一直出不來。原因是,循環讀了幾次後,Read(readbuff, &dwRead)的輸出參數dwRead每次都等於0,導致
nCurrentBodyLen += dwRead的當前進度值一直不變,始終不滿足nCurrentBodyLen >= nContentLen這個結束循環的條件。
    那爲什麼Read返回0字節呢?返回0字節又表示什麼意思呢?
    我們繼續看ZEvtSyncSocket::Read()裏的函數,我省略了一些與本問題無關的代碼:

   //pgp
    
//這是重疊模型讀數據

inline bool ZEvtSyncSocket::Read(const unsigned char *pBuff, DWORD *pdwSize) 
{
        ...........
        
    
bool bRet = true
;
    WSABUF buff;
    buff.buf 
= (char*
)pBuff;
    buff.len 
= *
pdwSize;
    
*pdwSize = 0
;
    DWORD dwFlags 
= 0
;
    WSAOVERLAPPED o;
    ZeroMemory(
&o, sizeof
(o));

    
// protect against re-entrency

    m_csRead.Lock();
    o.hEvent 
=
 m_hEventRead;
    WSAResetEvent(o.hEvent);
    
//pgp:先投遞一個讀請求

    if (WSARecv(m_socket, &buff, 1, pdwSize, &dwFlags, &o, 0))
    
{
        DWORD dwLastError 
=
 WSAGetLastError();
        
if (dwLastError !=
 WSA_IO_PENDING)
        
{
            m_dwLastError 
=
 dwLastError;
            bRet 
= false
;
        }

    }


    
// wait for the read to complete
    if (bRet)
    
{
        
//pgp:等待Read事件的發生,等待超時爲m_dwSocketTimeout,默認是10秒

        if (WAIT_OBJECT_0 == WaitForSingleObject((HANDLE)o.hEvent, m_dwSocketTimeout))
        
{
            dwFlags 
= 0
;
            
//
pgp:Read事件發生,調用WSAGetOverlappedResult返回一次讀的字節數
            
//
問題就在這裏!!在這個函數被調用了N次後,pdwSize指向的整數值爲0
            
//
當調用WSAGetOverlappedResult後,第三個參數,即pdwSize返回0意味着什麼?
            
//
意味着遠程服務器已斷開連接!!可MS卻當成正常讀數據處理了,或說沒處理第三個參數返回0的情況
            
//當遠程服務器斷開這個連接後,我們這邊也應該關閉連接了(還有Event等)

            if (WSAGetOverlappedResult(m_socket, &o, pdwSize, FALSE, &dwFlags))
                bRet 
= true
;
            
else

            
{
                m_dwLastError 
=
 ::GetLastError();
                bRet 
= false
;
            }

        }

        
else
            bRet 
= false;
    }


    m_csRead.Unlock();
    
return bRet;
}
    

看代碼裏的解釋,我們明白了出錯的地方,那就很easy了。改下代碼,處理下調用WSAGetOverlappedResult後,第三個參數返回0的情況:

    inline bool ZEvtSyncSocket::Read(const unsigned char *pBuff, DWORD *pdwSize)
    
{
        .............
        
if (WAIT_OBJECT_0 ==
 WaitForSingleObject((HANDLE)o.hEvent, m_dwSocketTimeout))
    
{
        dwFlags 
= 0
;
        
if (WSAGetOverlappedResult(m_socket, &o, pdwSize, FALSE, &
dwFlags))
        
{
            bRet 
= true
;
            
//
pgp:如果第三個參數返回0,則bRet=false表示,這次下載失敗了
            
//函數棧中有函數自會清掃戰場

            if(0 == *pdwSize)
                bRet 
= false
;
        }

        
else
        
{
            m_dwLastError 
=
 ::GetLastError();
            bRet 
= false
;
        }

    }

    
else
        bRet 
= false;
    ..............
    }

ok了,問題並不難。解決了,給老大有一個交代了

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