libcurl的多文件下载限速
LibCurl实现的下载器
使用curl_multi_perform实现的下载器,单线程运行curl的消息循环,并加入任务队列,在执行消息循环后执行任务队列实现,新下载文件的添加和已完成下载文件的移除,确保所有针对curl句柄的操作都在下载线程完成。
速度统计的实现
每个任务有单独的计时器用来统计单位时间内下载的字节数,字节数的统计是通过curl write callback的文件统计汇总到任务级别。
限速功能的实现
基于CURLOPT_MAX_RECV_SPEED_LARGE的限速实现,改选项的实现原理是全速下载,在达到预期平均速度后暂停下载,在实现过程中发现,在下载过程中实时修改该值虽然OK,但是修改后的速度统计发现波动非常大,并且不是很准确,应该是业务层的速度统计和curl自带的限速和速度统计有出入。但是curl的获取速度接口并不能较好的应用到任务级别并对外显示,针对整个文件下载过程中的平均速度,实时性比较差。
使用curl_easy_pause和CURLOPT_MAX_RECV_SPEED_LARGE配合的限速实现,某种程度上可以让速度变化的稍微平缓一些,因为在任务级别的下载过程中,会不断的增加新的文件和删除旧的文件,并且业务要求,同时下载的文件数量可能会变化,针对文件级别的限速和业务要求针对任务级别的限速略有冲突,平均分速的方式虽然可行(但是确实是浪费)。
这种CURLOPT_MAX_RECV_SPEED_LARGE限速更适合一开始就限速,不支持中途修改,并且,下载文件数量固定或者是单个文件下载更加适合。
出了几个的问题
- 当小文件数量过多,会出现频繁的添加和删除文件,重新分速和限速都是很大的负担,并且小文件完成速度非常快,新文件的限速并不会因为前一个文件的下载收到影响,新文件进来直接是全速下载(在文件大小小于限速时),导致速度根本无法限制住
- 暂停->设置
CURLOPT_MAX_RECV_SPEED_LARGE->恢复下载的方式可能会引发一部分下载错误,CURLE_PARTIAL_FILE错误出现了,可能和频繁的下载中暂停变速恢复有关,在HTTP2下表现的有些问题
限速功能的新方案
令牌桶控制的下载新方案
上个方案的实现中出现的问题,导致很难在CURLOPT_MAX_RECV_SPEED_LARGE上继续做文章。只能修改方案,采用令牌桶控制。
令牌桶:桶可以看成池,令牌的数量代表下载的通行证,当文件需要下载时,需要从令牌桶中取出下载许可,有许可才可以继续下载,没有许可,就不能下载,从而达到限速,由于有桶的概念,可以支持多个文件访问桶,来实现多文件下载的限速控制。
新方案将每秒可以下载字节数量视为令牌(每秒可下载字节数其实就看成bytes/s下载速度),令牌更新(重新下放)时机用定时器加锁或者直接在下载线程中简单实现都可以做到。
关于暂停的时机,这里试了两种
- 一种方案是在下载循环中实现,使用
curl_easy_pause进行暂停和恢复,这种方案并不OK,有明显的延后,在写回调高速触发的场景下,这种暂停和恢复都太不及时,无法做到限速。 - 一种方案是在写回调中实现,这种方法更加及时,但是需要在访问令牌桶的时候注意线程的问题。
于是新方案的下载限速实现部分如下
size_t write_data(void *ptr, size_t size, size_t nmemb, void *filep) {
if (filep) {
if (IsSpeedLimit()) {
auto reserved =UpdateTokenBucket(size * nmemb);
if (reserved == 0) {
return CURL_WRITEFUNC_PAUSE;
}
}
auto res = filep->Write(ptr, size, nmemb);
if (res < 0) {
return 0;
} else {
return res;
}
} else {
// filep nullptr error!
return 0;
}
}
需要注意的是返回CURL_WRITEFUNC_PAUSE进行暂停时,此次回调的数据要被丢弃,不能处理写入,恢复后curl会再吐一次这部分数据。
只针对有限速的情况下执行存取令牌和暂停下载的逻辑,否则正常下载
// in download thread loop
if (IsSpeedLimit()) {
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
uint64_t elapsed = now - last_update_time;
// release token per second
if (elapsed >= 1 * 1000) {
bool resume = false;
if (token_bucket == 0) {
resume = true;
}
token_bucket += speed_limit;
token_bucket = token_bucket >= speed_limit ? speed_limit : token_bucket; // max bucket size
last_update_time = now;
if (resume) {
for (const auto& handle : downloading_handles) {
curl_easy_pause(handle, CURLPAUSE_CONT);
}
}
}
}
当桶中数量为0时表明令牌被消耗完毕,应该尝试恢复下载,如果令牌没有被消耗完,没必要轮询恢复。当然也可以维护一些bool变量来表明句柄当前状态和是否需要恢复。
关于桶最大容量和每次取最小值的做法,是为了应用扩展,当暂停下载很久时,令牌是否允许有一个爆发速度。可以针对桶的最大容量做一些修改。就算长时间没有下载任务,也只是会定时更新一下令牌桶。消耗并不高。
虽然由于curl缓冲区的存在可能每秒下载字节数量会略微超出设置速度,但是可以接受。
总结
通过每秒写入字节数的限制可以解决只能针对单文件限速时多文件限速不准的问题。更加易于控制。

浙公网安备 33010602011771号