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 * 1024, 10); // 最小速度 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 <= 0) return;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 不就行了,为什么要多一层封装?
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 * 1024, 20); // 20秒内低于50KB/s算失速 zoe_.setNetworkConnectionTimeout(8000); }// 开始下载更新包,非阻塞std::future<void> startUpdate(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。
夜雨聆风