我寫的一個程序要從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是我加我註釋):
//循環的功能:不斷的從網絡底層緩存區中讀數據,直到完成指定數量才跳出循環
//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()裏的函數,我省略了一些與本問題無關的代碼:
//這是重疊模型讀數據
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的情況:
{
.............
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了,問題並不難。解決了,給老大有一個交代了