從HTTP服務器上下載一個文件有很多方法,“熱心”的微軟提供了 WinInet 類,用起來也很方便。當然,我們也可以自己實現這些功能,通過格式化請求頭很容易就能實現斷點續傳和檢查更新等等功能 。本文附帶的工程中有一個支持 HTTP1.1 協議,直接用 Socket 實現下載功能的 DLL,實現了以下功能:
連接主機
格式化請求頭
設置接收,發送超時
接收並分析回應頭
連接,發送,設置超時,接收數據等我就不細說了,windows socket早就做好了,調用相應的函數就OK了。
要想從服務器下載文件,首先要向服務器發送一個請求。HTTP 請求頭由若干行字符串組成。下面結合實例說說 HTTP 請求頭的格式。假設要下載 http://www.sina.com.cn/index.html 這個網頁 ,那麼請求頭的寫法如下:
第1行:方法,請求的內容,HTTP協議的版本
下載一般可以用GET方法,請求的內容是“/index.html”,HTTP協議的版本是指浏覽器支持的版本,對於下載軟件來說無所謂,所以用1.1版 “HTTP/1.1”;
“GET /index.html HTTP/1.1”
第2行:主機名,格式為“Host:主機”
在這個例子中是:“Host:www.sina.com.cn”
第3行:接受的數據類型,下載軟件當然要接收所有的數據類型,所以:
“Accept:*/*”
第4行:指定浏覽器的類型
有些服務器會根據客戶服務器種類的不同會增加或減少一些內容,在這個例子中可以這樣寫:
“User-Agent:Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)”
第5行:連接設置
設定為一直保持連接:“Connection:Keep-Alive”
第6行:若要實現斷點續傳則要指定從什麼位置起接收數據,格式如下:
“Range: bytes=起始位置 - 終止位置”
比如要讀前500個字節可以這樣寫:“Range: bytes=0 - 499”;從第 1000 個字節起開始下載:
“Range: bytes=999 -”
最後,別忘了加上一行空行,表示請求頭結束。整個請求頭如下:
GET /index.html HTTP/1.1
Host:www.sina.com.cn
Accept:*/*
User-Agent:Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)
Connection:Keep-Alive
CHttpSocket 提供了 FormatRequestHeader()函數,用以格式化輸出HTTP請求頭。代碼如下:
///根據請求的相對URL輸出HTTP請求頭
const char *CHttpSocket::FormatRequestHeader(char *pServer,char *pObject, long &Length,
char *pCookie,char *pReferer,long nFrom,
long nTo,int nServerType)
{
char szPort[10];
char szTemp[20];
sprintf(szPort,"%d",m_port);
memset(m_requestheader,''\0'',1024);
///第1行:方法,請求的路徑,版本
strcat(m_requestheader,"GET ");
strcat(m_requestheader,pObject);
strcat(m_requestheader," HTTP/1.1");
strcat(m_requestheader,"\r\n");
///第2行:主機
strcat(m_requestheader,"Host:");
strcat(m_requestheader,pServer);
strcat(m_requestheader,"\r\n");
///第3行:
if(pReferer != NULL)
{
strcat(m_requestheader,"Referer:");
strcat(m_requestheader,pReferer);
strcat(m_requestheader,"\r\n");
}
///第4行:接收的數據類型
strcat(m_requestheader,"Accept:*/*");
strcat(m_requestheader,"\r\n");
///第5行:浏覽器類型
strcat(m_requestheader,"User-Agent:Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)");
strcat(m_requestheader,"\r\n");
///第6行:連接設置,保持
strcat(m_requestheader,"Connection:Keep-Alive");
strcat(m_requestheader,"\r\n");
///第7行:Cookie.
if(pCookie != NULL)
{
strcat(m_requestheader,"Set Cookie:0");
strcat(m_requestheader,pCookie);
strcat(m_requestheader,"\r\n");
}
///第8行:請求的數據起始字節位置(斷點續傳的關鍵)
if(nFrom > 0)
{
strcat(m_requestheader,"Range: bytes=");
_ltoa(nFrom,szTemp,10);
strcat(m_requestheader,szTemp);
strcat(m_requestheader,"-");
if(nTo > nFrom)
{
_ltoa(nTo,szTemp,10);
strcat(m_requestheader,szTemp);
}
strcat(m_requestheader,"\r\n");
}
///最後一行:空行
strcat(m_requestheader,"\r\n");
///返回結果
Length=strlen(m_requestheader);
return m_requestheader;
}
請求頭發送給服務器後就可以接收來自服務器的回應頭了。回應頭也是由若干行字符串組成,除了第一行和最後一個空行以外,每一行都由一個域和一個值組成。第一行包括了服務器的回應狀態 ,從 2XX 到 5XX,每個狀態碼都有不同的意思,詳細內容可以查看RFC文檔下載需要關心的有 :2XX表示成功,可以繼續讀取數據;3XX表示目標已經轉移,新的地址在“Location”域中;4XX表示客戶端錯,可能是下載地址不對,等等;5XX表示服務器端錯 。回應頭中的域有“Content-Length”,“Accept-Ranges”,“Content-Type”,“Date”,“Last-Modified”,“Location”等等內容 ,下載比較關心的域有“Content-Length”域和“Location”域。“Content-Length”表示下載文件的大小 ,“Location”表示目標的實際存放位置,當回應碼為3XX時就要用該域中的值重新連接。
附帶源碼中的 CHttpSocket 類提供了以下幾個方法,分別用來讀取服務器狀態碼,某個域的值,回應頭中的一行以及整個回應頭:
int GetServerState(); //返回服務器狀態碼 -1表示不成功
int GetField(const char* szSession,char *szValue,int nMaxLength);//返回某個域值,-1表示不成功
int GetResponseLine(char *pLine,int nMaxLength);//獲取返回頭的一行
const char* GetResponseHeader(int &Length);
取得回應頭後,如果回應碼為2XX並且“Content-Length”的值不等於0就表示可以接收下載文件數據了,接下來的工作就很簡單了,調用 CHttpSocket::Recevie()直到接收的數據長度等於“Content-Length”的值就可以了 。
一個完整的使用過程由以下幾個步驟組成:
調用AfxParseURL()分析URL得到Server和下載路徑;
調用CHttpSocket::Socket()創建套接字;
調用CHttpSocket::Connect()連接服務器;
調用CHttpSocket::FormatRequestHeader()格式化請求頭;
調用CHttpSocket::SendRequest()向服務器發送請求頭;
調用CHttpSocket::GetServerState()得到回應狀態碼;
調用CHttpSocket::GetField("Content-Length")得到下載文件的大小;
調用CHttpSocket::Receive()接收數據直到數據接收完成;
本文附帶源代碼還包括了一個使用 CHttpSocket 實現下載功能的例子工程。注意,所有的調用都是阻塞的,所以最好為一個下載任務創建一個線程 ,否則會導致界面無法響應用戶輸入。程序運行界面如下圖所示:
該圖顯示了請求頭,回應頭以及下載進度。
當然,要真正實現多任務多線程下載還有很多工作要做。本文僅僅討論了自己實現下載的一種可能性,希望對讀者有所幫助。
本文配套源碼