乐于分享
好东西不私藏

C++ 网络库之 teemo:多线程下载,一行代码搞定断点续传

C++ 网络库之 teemo:多线程下载,一行代码搞定断点续传

一、你真的需要自己造这个轮子吗?

想象这样一个场景:你在做一个客户端软件的自动更新模块,需要在后台静默下载安装包。文件有 200MB,用户的网络时好时坏,还要支持暂停恢复、显示进度条、下载完后校验 MD5。

如果你打算用裸 libcurl 来实现,先不说多线程分片有多麻烦,光是断点续传的临时文件管理就能让你头皮发麻。你要自己管理 Range 请求头,自己拼临时文件,自己处理线程同步,自己计算已下载的偏移……写完一轮大概率还有 bug。

这时候,teemo(现更名为 zoe)就是为你准备的。

它是 GitHub 用户 winsoft666 开发的一个 C++ 文件下载库,底层基于 libcurl,把多线程分片、断点续传、限速、哈希校验这些功能全都封装好了,暴露出来的 API 极其简洁。项目从最初的 teemo 演进到今天的 zoe,MIT 许可,支持 Windows/Linux/macOS 三平台,vcpkg 一条命令安装。

版本演进时间线:v1.x (2019~2021) teemo 诞生,基础下载 → v2.x (2021~2022) 多线程分片、断点续传 → v3.x (2022~2024) 更名为 zoe,哈希校验/代理/SSL → v3.6+ (2025~今) vcpkg / MIT,SSD 友好写入


二、五分钟把它跑起来

依赖与安装

teemo/zoe 的唯一依赖是 libcurl。如果你已经在用 vcpkg,安装极其简单:

# vcpkg 安装(推荐)vcpkg install zoe# 手动构建 Windowsmkdir build && cd buildcmake ..cmake --build . --config Release# 手动构建 Linux / macOSmkdir build && cd buildcmake ..make

CMakeLists 里引入也很直接:

# CMakeLists.txtfind_package(zoe CONFIG REQUIRED)target_link_libraries(your_target PRIVATE zoe::zoe)

Hello Download — 最小可运行示例

先来一个最简版本,感受一下 API 的风格:

// 依赖:zoe (基于 libcurl),C++11// 编译:g++ -std=c++11 hello_zoe.cpp -lzoe -lcurl -o hello_zoe#include<zoe/zoe.h>#include<iostream>intmain(){    Zoe z;// start() 是异步的,返回一个 std::futureautofuture = z.start("https://example.com/file.zip",   // 下载 URL"file.zip",                        // 本地保存路径        [](ZoeResult result) {             // 完成回调if (result == ZoeResult::SUCCESSED)std::cout << "下载成功!" << std::endl;elsestd::cout << "下载失败,错误码:" << (int)result << std::endl;        },        [](int64_t total, int64_t downloaded) {  // 进度回调if (total > 0)printf("进度:%3d%%\r", (int)(downloaded * 100 / total));        },        [](int64_t bytes_per_second) {     // 速度回调printf("速度:%.2f MB/s\r", bytes_per_second / 1048576.0f);        }    );future.wait();   // 阻塞等待下载完成return0;}
下载成功!进度:100%速度:3.47 MB/s

三个 Lambda 回调,一个 future.wait(),连接池、分片、重试逻辑全部在库里。这就是 teemo 的核心设计哲学:你只管告诉我下哪儿、存哪儿、通知我进度,其他全包。


三、多线程分片到底是怎么跑的?

要理解 teemo 为什么快,得先搞清楚它的多线程分片下载原理。简单说,就是把一个大文件切成 N 段,每个线程用 HTTP Range 请求并发下载各自的段,最后合并成完整文件。

多线程分片下载原理图file.zip (100MB) → 4 个线程分别请求 Range 0~25MB / 25~50MB / 50~75MB / 75~100MB → 各自写入 slice_0.tmp ~ slice_3.tmp → 合并成完整文件。断点续传通过保留 tmp 文件实现,下载完成后进行 MD5/SHA256 完整性校验。

HTTP Range 请求长这样:

GET /file.zip HTTP/1.1Range: bytes=26214400-52428799

服务器如果支持 Accept-Ranges(大多数文件服务器都支持),就会返回对应数据段。teemo 在内部把这些细节全部处理掉了——你只需要告诉它线程数。

断点续传的关键在于:每个 .tmp 分片文件在下载时会持续落盘,程序崩溃或网络中断后,下次启动发现 tmp 文件还在,就直接从上次的断点继续,不用重头下载。


四、真正项目里你会这样用

下面这个例子更贴近实际需求:多线程 + 限速 + 超时重试 + 完成后校验 MD5:

// 完整配置示例:4线程下载,限速1MB/s,MD5校验// 依赖:zoe v3.x,C++11#include<zoe/zoe.h>#include<iostream>#include<atomic>intmain(){    Zoe z;// ---- 线程数与速度配置 ----    z.setThreadNum(4);                              // 4线程并发下载    z.setMaxDownloadSpeed(1 * 1024 * 1024);         // 最大速度 1MB/s    z.setMinDownloadSpeed(100 * 102410);          // 最小速度 100KB/s,持续10秒触发失速重试// ---- 超时与重试 ----    z.setNetworkConnectionTimeout(5000);            // 连接超时 5s    z.setRetryTimesOfFetchFileInfo(3);              // 获取文件信息最多重试3次    z.setExpiredTimeOfTmpFile(3600);                // tmp 文件 1小时内有效(断点续传窗口)// ---- 磁盘缓存(关键!)----// 每个分片先写到内存缓冲,积累到 10MB 再一次性落盘// 大幅减少对 SSD 的随机写入次数,延长固态盘寿命    z.setDiskCacheSize(10 * 1024 * 1024);           // 10MB 内存缓存// ---- 分片策略 ----// FixedNum:固定分片数;FixedSize:每片固定大小    z.setSlicePolicy(SlicePolicy::FixedNum, 4);     // 分成4片(对应4线程)// ---- 哈希校验 ----    z.setHashVerifyPolicy(        HashVerifyPolicy::AlwaysVerify,        HashType::MD5,"d41d8cd98f00b204e9800998ecf8427e"// 预期 MD5 值    );// ---- 进度 & 调试 ----    z.setVerboseOutput([](const utf8string& msg) {// 开启详细日志,调试时很有用printf("[DBG] %s", msg.c_str());    });std::atomic<int> last_pct{0};// ---- 启动下载(异步)----autofuture = z.start("https://example.com/large_installer.zip","./large_installer.zip",        [](ZoeResult result) {             // 结果回调constchar* msg[] = {"成功""URL无效""文件信息获取失败","目标路径无效""磁盘空间不足","哈希校验失败""用户取消""速度过慢"            };int idx = (int)result;printf("\n下载结果:%s\n", idx < 8 ? msg[idx] : "未知错误");        },        [&last_pct](int64_t total, int64_t downloaded) {  // 进度回调if (total <= 0return;int pct = (int)(downloaded * 100 / total);if (pct != last_pct.load()) {                last_pct.store(pct);printf("  [%3d%%] %.1f / %.1f MB\r",                    pct,                    downloaded / 1048576.0,                    total / 1048576.0);                fflush(stdout);            }        },        [](int64_t bps) {                  // 速度回调printf("  速度:%.2f MB/s   \r", bps / 1048576.0f);        }    );future.wait();return0;}
[DBG] Fetch file info: https://example.com/large_installer.zip[DBG] File size: 209715200 bytes (200MB), Slice num: 4[DBG] Thread 0: Range 0 - 52428799[DBG] Thread 1: Range 52428800 - 104857599...  [ 47%] 94.0 / 200.0 MB  速度:3.18 MB/s...  [100%] 200.0 / 200.0 MB下载结果:成功

⚠️ 踩坑警告setMinDownloadSpeed 的第二个参数是”持续多少秒低于阈值才触发”,不是总超时。如果设置太小(比如 1 秒),在网络抖动时会频繁重启下载任务,反而更慢。建议设为 10~30 秒。


五、跟同类库比一比

很多人在问:我直接用 libcurl 不就行了,为什么要多一层封装?

特性
teemo/zoe
libcurl(裸用)
aria2c
libtorrent
多线程分片
需自行实现
协议耦合
断点续传
需自行实现
下载速度限制
部分支持
哈希校验
✓ MD5/SHA
外部验证
SSD 写入友好
✓ 磁盘缓存
可嵌入 C++ 程序
独立进程

aria2c 虽然功能强大,但它是独立进程,你的 C++ 程序要通过 RPC 与它通信,架构上增加了一层耦合。libtorrent 专注 P2P,HTTP 下载只是附带功能。libcurl 裸用太低级,每个功能都要自己实现。

teemo/zoe 的定位很明确:给 C++ 程序员提供一个「开箱即用」的文件下载模块,不是命令行工具,不是 P2P 客户端,就是一个干净的库。


六、SSD 友好写入——一个你可能没注意到的细节

多线程下载有一个容易被忽视的问题:频繁的随机写入会大幅加速 SSD 磨损

假设你用 4 个线程同时下载,每个线程每收到一个 TCP 数据包就往磁盘写一次,每次写入 16KB,那 200MB 的文件就有 200 × 1024 / 16 / 4 = 3200 次写入,全部是随机 I/O。

teemo 的解决方案是 磁盘缓存(Disk Cache):每个下载分片的数据先写到内存缓冲区,等缓冲区积满到设定阈值(比如 10MB)后,一次性顺序写入磁盘。随机写变顺序写,I/O 次数骤降。

// 设置磁盘缓存大小// 缓存越大,写盘越少,内存占用越高// 推荐值:单线程 5~10MB,多线程总量 20~40MBz.setDiskCacheSize(10 * 1024 * 1024);  // 每个分片 10MB 缓存// 设置未完成分片的保存策略// ALWAYS_SAVE:即使没攒满缓冲区也强制写盘(防止崩溃丢数据)// NEVER_SAVE:只在缓冲区满时写盘(性能最高,但崩溃可能丢部分进度)z.setUncompletedSliceSavePolicy(UncompletedSliceSavePolicy::ALWAYS_SAVE);

💡 对于下载工具类应用,推荐 ALWAYS_SAVE + 适中的缓冲区大小(10~20MB)。对于批量后台下载(网速稳定、不在乎偶尔重下一个分片),可以用 NEVER_SAVE + 大缓冲区换性能。


七、实战:嵌入到软件更新模块

把 teemo 集成进软件自动更新流程,这是它最典型的使用场景之一。下面是一个稍微完整一些的封装:

// updater.h - 软件更新模块封装示例// 依赖:zoe v3.6+,C++14#pragma once#include<zoe/zoe.h>#include<functional>#include<string>#include<future>classUpdater {public:using ProgressCallback = std::function<void(int pct, double speed_mb)>;using DoneCallback     = std::function<void(bool ok, conststd::string& err)>;    Updater() {        zoe_.setThreadNum(4);        zoe_.setDiskCacheSize(20 * 1024 * 1024);    // 20MB 磁盘缓存        zoe_.setMinDownloadSpeed(50 * 102420);     // 20秒内低于50KB/s算失速        zoe_.setNetworkConnectionTimeout(8000);    }// 开始下载更新包,非阻塞std::future<voidstartUpdate(conststd::string& url,conststd::string& savePath,conststd::string& md5,           // 可以为空字符串跳过校验        ProgressCallback onProgress,        DoneCallback onDone){// 配置哈希校验(如果提供了 MD5)if (!md5.empty()) {            zoe_.setHashVerifyPolicy(                HashVerifyPolicy::AlwaysVerify,                HashType::MD5,                md5            );        }return zoe_.start(            url, savePath,            [onDone](ZoeResult r) {bool ok = (r == ZoeResult::SUCCESSED);std::string err;if (!ok) {// ZoeResult 是 enum,可直接映射错误描述                    err = "错误码 " + std::to_string((int)r);                }if (onDone) onDone(ok, err);  // 通知调用方            },            [onProgress](int64_t total, int64_t downloaded) {if (total > 0 && onProgress) {int pct = (int)(downloaded * 100 / total);                    onProgress(pct, 0.0);       // 速度在速度回调里更新                }            },            [onProgress](int64_t bps) {if (onProgress) onProgress(-1, bps / 1048576.0);  // -1 表示只更新速度            }        );    }// 取消下载(stop 是线程安全的)voidcancel(){ zoe_.stop(); }private:    Zoe zoe_;};// ---- 使用示例 ----// Updater u;// auto fut = u.startUpdate(//     "https://cdn.example.com/v2.0.1/installer.exe",//     "C:/Temp/installer.exe",//     "a3d2e1f4b5c6d7e8f9a0b1c2d3e4f5a6",//     [](int pct, double speed) {//         if (pct >= 0) printf("进度:%d%%\n", pct);//         else printf("速度:%.2f MB/s\n", speed);//     },//     [](bool ok, const std::string& err) {//         if (ok) printf("更新包下载完成,启动安装...\n");//         else printf("下载失败:%s\n", err.c_str());//     }// );// fut.wait();
进度:0%速度:0.00 MB/s进度:12%速度:3.47 MB/s进度:45%速度:3.51 MB/s...进度:100%更新包下载完成,启动安装...

⚠️ 踩坑警告Zoe 对象不要在回调函数里析构!因为回调是在内部工作线程中触发的,如果你在 onDone 回调里直接 delete this(删除持有 Zoe 成员的对象),会出现在自己的工作线程里 join 自己的死锁。正确做法是在回调里 post 一个事件给主线程,由主线程来做清理。


八、什么时候该用,什么时候不该用

teemo 的适合场景:

  • 客户端软件的自动更新模块(大文件、断点续传、校验)
  • 游戏/应用的资源热更新(多文件并发、限速不抢用户带宽)
  • 嵌入式工具软件里的固件/数据包下载(跨平台、依赖少)
  • CI/CD 流程里的产物下载脚本(代替 wget/curl,更健壮)

不适合的场景:

  • 需要 P2P / BitTorrent(用 libtorrent)
  • 只需要简单的 HTTP GET 拿一个小文件(libcurl 直接够用)
  • 需要浏览器级别的 cookie、session 管理(libcurl 原生更灵活)

GitHub 仓库:https://github.com/winsoft666/zoe,vcpkg 包名:zoe