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、对于整个流程的出错检测不是很清晰。

原文地址:https://www.cnblogs.com/monotone/p/2800934.html