WinInet单线程断点续传下载
最近比较空闲,尝试了一下网络方面的编程,于是兴起写一个多线程断点续传下载的简单demo。于是首先在网络上搜索各种实现思路,最终决定先从一个简单的单线程断点续传下载开始。
实现思路:
每次都以如果不存在则创建的方式打开要下载的文件,然后获取其大小size,然后给URL发送请求头,带上头信息range,并以size作为要获取的数据起始位置,终止位置不写(表明要获取后面所有的数据)。然后便是不断的向文件末尾追写数据。直至某一次启动下载的时候发现返回的状态码为416,表明范围超出了,则表示文件已经下载完成了。
关键知识点:
1、需要对HTTP协议有初步的理解,主要是对请求头与返回头的格式要求进行理解。
2、要明白Http请求头中range关键字的用法。
3、学会使用Goole Chrome的审查元素功能和微软的wfetch小工具进行Http请求头和返回头信息的查看。
4、最后,当然就是编程方面的知识了,包括wininet库的使用和基本的线程和文件的操作等。
剩下的便是按照这个思路,去慢慢的学习相关知识,然后编程实现这个Demo了。我这里粘上自己的代码,本人英语实在很差,但是还是尝试着使用英语去注释了,看不太懂的,可以来问我。
/* Filename: main.cpp Function: 单线程断点续传功能实验。 以文件本身的大小(没有则新创建)作为本次要下载时请求数据的起始位置,每次都将读取下来的数据追加到文件末尾。 Knowledge: 需要具备HTTP相关知识。 HTTP 请求头中verb(行为)可以用HEADER去获取大小,但是我们这里不需要去获取大小,所以不需要。 HTTP Header中存在range关键字用于指定所请求的数据范围。格式为"Range: bytes=StartPos-EndPos\r\n"如果"EndPos"不写,则默认为接收后面所有数据。 如果StartPos超出了范围,则会返回”416 Requested Range Not Satisfiable“。当存在Range时返回的状态码始终为206。 另外当EndPos==StartPos时,会返回1字节数据。当EndPos>StartPos时,返回所有数据。 History: time: 2012/11/26 remarks: test finish auth: monotone */ #include <Windows.h> #include <wininet.h> #include <stdio.h> #include <string> #include <iostream> #include <tchar.h> using namespace std; #pragma comment(lib, "wininet.lib") const char* STR_TEST_URL = "http://dl_dir.qq.com/qqfile/qq/QQ2013/QQ2013Beta1.exe" // 以腾讯的QQ下载作为实验,这里是我临时加上的,之前我测试过下载更大的文件,没问题。 const DWORD DWORD_MAX_CCH_OF_TEST_URL = 256; const DWORD DWORD_MAX_CCH_OF_HOST_NAME = 128; const DWORD DWORD_MAX_CCH_OF_URL_PATH = 256; BOOL GetWininetLastErrorMsgA(OUT string& rStrErrorMsg) { BOOL lbResult = FALSE; char* lscErrorMsg = NULL; if(0 != FormatMessageA( FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ALLOCATE_BUFFER, // dwFlags GetModuleHandle( TEXT("wininet.dll") ), // lpSource GetLastError(), // dwMessageId 0, // dwLanguageId (LPTSTR)&lscErrorMsg, // lpBuffer 0, // nSize NULL)) { rStrErrorMsg = lscErrorMsg; lbResult = TRUE; } if(NULL != lscErrorMsg) LocalFree(lscErrorMsg); return lbResult; } int main() { HINTERNET hInetOpen = NULL; HINTERNET hInetConnect = NULL; HINTERNET hInetRequest = NULL; HANDLE lhFile = NULL; do { // struct to contains the constituent parts of a URL URL_COMPONENTS ldCrackedURL; ZeroMemory(&ldCrackedURL, sizeof(URL_COMPONENTS)); ldCrackedURL.dwStructSize = sizeof(URL_COMPONENTS); // 必须设置 // buffer to store host name TCHAR szHostName[DWORD_MAX_CCH_OF_HOST_NAME] = {0}; ldCrackedURL.lpszHostName = szHostName; ldCrackedURL.dwHostNameLength = DWORD_MAX_CCH_OF_HOST_NAME; // 字符数 // buffer to store url path char szUrlPath[DWORD_MAX_CCH_OF_URL_PATH] = {0}; ldCrackedURL.lpszUrlPath = szUrlPath; ldCrackedURL.dwUrlPathLength = DWORD_MAX_CCH_OF_URL_PATH; // 字符数 // 该函数用来将给定的Ulr分割成对应的部分。如果URL_COMPONENTS内部成员指针指向提供的缓冲,则其对应的长度也必须提供缓冲区大小。函数成功返回后,会将实际拷贝的内容大小存放在指针对象的大小中,不包括最后结束符。 // 如果提供的URL_COMPONENTS内部各指针指向NULL,而dwStructSize成员不为0,则调用函数后,指针成员会存储对应内容的第一个字符的地址,对应长度则为该内容实际的长度。 // 注意不要在使用"file://"类的URL时包含空格。 if(FALSE == InternetCrackUrlA(STR_TEST_URL, (DWORD)strlen(STR_TEST_URL), 0, &ldCrackedURL)) { // GetLastError(); break; } // Get file name,注意,只适用于ulr末尾包含了文件名的url。 string loStrFileName(ldCrackedURL.lpszUrlPath); string::size_type liFileNamePos = loStrFileName.rfind("/"); if(string::npos != liFileNamePos) { loStrFileName = loStrFileName.substr(liFileNamePos + 1, string::npos); } // open internet hInetOpen = InternetOpenA("Breakpoint Continue Dounload Sample", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if(NULL == hInetOpen) { // GetLastError(); break; } // connect server hInetConnect = InternetConnectA(hInetOpen, ldCrackedURL.lpszHostName, ldCrackedURL.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); if(NULL == hInetConnect) { // GetLastError(); break; } /* test 1: Read the destination file size as the size of data which download last(if file not exist, set the size to zero). And set the value argument of HTTP Header "Range: bytes=value-\r\n" to "size - 1"(the size position). Then open(or create) the file and append data from the end until no data to read, it means file download over. */ // Get the file size lhFile = CreateFileA(loStrFileName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if(lhFile == INVALID_HANDLE_VALUE) { break; } LARGE_INTEGER ldFileSize; if(FALSE == GetFileSizeEx(lhFile, &ldFileSize)) { break; } // make the start position LONGLONG lllStartPos = 0; if(0 == ldFileSize.QuadPart) { cout << "new file to download. " << endl; } else { // Set the file pointer position if(INVALID_SET_FILE_POINTER == SetFilePointer(lhFile, 0, NULL, FILE_END)) { cout << "Move file pointer failed. " << endl; break; } lllStartPos = ldFileSize.QuadPart; cout << "continue to download from position:" << lllStartPos << endl; } // convert the range start position to character char lscRangeStartPosition[30] = {0}; if(0 != _i64toa_s((__int64)(lllStartPos), lscRangeStartPosition, sizeof(lscRangeStartPosition), 10)) { break; } // additional header: set the file data range . string loAdditionalHeader = "Range: bytes="; loAdditionalHeader += lscRangeStartPosition; // start position of remaining loAdditionalHeader += "-\r\n"; // open request with "GET" verb to get the remaining file data const char* lplpszAcceptTypes[] = {"*/*", NULL}; hInetRequest = HttpOpenRequestA(hInetConnect, "GET", ldCrackedURL.lpszUrlPath, "HTTP/1.1", NULL, lplpszAcceptTypes, 0, 0); if(NULL == hInetConnect) { // GetLastError(); break; } // send request with additional header if(FALSE == HttpSendRequestA(hInetRequest, loAdditionalHeader.c_str(), loAdditionalHeader.size(), NULL, 0)) { // GetLastError(); break; } // query the status code from the reponse of servers DWORD ldwStatusCode; DWORD ldwCbOfStatusCode = sizeof(ldwStatusCode); if(FALSE == HttpQueryInfo(hInetRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &ldwStatusCode, &ldwCbOfStatusCode, 0)) { break; } //HTTP_QUERY_CONTENT_RANGE //// get file size // check the status code if(416 == ldwStatusCode) // 416 Requested Range Not Satisfiable { cout << "the file does not need to download." << endl; break; } else if(200 != ldwStatusCode && 206 != ldwStatusCode) // 206 Partial Content { // statuscode means error occurred. break; } // loop to read the data from HTTP and store to file BYTE lpbBufferToReceiveData[2048]; // 存放读取数据的Buffer DWORD ldwCbBuffer = 2048; DWORD ldwCrtCbReaded; // 本次实际读取的字节数 DWORD ldwCbWritten = 0; // 本次实际写入到文件的字节数 bool lbIsOk = false; LONGLONG lllCbAllRead = 0; do { // read data if (FALSE == InternetReadFile(hInetRequest, lpbBufferToReceiveData, ldwCbBuffer, &ldwCrtCbReaded)) { cout << "read data failed." << endl; break; } if(ldwCrtCbReaded == 0) // all data haved been read. { cout << "Congratulation! file download finish successfully." << endl; break; } // write to file if(FALSE == WriteFile(lhFile, lpbBufferToReceiveData, ldwCrtCbReaded, &ldwCbWritten, NULL) || ldwCbWritten != ldwCrtCbReaded) { cout << "A exception happens when write data to file" << endl; break; } // clear data in buffer ZeroMemory(lpbBufferToReceiveData, ldwCrtCbReaded); lllCbAllRead += ldwCrtCbReaded; cout << "crt readed data size:——————————" << lllCbAllRead / 1048576 << "MB" << endl; } while (true); }while(false); string loStrErrorMsg; if(FALSE != GetWininetLastErrorMsgA(loStrErrorMsg)) { cout << loStrErrorMsg.c_str() << endl; } if(NULL != lhFile) { CloseHandle(lhFile); } if(NULL != hInetRequest) { InternetCloseHandle(hInetRequest); } if(NULL != hInetConnect) { InternetCloseHandle(hInetConnect); } if(NULL != hInetOpen) { InternetCloseHandle(hInetOpen); } getchar(); return 0; }
不足之处:
1、没有检测服务器端到底支不支持断点续传。
2、没有检测当前网络状态。
3、对于整个流程的出错检测不是很清晰。
友情提示:
写个博客对于我来说不容易,如果此文是我原创,烦请转载加个链接http://www.cnblogs.com/monotone/。谢谢。