分片传输、断点续传相关研究
场景,构建一个下载类组件,基于libcurl,达到正常下载、分片传输、断点续传等功能,同时保证组件的健壮性、对极限情况的兼容性、对上层业务回抛信息的完善
对本次任务的前置校验操作
分片传输+断点的实现对本次任务有要求限制,一般来说:针对大文件传输服务端都会进行该功能的配置
- 通过服务端请求拿到文件总长度
给出一个demo例子,具体需要根据业务场景添加
bool getFileLength(const std::string url, curl_off_t& fileLength) {
bool retValue = false;
for (int retrytime = 0; retrytime < 5; retrytime++) {
CURL* curlHandle = curl_easy_init();
curl_easy_setopt(curlHandle, CURLOPT_SHARE, sharednsHandle);
curl_easy_setopt(curlHandle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5);
curl_easy_setopt(curlHandle, CURLOPT_CUSTOMREQUEST, "GET");
curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str());
curl_easy_setopt(curlHandle, CURLOPT_HEADER, 1);
curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 1);
curl_easy_setopt(curlHandle, CURLOPT_TIMEOUT, MM_TIMEOUT);
curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1L);
CURLcode code = curl_easy_perform(curlHandle);
if (code == CURLE_OK) {
curl_easy_getinfo(curlHandle,
CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
&fileLength);
retValue = true;
break;
}
}
return retValue;
}
- 通过请求得到的HEADERDATA中是否含有Content-Range: bytes、Accept-Ranges: bytes确定该下载是否支持分段传输
bool useMutilDownload(std::string url) {
bool ret = false;
for (int retryTime = 0; retryTime < 5; retryTime++) {
CURL* curlHandle = curl_easy_init();
curl_easy_setopt(curlHandle, CURLOPT_SHARE, sharednsHandle);
curl_easy_setopt(curlHandle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5);
curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str());
curl_easy_setopt(curlHandle, CURLOPT_HEADER, 1);
curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 1);
std::string strHeader;
curl_easy_setopt(curlHandle, CURLOPT_HEADERDATA, &strHeader);
curl_easy_setopt(curlHandle, CURLOPT_HEADERFUNCTION,
&LibcurlMultiThread::headerInfo);
curl_easy_setopt(curlHandle, CURLOPT_RANGE, "0-");
curl_easy_setopt(curlHandle, CURLOPT_TIMEOUT, MM_TIMEOUT);
curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1L);
CURLcode code = curl_easy_perform(curlHandle);
if (code == CURLE_OK) {
ret =
((strHeader.find("Content-Range: bytes") !=
std::string::npos) ||
(strHeader.find("Accept-Ranges: bytes") != std::string::npos));
break;
}
}
return ret;
}
针对多任务下载及断点传输文件的设计
注:所有的操作,保证使用curl_off_t(__int64)
- . 使用.dltmp文件进行过度,用内存映射创建临时文件,大小为前置工作中已获取的长度+分片传输自定义信息
beginPos:该块开始的位置,blockSize:该块下载内容大小,recvSize:该块下载已完成的大小
最优线程数计算:min(文件长度/最优下载块大小,自定义最大线程数量)
例:文件长度为2001byte,将分为5个任务下载,临时信息长度5 * 3 * sizeof(curl_off_t)+sizeof(curl_off_t);5块分别下载大小400、400、400、400、401,则第一块的信息是,beginPos 0,blockSize 400,recvSize 0,第二块beginPos 400,blcokSize 400,recvSize 400…
实际开发中,可自定义数据结构,用于存储若干个任务块的信息。同时在开始传输前,将临时文件尾部的信息进行准确填写,具体操作为_fseeki64配合已知的文件长度,定位到待写入的位置,信息依次写入。注意:一旦某个字节出错,则会导致全部下载失败!
- . 使用curl_multi_init()替换多线程
7.9.6版本后引入的multi接口,可以一次针对多个easy_curl句柄进行操作,类似多路复用IO。个人的设计中,将多线程替换为该方式,可以很大程度上降低复杂度。
libcurl的multi interface,7.9.6之后引入。个人分析为:easy_curl在开始执行后会阻塞当前线程,因此通常使用多线程来实现下载,而multi interface可以对多个句柄共同操作,同时这个操作可以是异步的,避免了主线程阻塞。但对于任务的结束,仍需要遍历任务堆栈。
思路:
- n个线程变为获取n个easy_curl任务句柄,配合分片传输中的beginPos、blockSize、recvSize信息,确定该句柄执行的下载任务范围;
- 若干句柄使用同一个写入文件回调函数来避免同步的问题;
- 将若干句柄加入multi中,异步启动,不阻塞主线程;
- 文件写入回调函数
该回调函数是分片传输的核心,具体分为以下几个步骤
- 多个任务要使用同一个回调来避免IO等问题
- 根据自定义的数据结构(主要存放每个任务的信息,包括beginPos,blockSize,recvSize等),将文件句柄_fseeki64重新定位,值为beginPos+recvSize,再将本次得到的buffer进行写入操作;
- 重新定位到该次写入任务对应的块位置,为达到这个目的,也许需要在自定义的数据结构中加入相关信息实现,当_fseeki64定位到块的位置后,将beginPos、blockSize、recvSize进行值更新
断点流程
- 根据临时文件的信息判断是否可续传
- 首先得到文件长度,同从服务端请求的长度比对,若不同则可能文件已更新或损坏,需要重新构建下载任务
- 根据文件长度,得到临时信息相关的位置,将文件句柄定位至该处,读取每一个块的信息(beginPos、blockSize、recvSize),更新自定义数据结构
- 根据读取的内容,构建本次下载任务对应块的偏移量(beginPos,blockSize-recvSize),将多个easy_curl句柄加入mutil_curl,断点传输
核心流程总结梳理
- 本次下载的前置操作,包括请求本次下载文件长度、是否启动分片传输
- 临时文件的构建,使用内存映射创建临时文件格式为.dltmp,大小为请求的文件长度+分片传输信息长度(sizeof(cur_off_t))+n* 3 * sizeof(cur_off_t)
- 填充自定义的数据结构,并将内容写入临时文件的后缀信息中
- 构建若干个cur_easy句柄任务,设置每个句柄下载任务区间,将其加入到multi句柄
- 为若干句柄构建同一个文件写入回调,在回调中,重定向文件写入指针位置,写入本次buffer数据;再次重新定位文件句柄到对应任务的尾部信息位置,更新数据结构和文件尾部信息内容(recvSize);
- 当断点续传时,首先读取尾部文件确定文件是否可用,后根据尾部的块信息定义本次下载任务每个块对应的区间
- 利用mutil_curl进行任务异步进行
待解决
- 健壮性,任何一个字节的出错将导致整个文件的出错,而实际应用中网络环境复杂、机器IO性能差异化明显,因此该设计使用需要构建完善的边界极限处理机制。
- 上层业务的感知性,libcurl的easy_curl可以得到错误码,只针对本块传输的下载和写入,而对于所有的读取、临时信息解析、临时信息写入、IO等操作,需要构建一套可回抛给上层业务的错误码机制
- 异步问题,虽然使用mutil代替线程已经很大程度降低了复杂度,但获取当前的下载状态信息等仍需要轮询mutil本身的消息堆栈,而该操作不能影响主线程,因此仍需要异步任务来调用curl_multi_poll或curl_multi_perform来实现,作为组件的效果不如直接对接具体业务。(组件黑盒化,对外界无感知)
实际使用,需要从整个框架层面设计,包含主流程、网络错误处理、IO错误处理、业务层回抛信息、异步操作等内容综合设计