转载 Qt中使用Http下载文件并实时显示进度 - 知乎
前几天写了一篇关于Http通讯进行GET、POST的操作,那么,今天的这篇文章也是基于Http通讯的,但是功能不同。
当前文章讲解的主要功能是:
使用Http/Https通讯下载文件并将下载进度实时显示到页面上。
对于下载文件来说,不是很难,我们可以从网上搜索。难点在于如何将下载进度实时的呈现给用户呢?
首先,想要在下载的同时并实时的将数据传送给页面,我们可以用发消息的方式通知给页面。在以下讲解中,我使用的回调方法。
发消息不是很简单的解决了吗?为什么还要搞那么复杂?
有些刚刚学编程的人甚至对回调不熟悉,我想说明下我为什么要使用回调。看过我以往写过文章的人会知道(csdn上),我之前的开发环境是MFC框架,也是最近这几年转成了QT框架开发。两个框架的消息通知是不一致的,但是!它们都是基于标准C++语言的。只是书写的形式不同。
在写一些核心的功能性代码时,最好采用标准语言来写,当我们以后切换框架时,不会修改核心代码,只是更改消息机制。而且,在我们开发过程中,最好功能性代码与业务处理代码分开(仅做建议,根据个人书写习惯决定)
话不多说,我们来讲解下实现方法。
1:Http通讯下载
1.1:分解指定的Url
需要用到的API:WinHttpCrackUrl
The WinHttpCrackUrl function separates a URL into its component parts such as host name and path.
将Url分割成多个组成部分,如主机名和路径
假设,我们需要访问的http的Url路径用 std::string strRemoteFile表示。
DWORD dwErrorCode = NO_ERROR;
URL_INFO url_info = { 0 };
URL_COMPONENTSW lpUrlComponents = { 0 };
lpUrlComponents.dwStructSize = sizeof(lpUrlComponents);
lpUrlComponents.lpszExtraInfo = url_info.szExtraInfo;
lpUrlComponents.lpszHostName = url_info.szHostName;
lpUrlComponents.lpszPassword = url_info.szPassword;
lpUrlComponents.lpszScheme = url_info.szScheme;
lpUrlComponents.lpszUrlPath = url_info.szUrlPath;
lpUrlComponents.lpszUserName = url_info.szUserName;
lpUrlComponents.dwExtraInfoLength = 512;
lpUrlComponents.dwHostNameLength = 512;
lpUrlComponents.dwPasswordLength = 512;
lpUrlComponents.dwSchemeLength = 512;
lpUrlComponents.dwUrlPathLength = 512;
lpUrlComponents.dwUserNameLength = 512;
if (!WinHttpCrackUrl(stringToWString(strRemoteFile).c_str(), 0, ICU_ESCAPE, &lpUrlComponents))
{
return GetLastError();
}
在这里,我需要强调的是512这个长度。一般应用这个函数时是不会出错的,但是,有一次我就获取到了错误,经过发现是我的路径太长太长了,超过了512,简直让我吐血,后来再后台做了限制,不让访问过长的路径。不过,一般情况下使用512是没有问题的,当时做的是极限测试,也请大家注意这一点。
stringToWString这个函数的将string类型转成Wstring类型,以前的文章中有写,有需要可以查看,这里不是重点。
URL_INFO的定义,如下:
typedef struct
{
WCHAR szScheme[512];
WCHAR szHostName[512];
WCHAR szUserName[512];
WCHAR szPassword[512];
WCHAR szUrlPath[512];
WCHAR szExtraInfo[512];
}URL_INFO, *PURL_INFO;
1.2:创建一个会话
需要用到的API:WinHttpOpen
The WinHttpOpen function initializes, for an application, the use of WinHTTP functions and returns a WinHTTP-session handle.
初始化WinHTTP函数的使用,并返回一个WinHTTP会话句柄。
HINTERNET hSession = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, NULL);
1.3:创建一个连接
需要用到的API:WinHttpConnect
The WinHttpConnect function specifies the initial target server of an HTTP request and returns an HINTERNET connection handle to an HTTP session for that initial target.
指定HTTP请求的初始目标服务器,并返回该初始目标的HTTP会话的HINTERNET连接句柄。
HINTERNET hConnect = WinHttpConnect(hSession, lpUrlComponents.lpszHostName, lpUrlComponents.nPort, 0);
1.4:创建请求句柄
需要用到的API:WinHttpOpenRequest、WinHttpSetOption
整个下载文件区分Http/Https的核心来了!
我们在使用两种不同方式下载时,所对应的参数也是不一致的,咱们直接上代码更清楚,哈哈
DWORD dwFlags;
if (enumType == HttpType_HTTP)
{
dwFlags=WINHTTP_FLAG_REFRESH;
}
else if (enumType == HttpType_HTTPS)
{
dwFlags = WINHTTP_FLAG_REFRESH| WINHTTP_FLAG_SECURE;
}
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", lpUrlComponents.lpszUrlPath, L"HTTP/1.1", WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags);
if (enumType == HttpType_HTTPS)
{
DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE |
SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;
//设置支持https
if (!WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags)))
{
return GetLastError();
}
}
其中,enumType是Http/Https的枚举类型。
1.5:发送请求
需要用到的API:WinHttpSendRequest
The WinHttpSendRequest function sends the specified request to the HTTP server.
向HTTP服务器发送指定的请求
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
{
return GetLastError();
}
1.6:等待接收请求响应
需要用到的API:WinHttpReceiveResponse
The WinHttpReceiveResponse function waits to receive the response to an HTTP request initiated by WinHttpSendRequest.
等待接收由WinHttpSendRequest发起的HTTP请求的响应。当WinHttpReceiveResponse成功完成时,状态代码和响应头已经被接收,应用程序可以使用WinHttpQueryHeaders进行检查。 在使用WinHttpQueryDataAvailable和winhttppreaddata访问响应实体(如果有的话)之前,应用程序必须调用WinHttpReceiveResponse。
if (!WinHttpReceiveResponse(hRequest, 0))
{
return GetLastError();
}
1.7:文件总体大小获取
需要用到的API:WinHttpQueryHeaders
The WinHttpQueryHeaders function retrieves header information associated with an HTTP request.
经过前面6个步骤的操作后,如果没有任何错误,我们就可以获取服务器上的文件了。首先第一步需要获取文件的总体大小。其中获取文件大小的另一个功能是需要将该大小传递给页面。
在实时显示下载进度时需要知道文件的总大小。这里我采用的是进度条的方式,简单方便查看。
DWORD dwContentSize=0,dwIndex = 0, dwSizeDW = sizeof(dwSizeDW);
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, NULL, &dwContentSize, &dwSizeDW, &dwIndex);
if (m_pCallBackTotalData != nullptr)
{
m_pCallBackTotalData(dwContentSize);
}
其中,m_pCallBackTotalData是回调函数,只作用于回调文件总长度。
1.8:创建本地文件
BYTE *pBuffer = new BYTE[4096];
HANDLE hFile = CreateFile(StringToWString(strLocalFile).c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
//创建文件失败
return GetLastError();
}
以上代码就不用我再过多解释了,只是一个文件的创建,很简单。
1.9:读取有效数据
需要用到的API:WinHttpQueryDataAvailable、WinHttpReadData
The WinHttpQueryDataAvailable function returns the amount of data, in bytes, available to be read with WinHttpReadData.
在这里,我是以每次最大4096的长度读取。并将每次读取的长度实时的传递给页面
DWORD dwSize, dwWrite, dwCalcSize = 0;
while (WinHttpQueryDataAvailable(hRequest, &dwSize) && dwSize)
{
if (dwSize > 4096)
{
dwSize = 4096;
}
if (m_pCallBackDownload != nullptr)
{
dwCalcSize += dwSize;
m_pCallBackDownload(dwCalcSize);
}
ZeroMemory(pBuffer, 4096);
WinHttpReadData(hRequest, pBuffer, dwSize, &dwSize);
WriteFile(hFile, pBuffer, dwSize, &dwWrite, NULL);
}
CloseHandle(hFile);
delete[]pBuffer;
1.10:关闭访问句柄
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
这里没什么可说的,直接关闭即可。在程序使用的过程中,如果遇到了错误时,也要记得先关闭句柄再返回错误值。好习惯一定要养成。以上代码在return GetLastError时并没有写明,大家需要注意,我只是放到了最后统一说明,并不是不重要!
2:回调函数使用
第一部分只是说了在哪里使用回调函数,接下来我们实现以下回调函数的用法。
2.1:定义回调函数
当前功能中只用到了两个回调方法,那么我就以这两个回调为例。
typedef void(*DownloadFilesProgressData)(int dwCurrentSize); //文件实时下载进度
typedef void(*DownloadFilesProgressTotalData)(int dwTotalSize); //文件下载文件总长度
2.2:定义下载类中回调函数变量
DownloadFilesProgressData m_pCallBackDownload;
DownloadFilesProgressTotalData m_pCallBackTotalData;
2.3:回调函数在类中传递消息应用
第一部分已经介绍了,这里我也不做特殊说明了,看1.7、1.9即可
2.4:回调函数的注册
这一步骤是重点操作。我们设置了回调函数后,外面如何调用呢?用哪个函数来接收呢?
void SetDownloadFilesProgressMsg(DownloadFilesProgressTotalData callBackFuncTotalData, DownloadFilesProgressData callBackFunc);
void HttpRequest::SetDownloadFilesProgressMsg(DownloadFilesProgressTotalData callBackFuncTotalData, DownloadFilesProgressData callBackFunc)
{
//TODO:设置下载文件的进度回调消息
m_pCallBackTotalData = callBackFuncTotalData;
m_pCallBackDownload = callBackFunc;
}
2.5:外部应用使用
经过以上步骤,我们的回调函数的框架已经写好了,在我们实际调用的地方如何使用呢
/创建、注册下载类回调信息
m_pHttpResult = new HttpRequest();
m_pHttpResult->SetDownloadFilesProgressMsg(CallBackDownloadTotalData, CallBackDownloadCurrentData);
CallBackDownloadTotalData、CallBackDownloadCurrentData是在应用类中回调的响应函数,记住,必须使用static
static void CallBackDownloadTotalData(int dwTotalSize);
static void CallBackDownloadCurrentData(int dwCurrentSize);
写到这里的时候,至于你是用MFC框架的sendMessage/PostMessage或者是Qt的emit方法都随你。
3:页面展示
前面我说到了,在显示下载进度时使用了进度条,简单明了。进度条的使用不用我再过多解释了吧。
主要流程是:当我们需要下载某个文件时将进度条页面显示出来,随着从服务