下載原理: 網上介紹很多,就是按照Http協議,使用Socket連接並發送請求頭給Http服務器,若服務器正確響應,返回請求文件數據,接收並寫文件保存.至於Http協議的請求頭及響應頭的格式,這里不再贅述,請Google之.
實現: 為此,我封裝了一個HttpDownload類.先上代碼...(基於WinSock,移植時請注意部分函數)
(HttpDownload.h)
1 #ifndef HTTP_DOWNLOAD_H 2 #define HTTP_DOWNLOAD_H 3 4 #include <cstdio> 5 #include <string> 6 #include <winsock2.h> 7 8 class HttpDownload { 9 public: 10 HttpDownload(const char* hostAddr, const int port, 11 const char* getPath, const char* saveFileName); 12 ~HttpDownload(); 13 bool start(); 14 void cancel(); 15 void getPos(ULONGLONG& totalSize, ULONGLONG& downloadSize); 16 protected: 17 bool initSocket(); //初始化Socket 18 bool sendRequest(); //發送請求頭 19 bool receiveData(); //接收數據 20 bool closeTransfer(); //關閉傳輸 21 private: 22 std::string m_hostAddr; //目標主機IP 23 int m_port; //HTTP端口號 24 std::string m_getPath; //目標文件相對路徑 25 std::string m_saveFileName; //保存文件路徑 26 SOCKET m_sock; //Socket 27 FILE* m_fp; //保存文件指針 28 ULONGLONG m_fileTotalSize; //目標文件總大小 29 ULONGLONG m_receivedDataSize; //已接收數據大小 30 bool m_cancelFlag; //取消下載標記 31 }; 32 33 #endif //HTTP_DOWNLOAD_H
(HttpDownload.cpp)
1 #include "HttpDownload.h" 2 3 #define BUFFER_SIZE 1024 4 5 HttpDownload::HttpDownload(const char* hostAddr, const int port, const char* getPath, const char* saveFileName) 6 { 7 m_hostAddr = hostAddr; 8 m_port = port; 9 m_getPath = getPath; 10 m_saveFileName = saveFileName; 11 m_sock = 0; 12 m_fp = NULL; 13 m_fileTotalSize = 1; //沒給0,因為分母 14 m_receivedDataSize = 0; 15 m_cancelFlag = false; 16 } 17 18 HttpDownload::~HttpDownload() 19 { 20 21 } 22 23 bool HttpDownload::initSocket() 24 { 25 m_sock = socket(AF_INET, SOCK_STREAM, 0); 26 if (m_sock < 0) 27 return false; 28 29 //設置Socket為非阻塞模式 30 unsigned long mode = 1; 31 if (ioctlsocket(m_sock, FIONBIO, &mode) < 0) 32 return false; 33 34 if (m_hostAddr.empty()) 35 return false; 36 37 struct sockaddr_in destaddr; 38 destaddr.sin_family = AF_INET; 39 destaddr.sin_port = htons(m_port); 40 destaddr.sin_addr.s_addr = inet_addr(m_hostAddr.c_str()); 41 42 int nRet = connect(m_sock, (struct sockaddr*)&destaddr, sizeof(destaddr)); 43 if (nRet == 0) //如果立即連接成功 44 return true; 45 //雖直接返回,但未立即成功,用select等待看socket是否可寫來判斷connect是否成功 46 if (WSAGetLastError() != WSAEWOULDBLOCK) 47 return false; 48 int retryCount = 0; 49 while(1) 50 { 51 fd_set writeSet, exceptSet; 52 FD_ZERO(&writeSet); 53 FD_SET(m_sock, &writeSet); 54 exceptSet = writeSet; 55 56 struct timeval timeout; 57 timeout.tv_sec = 3; 58 timeout.tv_usec = 0; 59 60 int err = select((int)m_sock+1, NULL, &writeSet, &exceptSet, &timeout); 61 if (err < 0) //出錯 62 break; 63 if (err == 0) //超時 64 { 65 if (++retryCount > 10) //重試10次 66 return false; 67 continue; 68 } 69 if (FD_ISSET(m_sock, &writeSet)) 70 return true; 71 if (FD_ISSET(m_sock, &exceptSet)) 72 break; 73 } 74 return false; 75 } 76 77 bool HttpDownload::sendRequest() 78 { 79 if (m_getPath.empty()) 80 return false; 81 82 char requestHeader[256]; 83 //格式化請求頭 84 int len = sprintf(requestHeader, 85 "GET %s HTTP/1.1\r\n" 86 "Host: %s\r\n" 87 "Range: bytes=%I64d-\r\n" //從m_receivedDataSize位置開始 88 "Connection: close\r\n" 89 "\r\n", 90 m_getPath.c_str(), m_hostAddr.c_str(), m_receivedDataSize); 91 92 int nSendBytes = 0; //已發送字節數 93 while(1) 94 { 95 fd_set writeSet; 96 FD_ZERO(&writeSet); 97 FD_SET(m_sock, &writeSet); 98 99 struct timeval timeout; 100 timeout.tv_sec = 3; 101 timeout.tv_usec = 0; 102 103 int err = select((int)m_sock+1, NULL, &writeSet, NULL, &timeout); 104 if (err < 0) 105 break; 106 if (err == 0) 107 continue; 108 int nBytes = send(m_sock, requestHeader+nSendBytes, len, 0); 109 if (nBytes < 0) 110 { 111 if (WSAGetLastError() != WSAEWOULDBLOCK) 112 break; 113 nBytes = 0; 114 } 115 nSendBytes += nBytes; //若一次未發完,累計,循環send 116 len -= nBytes; 117 if (len == 0) 118 return true; 119 } 120 return false; 121 } 122 123 bool HttpDownload::receiveData() 124 { 125 char responseHeader[BUFFER_SIZE] = {0}; 126 127 struct timeval timeout; 128 timeout.tv_sec = 3; 129 timeout.tv_usec = 0; 130 131 //接收響應頭 132 int retryCount = 0; 133 int nRecvBytes = 0; //已接收字節數 134 while (1) 135 { 136 fd_set readSet; 137 FD_ZERO(&readSet); 138 FD_SET(m_sock, &readSet); 139 int nRet = select(m_sock+1, &readSet, NULL, NULL, &timeout); 140 if (nRet < 0) //出錯 141 return false; 142 if (nRet == 0) //超時 143 { 144 if (++retryCount > 10) 145 return false; 146 continue; 147 } 148 retryCount = 0; 149 if (recv(m_sock, responseHeader+nRecvBytes, 1, 0) <= 0) 150 return false; 151 nRecvBytes++; 152 if (nRecvBytes >= BUFFER_SIZE) 153 return false; 154 if (nRecvBytes >= 4 && 155 responseHeader[nRecvBytes-4]=='\r' && responseHeader[nRecvBytes-3]=='\n' && 156 responseHeader[nRecvBytes-2]=='\r' && responseHeader[nRecvBytes-1]=='\n') 157 break; 158 } 159 responseHeader[nRecvBytes] = '\0'; 160 161 if (strncmp(responseHeader, "HTTP/", 5)) 162 return false; 163 int status = 0; 164 float version = 0.0; 165 ULONGLONG startPos, endPos, totalLength; 166 startPos = endPos = totalLength = 0; 167 if (sscanf(responseHeader, "HTTP/%f %d ", &version, &status) != 2) 168 return false; 169 char* findStr = strstr(responseHeader, "Content-Range: bytes "); 170 if (findStr == NULL) 171 return false; 172 if (sscanf(findStr, "Content-Range: bytes %I64d-%I64d/%I64d", 173 &startPos, &endPos, &totalLength) != 3) 174 return false; 175 if (status != 200 && status != 206 || totalLength == 0) 176 return false; 177 if (m_fileTotalSize == 1) //第一次獲取HTTP響應頭,保存目標文件總大小 178 m_fileTotalSize = totalLength; 179 if (m_receivedDataSize != startPos) 180 return false; 181 182 //接收目標文件數據 183 retryCount = 0; 184 while (1) 185 { 186 char buf[BUFFER_SIZE] = {0}; 187 fd_set readSet; 188 FD_ZERO(&readSet); 189 FD_SET(m_sock, &readSet); 190 191 int nRet = select((int)m_sock+1, &readSet, NULL, NULL, &timeout); 192 if (nRet < 0) 193 break; 194 if (nRet == 0) { 195 if (++retryCount > 10) 196 break; 197 continue; 198 } 199 int length = recv(m_sock, buf, BUFFER_SIZE, 0); 200 if(length < 0) //出錯 201 return false; 202 if (length == 0) //socket被優雅關閉 203 return true; 204 size_t written = fwrite(buf, sizeof(char), length, m_fp); 205 if(written < length) 206 return false; 207 m_receivedDataSize += length; 208 if (m_receivedDataSize == m_fileTotalSize) //文件接收完畢 209 { 210 return true; 211 } 212 } 213 return false; 214 } 215 216 bool HttpDownload::closeTransfer() 217 { 218 if (m_sock > 0) { 219 if (closesocket(m_sock) < 0) 220 return false; 221 m_sock = 0; 222 } 223 else 224 m_sock = 0; 225 return true; 226 } 227 228 bool HttpDownload::start() 229 { 230 m_fp = fopen(m_saveFileName.c_str(), "wb"); //創建文件 231 if (m_fp == NULL) 232 return false; 233 bool errFlag = false; 234 while(1) 235 { 236 if (!initSocket() || !sendRequest() || !receiveData()) 237 { 238 if (m_cancelFlag) 239 { 240 errFlag = true; 241 break; 242 } 243 if (!closeTransfer()) 244 { 245 errFlag = true; 246 break; 247 } 248 Sleep(1000); 249 continue; 250 } 251 break; 252 } 253 if(m_fp != NULL) 254 { 255 fclose(m_fp); 256 m_fp = NULL; 257 } 258 if (errFlag) 259 return false; 260 return true; 261 } 262 263 void HttpDownload::cancel() 264 { 265 m_cancelFlag = true; 266 closeTransfer(); 267 } 268 269 void HttpDownload::getPos(ULONGLONG& totalSize, ULONGLONG& downloadSize) 270 { 271 totalSize = m_fileTotalSize; 272 downloadSize = m_receivedDataSize; 273 }
其中4個主要函數功能如下:
1) initSocket() : 初始化Socket->設置Socket為非阻塞模式->connect()
值得注意的是,這里我采用了非阻塞的select模型.相對來說非阻塞模式可以減少開銷,增加錯誤控制能力,顯得更靈活.因此,Line 42-73
connect之,因為非阻塞,立即返回,得到返回值判斷,通常不會立即成功,然后
while(1) {
socket加入寫描述符集
用select檢測寫寫描述符集,延時3秒,可寫就返回true
否則,重復10次(總共30秒),如果仍不成功,認為connect失敗,返回false
}
2) sendRequest() : 格式化Http請求字符串->用send發送之.send依然使用非阻塞select模型判斷,同上.
注意的是:這里Http請求字符串里的"Range: bytes="字段用m_receivedDataSize來格式化,此成員變量用於保存請求的目標文件開始位置.目的是為實現斷點續傳,若傳輸文件過程中網絡異常后,可重新發送請求頭,則只需從已下載位置之后繼續傳輸.
3) receiveData() : recv接收Http響應頭字符串->取出響應頭中的信息(如文件大小)->若響應信息正確,開始recv接收目標文件數據->寫文件
recv依然使用非阻塞select模型判斷,同上.
4) closeTransfer() : 關閉套接字.(這里因在windows下,所以使用closesocket)
因為代碼中多有注釋,細節就不再多解釋了.
另外,給出3個接口函數:
1) start() : 創建文件->分別依次調用initSocket(),sendRequest(),receiveData()->關閉文件,套接字
在一個循環,不斷判斷initSocket(),sendRequest(),receiveData()是否成功,若任一失敗(網絡異常),調用closeTransfer(),然后重新來,直到下載完畢或被cancel()中斷
2) cancel() : 關閉套接字,並將m_cancelFlag置true
3) getPos() : 用於得到當前文件下載的進度
最后,附上源碼,包含一個實現下載的控制台程序例子(MinGW編譯),上圖
(Main.cpp)

1 #include <cstdio> 2 #include "pthread.h" 3 #include "HttpDownload.h" 4 #include "InitWinSocket.h" 5 6 InitWinSocket init; 7 const char* g_progName = NULL; 8 const char* g_saveFileName = "savefilename"; 9 10 void usage() { 11 printf("Usage: %s http://www.xxx.com/filename %s\n", g_progName, g_saveFileName); 12 } 13 14 void progressBar(float percent) { 15 const int numTotal = 50; 16 int numShow = (int)(numTotal * percent); 17 if (numShow == 0) 18 numShow = 1; 19 if (numShow > numTotal) 20 numShow = numTotal; 21 char sign[numTotal+1] = {0}; 22 memset(sign, '=', numTotal); 23 printf("\r%.2f%%\t[%-*.*s]", percent*100, numTotal, numShow, sign); 24 fflush(stdout); 25 if (numShow == numTotal) 26 printf("\n"); 27 } 28 29 void parseURL(const char* url, char* hostAddr, int& port, char* getPath) { 30 if (url == NULL || hostAddr == NULL || getPath == NULL) 31 return; 32 const char* temp = strstr(url, "http://"); 33 if (temp == NULL) 34 return; 35 const char* hostStart = temp + strlen("http://"); 36 const char* colon = strchr(hostStart, ':'); 37 if (colon != NULL) //表示存在冒號,有端口號 38 sscanf(hostStart, "%[^:]:%d%s", hostAddr, &port, getPath); 39 else 40 sscanf(hostStart, "%[^/]%s", hostAddr, getPath); 41 //通過主機名轉IP地址 42 struct hostent* hostEntry; 43 hostEntry = gethostbyname(hostAddr); 44 if (hostEntry == NULL) 45 { 46 printf("Hostname not available!\n"); 47 return; 48 } 49 struct in_addr inAddr = {0}; 50 memcpy(&inAddr.s_addr, hostEntry->h_addr, sizeof(inAddr.s_addr)); 51 strcpy(hostAddr, inet_ntoa(inAddr)); 52 } 53 54 void* task(void* arg) { 55 HttpDownload* pObj = (HttpDownload*)arg; 56 if (pObj->start()) 57 return ((void*)1); 58 else 59 return ((void*)0); 60 } 61 62 int main(int argc, char** argv) { 63 g_progName = strrchr(argv[0], '\\'); 64 if (g_progName != NULL) 65 g_progName += 1; 66 else 67 g_progName = argv[0]; 68 if (argc != 3 || strncmp(argv[1], "http://", strlen("http://")) != 0) { 69 usage(); 70 return -1; 71 } 72 g_saveFileName = argv[2]; 73 74 char hostAddr[256] = {0}; 75 int port = 80; 76 char getPath[256] = {0}; 77 parseURL(argv[1], hostAddr, port, getPath); 78 79 HttpDownload obj(hostAddr, port, getPath, g_saveFileName); //創建下載類對象 80 pthread_t tid; 81 int err = pthread_create(&tid, NULL, task, &obj); 82 if (err != 0) 83 printf("Start Download Failed!\n"); 84 else 85 printf("Start Downloading...\n"); 86 87 ULONGLONG totalSize = 1; 88 ULONGLONG downloadSize = 0; 89 float percent = 0; 90 while (1) { 91 obj.getPos(totalSize, downloadSize); 92 percent = downloadSize/(float)totalSize; 93 progressBar(percent); 94 if (downloadSize == totalSize) 95 break; 96 Sleep(500); 97 } 98 99 void* ret = NULL; 100 pthread_join(tid, &ret); 101 if (ret) 102 printf("Download Finished.\n"); 103 else 104 printf("Download Failed!\n"); 105 return 0; 106 }
附件: 源碼下載