简介这是一份MFC单文档/对话框框架下的文件下载工程源码包面向Win32桌面应用初学者与需要实现HTTP下载、进度反馈和自定义界面的C开发者。包内包含完整的工程文件、界面位图资源、说明文档及编译生成文件涵盖CInternetSession/CHttpFile的HTTP下载流程、定时器驱动进度刷新、控件的OnPaint背景重绘、图片按钮状态切换以及CProgressCtrl进度条同步等关键实现。共42个文件以cpp/h源码、bmp按钮与背景图、rc资源脚本、工程配置sln/vcproj及可执行exe为主压缩包10.64MB。已有336人学习下载。通过该工程可快速搭建一个支持URL输入、继续/暂停操作并实时显示下载状态的实战项目便于对照学习MFC网络编程、UI交互与资源管理也能直接参考其图片按钮和进度条控件的封装写法。1. 文件下载工程一个真实能跑的 MFC 下载器不只是“能下载就行”这个工程是一个用 MFC 对话框写出来的完整文件下载工具不是那种只丢一个URLDownloadToFile调用的教学 Demo。它里面把下载、进度条刷新、定时器驱动、图片按钮、控件背景这几个点全串在了一条主线上运行起来就是一个带自绘按钮和动态进度的下载界面。我最初拿到这套资源是想找一个能直接改的 MFC 下载框架结果发现里面真正值钱的是定时器与消息循环的配合方式以及那套自绘按钮和背景重绘的思路。适合正在做 MFC 界面程序、需要给项目加下载功能但不想从 WinInet API 一步步写起的开发者也适合想看对话框程序怎么让界面“活”起来的初学者。2. 下载核心模块选型为什么不用 URLDownloadToFile而选 CInternetSession CHttpFile拿到这个工程第一件事是看它到底用哪种方式拉文件。工程里没有用现成的URLDownloadToFile而是用 MFC 的 WinInet 封装类CInternetSession和CHttpFile自己写了下载流程。这个选择很关键因为URLDownloadToFile虽然代码少但它把进度回调、超时控制、暂停续传这些全锁在黑匣子里你想在进度条上做文章几乎只能靠轮询文件大小非常别扭。2.1 下载会话的创建与参数配置工程下载模块的入口是一个DownloadFile函数流程是先创建CInternetSession再根据 URL 打开一个CHttpConnection最后请求文件。核心代码大概是这样CInternetSession session(_T(MyDownloader/1.0), 0, INTERNET_FLAG_DONT_CACHE); CHttpFile* pFile nullptr; try { // 第二个参数是 HTTP 动词GET 拉取文件 pFile (CHttpFile*)session.OpenURL(strUrl, 1, INTERNET_FLAG_TRANSFER_BINARY | INTERNET_FLAG_RELOAD, NULL, 0); pFile-QueryInfo(HTTP_QUERY_CONTENT_LENGTH, strContentLength); // 总长度转 64 位整数供进度条计算百分比 m_nTotalLength _ttoi64(strContentLength); // 然后循环 Read 写文件这部分在下一节展开 } catch (CInternetException* pEx) { TCHAR szErr[256]; pEx-GetErrorMessage(szErr, 256); // 记录错误同时清理会话 pEx-Delete(); }这里有几个参数值得说。INTERNET_FLAG_TRANSFER_BINARY必须加不加以文本方式读取会把二进制文件搞坏尤其是压缩包和 exe下载完打不开是常见翻车点。INTERNET_FLAG_RELOAD表示强制从服务器拉取不读本地缓存用于避免进度条都跑完了但文件是旧缓存的情况。OPENURL的第一个参数是代理名第二个 0 表示无代理实测内网下载不需要代理时直接给 0 最省心。2.2 循环读取与文件写入拿到CHttpFile指针后下载主循环用Read分块读数据同步写入本地文件。工程里默认缓冲区是 64KB这个值不是随手写的。调小到 4KB 进度条会频繁刷新CPU 占用高调到 256KB 以上网络波动时界面响应会明显变慢。BYTE buffer[65536]; CString strLocalFile m_strSavePath; CFile localFile; if (!localFile.Open(strLocalFile, CFile::modeCreate | CFile::modeWrite)) return; while (true) { UINT nRead pFile-Read(buffer, sizeof(buffer)); if (nRead 0) break; localFile.Write(buffer, nRead); m_nDownloaded nRead; // 只通知进度不直接在这里更新 UI if (m_hWnd) ::PostMessage(m_hWnd, WM_UPDATE_PROGRESS, (WPARAM)nRead, 0); }循环里没有直接调进度条控件的SetPos而是发自定义消息WM_UPDATE_PROGRESS这是工程里一个很小的设计点但非常值得抄。原因是下载循环运行在主线程里如果每读一块就SetPos消息队列会被大量WM_CTLCOLOR、WM_PAINT滞后处理界面看起来就卡。用PostMessage把进度更新请求丢进队列让OnUpdateProgress在空闲时统一刷新进度条走得更平滑。2.3 处理超时与断线重连的边界工程里的下载类还封装了超时设置通过CInternetSession::SetOption设置连接和读取超时时间。这里的坑是 MFC 的SetOption需要先拿到句柄而且参数类型是LPVOID传 int 时容易踩内存错位。安全写法是把超时值放进一个DWORD变量再取地址。DWORD dwTimeout 15000; // 15 秒连接超时 session.SetOption(INTERNET_OPTION_CONNECT_TIMEOUT, dwTimeout, sizeof(DWORD)); DWORD dwReadTimeout 10000; session.SetOption(INTERNET_OPTION_RECEIVE_TIMEOUT, dwReadTimeout, sizeof(DWORD));超时设多久要看实际网络环境。做内网工具 10 秒足够公网下载建议给到 30 秒否则服务器响应稍慢就直接中断用户会以为程序坏了。工程里还把断线重试次数写成了配置项默认 3 次每次重试前等待 2 秒。这个重试逻辑不是简单重新调用OpenURL而是会先释放旧的CHttpFile和CInternetSession再重新创建避免句柄泄漏导致内存疯涨。2.4 文件下载的完整状态机整个下载流程其实是一个状态机空闲、连接中、下载中、暂停、完成、失败。工程里用m_nState整型变量表示配合定时器做界面刷新。状态迁移集中在OnDownload、OnPause、OnStop三个消息处理函数中。状态值状态切换触发条件界面表现空闲0初始、下载完成后重置按钮可用进度条 0%连接中1点击下载按钮OpenURL 之前按钮置灰显示“连接中”下载中2OpenURL 成功进度条刷新显示速度和百分比暂停3点击暂停按钮定时器停止但连接保留完成4Read 返回 0进度条 100%按钮切换为“重新下载”失败5任何异常捕获弹出错误信息重置状态这个状态机看着简单实际帮助很大。没有它下载过程中点击按钮会重复开启新线程或重复打开文件导致文件被占用或进度条回跳。3. 定时器驱动界面刷新让进度条不“按帧跳”而是“按状态走”这个工程里进步度条不是用一个死循环刷新而是借助SetTimer定时器每隔 300ms 读一次“当前已下载字节数”并换算成百分比。这种方式把网络 I/O 和界面绘制解耦是 MFC 小程序里最稳妥的做法。3.1 定时器的创建与销毁时机定时器在下载按钮触发时创建在下载完成、失败或用户手动停止时销毁。工程里OnInitDialog中没有搞常驻定时器只有点击下载后才SetTimer(1, 300, NULL)。这样避免程序空闲时一直在空转也避免定时器回调里访问尚未初始化的连接对象。void CMyDownloadDlg::OnBnClickedDownload() { if (m_nState ! STATE_IDLE m_nState ! STATE_COMPLETED) return; // 启动后台下载这里不开线程用消息循环分片处理 m_nDownloaded 0; m_nTotalLength 0; SetTimer(TIMER_DOWNLOAD, 300, NULL); // 调用实际下载函数内部同步循环 DownloadFile(m_strUrl, m_strSavePath); } void CMyDownloadDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_DOWNLOAD) { // 更新界面进度条、文字、剩余时间 UpdateProgressUI(); } CDialogEx::OnTimer(nIDEvent); }注意这里的下载函数是同步的但它没有把界面卡死。原因是下载循环里每读一块内容就PostMessage让主窗口消息队列不断有新的WM_UPDATE_PROGRESS进来OnTimer也能正常被触发。如果换成Sleep等待或者下载函数内部用了WaitForSingleObject等阻塞调用定时器就会罢工界面直接死白。3.2 进度条控件与百分比的换算进度条控件用的是典型CProgressCtrl但没有设setRange为 0-100而是把范围直接设为 0 到总字节数。这样省去换算SetPos(m_nDownloaded)一步到位。void CMyDownloadDlg::UpdateProgressUI() { if (m_nTotalLength 0) return; m_ProgressCtrl.SetRange32(0, (int)m_nTotalLength); m_ProgressCtrl.SetPos((int)m_nDownloaded); int nPercent (int)(m_nDownloaded * 100 / m_nTotalLength); CString strPercent; strPercent.Format(_T(%d%%), nPercent); m_StaticPercent.SetWindowText(strPercent); // 如果已完成停止定时器并复位状态 if (m_nDownloaded m_nTotalLength) { KillTimer(TIMER_DOWNLOAD); m_nState STATE_COMPLETED; } }这段代码里有一个容易被忽略的坑SetRange32的参数是int类型而文件大小经常超过 2GB强转后溢出的总长度会让进度条跑到一半就跳满或者卡住。工程里对这个做了限制超过 2GB 的文件直接采用百分比换算方式也就是定义SetRange32(0, 100)再手动计算nPos min(100, nPercent)。我一般会在下载开始前就判断文件大小超过 1.5GB 就自动切换为“隐藏真实字节数只显示百分比”的模式避免大文件下载时SetPos的 int 被截断。3.3 速度与剩余时间的计算定时器另一个职责是算瞬时速度和剩余时间。工程里记录上一次刷新的时间和已下载字节两次作差再求平均速度显示在静态文本上。代码也不复杂ULARGE_INTEGER curSpeed, deltaSize, deltaTime; deltaSize.QuadPart m_nDownloaded - m_nLastTickBytes; deltaTime.QuadPart GetTickCount64() - m_llLastTickTime; DWORD dwSpeed 0; if (deltaTime.QuadPart 0) dwSpeed (DWORD)(deltaSize.QuadPart * 1000 / deltaTime.QuadPart); // 字节/秒 CString strSpeed; if (dwSpeed 1024 * 1024) strSpeed.Format(_T(%.2f MB/S), dwSpeed / 1024.0 / 1024.0); else strSpeed.Format(_T(%.2f KB/S), dwSpeed / 1024.0); m_StaticSpeed.SetWindowText(strSpeed);这里要特别说一下GetTickCount64与GetTickCount的区别。工程代码早期用的是GetTickCount下载超过 49.7 天后会归零速度值会出现负数。虽然这个下载工具大概率没人会连续跑那么久但在 8 月某个版本里确实有用户反馈下载超过 3 小时后速度变成负数后来改成GetTickCount64彻底解决。这个坑值得记下来凡是做长时间运行的程序不要再碰旧的GetTickCount。3.4 暂停与恢复暂停功能这个工程里是通过销毁定时器 保持连接对象不释放实现的。暂停时记录当前文件指针位置恢复时继续Read不重新建立连接。这需要CHttpFile支持断点续传也就是请求头里带上Range字段。工程代码里预留了m_bSupportRange标志if (m_bSupportRange) { CString strRange; strRange.Format(_T(bytes%I64d-), m_nDownloaded); pFile-AddRequestHeaders(_T(Range: ) strRange); }真正的断点续传还需要服务器返回 206 状态码这个在第六节再展开。暂停时如果直接销毁连接对象下次恢复就得从 0 开始重新下所以工程暂停流程是KillTimer 保存偏移量 置状态为暂停连接和文件句柄都不关。等用户点“继续”重新创建一个CHttpFile带上Range头再打开同一文件并 Seek 到偏移位置继续写。4. 图片按钮与控件背景设置让 MFC 界面不再那么“原生”下载工具的界面本身就是噱头的一部分——用图片按钮替代默认的灰色按钮用自绘背景替代白色对话框。这套资源里最花时间的不是下载逻辑而是让按钮在鼠标悬停和按下时显示不同图片以及控件背景能和主界面融合不显出一块白底。4.1 图片按钮的三态实现工程里按钮不是用CBitmapButton因为CBitmapButton需要三个资源 ID 对应正常、按下、聚焦资源管理非常麻烦。它用的是自绘按钮也就是重载CButton的DrawItem在消息处理里根据按钮状态绘制不同位图。这里先看消息映射void CMyDownloadDlg::OnTcnHotTrackDownload(NMHDR *pNMHDR, LRESULT *pResult) { LPNMTTDISPINFO pTTT reinterpret_castLPNMTTDISPINFO(pNMHDR); if (m_btnDownload.IsTrackingNow()) m_btnDownload.SetPicture(IDB_BTN_HOVER); *pResult 0; }实际上工程里的自绘按钮更直接在OnMouseMove里检测鼠标是否在按钮区域内然后切换背景图。核心的DrawItem代码如下void CImageButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); CRect rcBtn lpDrawItemStruct-rcItem; UINT uState lpDrawItemStruct-itemState; CBitmap bmp; if (uState ODS_SELECTED) bmp.LoadBitmap(IDB_BTN_DOWN); // 按下状态 else if (m_bHover) bmp.LoadBitmap(IDB_BTN_HOVER); // 悬停状态 else bmp.LoadBitmap(IDB_BTN_NORMAL); // 普通状态 // 绘制位图到按钮区域同时去掉边框 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap* pOld memDC.SelectObject(bmp); pDC-StretchBlt(rcBtn.left, rcBtn.top, rcBtn.Width(), rcBtn.Height(), memDC, 0, 0, 32, 32, SRCCOPY); memDC.SelectObject(pOld); }这个自绘按钮的关键在于StretchBlt拉伸位图虽然简单但图片质量会差尤其按钮尺寸和原图不一致时边缘会出现锯齿。工程里为了一下到位直接把按钮固定位 100x36位图也切成了同样尺寸。如果你的按钮要支持 DPI 缩放这里就得换成GDI平滑缩放否则高 DPI 下图片边界会很脏。4.2 控件背景设置的三种常用手段MFC 中设置控件背景常见三种OnCtlColor改画刷、WM_ERASEBKGND自绘背景、给控件设BS_OWNERDRAW。工程里主对话框和几个静态控件用的是OnCtlColor方案重写返回一个画刷句柄HBRUSH CMyDownloadDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor CTLCOLOR_STATIC) { // 让静态文字背景透明露出对话框位图背景 pDC-SetBkMode(TRANSPARENT); pDC-SetTextColor(RGB(255, 255, 255)); return (HBRUSH)GetStockObject(NULL_BRUSH); } if (nCtlColor CTLCOLOR_DLG) { // 对话框背景使用位图画刷 if (!m_hBackBrush) { CBitmap bmp; bmp.LoadBitmap(IDB_MAIN_BG); m_hBackBrush CreatePatternBrush((HBITMAP)bmp.GetSafeHandle()); } return m_hBackBrush; } return hbr; }这里有个很经典的坑CreatePatternBrush使用的是位图的左上角颜色作为透明色吗不是。它会用位图来自动填充背景但如果你同时把SetBkMode(TRANSPARENT)应用到静态控件那么在 OnCtlColor 返回画刷后静态文本的背景不会显示为画刷的图案而是白色。原因在于NULL_BRUSH只对静态框透明但对话框背景上的静态文字需要把父窗口背景先绘制出来否则文字会直接叠在白底上。工程的处理方式是让静态控件也响应WM_CTLCOLORSTATIC并且把文字背景设为透明再把对话框背景画刷返回给静态控件。if (pWnd-GetDlgCtrlID() IDC_STATIC_PRECENT) { pDC-SetBkMode(TRANSPARENT); return m_hBackBrush; // 不是 NULL_BRUSH而是同一把背景刷 }这样文字背景才能“看到”对话框背景。这个细节调试起来非常揪心很多人做完背景图之后发现所有文字都浮在白底上其实就是这里返回了NULL_BRUSH而不是背景画刷。4.3 图片按钮与背景的层级关系按钮放在背景位图上方要完美融合需要按钮本身是透明的也就是绘制按钮位图时把位图里的背景色抠掉。工程的做法是给按钮图片单独定一个透明色用TransparentBlt绘制#define RGB_MASK RGB(255, 0, 255) // 以品红色为透明色 void CImageButton::DrawTransparent(CDC* pDC, CBitmap bmp, CRect rcDest) { CDC memDC; memDC.CreateCompatibleDC(pDC); BITMAP bm; bmp.GetBitmap(bm); // TransparentBlt 需要 Win2000MFC 里可以直接调用 TransparentBlt(pDC-GetSafeHdc(), rcDest.left, rcDest.top, rcDest.Width(), rcDest.Height(), memDC.GetSafeHdc(), 0, 0, bm.bmWidth, bm.bmHeight, RGB_MASK); }TransparentBlt的来源位图里的指定颜色会被替换成目标区域原有的像素。这里按习惯选了品红你也可以选别的但不要用图片里真实存在的颜色比如按钮图标是蓝色块透明色就不能用蓝色。这部分的坑集中在TransparentBlt只能做一色透明按钮半透明效果做不了如果按钮图片带阴影阴影边缘会被抠掉一半看起来像狗啃。工程里为了避免这个问题把按钮图片的边缘阴影直接去掉了一律用硬边矩形按钮。如果你要圆角按钮透明色抠不出来圆角需要配合CreateRoundRectRgn做区域裁剪。5. 避坑指南定时器、下载线程、界面卡死这四个问题我全踩过做这个工程时踩的坑比写功能还多而且每一个都是网上帖子讲一半、实际运行才炸掉的经典问题。这里按“现象 → 原因 → 解决”写四条最有代表性的。5.1 进度条不走动但文件已经下载完了现象点击下载后文件在后台正常下载可进度条一直停在 0%等文件结束时才突然跳到 100%。原因CProgressCtrl的SetPos或SetRange32被调用时使用了m_nDownloaded变量但在下载循环里没有正确更新这个变量。更典型的是下载循环跑在子线程里界面线程的定时器读取时变量还未同步。工程早期版本是把下载放在了工作线程可进度条却由主线程定时器直接读m_nDownloaded没有用volatile或临界区保护导致定时器读到的永远是旧值。解决要么把下载放到主线程让消息循环触发更新要么用InterlockedExchange更新共享字节数。工程最终把下载拉回主线程成了前面代码中的同步循环加PostMessage的方式用PostMessage携带本次读取字节数在OnUpdateProgress里累加m_nDownloaded彻底绕开线程同步问题。LRESULT CMyDownloadDlg::OnUpdateProgress(WPARAM wParam, LPARAM lParam) { m_nDownloaded (ULONGLONG)wParam; // 消息队列保证顺序 UpdateProgressUI(); return 0; }这里PostMessage的消息是排队发送的下载循环每读一块发一条界面线程会按顺序处理累加结果不会乱。这个技巧比临界区更简单但也要求下载函数和 UI 在同一线程否则消息不会被处理。5.2 下载过程中拖动窗口下载速度骤降甚至超时现象下载大文件时用户拖动对话框网络速度明明很高可下载进度反而变慢有时直接连接超时。原因拖拽窗口时主线程忙于处理WM_MOVE、WM_PAINT、WM_SIZE下载循环的Read调用被卡住底层 TCP 缓冲区满后服务器停止发送进而触发超时。这就是典型的单线程阻塞问题。解决在下载开始前禁止窗口移动或重绘吗工程里是用WM_SYSCOMMAND拦截拖拽消息在下载期间禁用最大化框。另一个的办法是下载期间把定时器刷新频率降低给消息循环更多喘息时间。但最有效的还是把下载放到独立线程界面主线程专心处理消息。工程里后来做了一个折中下载循环每次Read后调用PeekMessage处理窗口消息手动跳动消息泵拖拽时也不再卡读数据。MSG msg; while (PeekMessage(msg, m_hWnd, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); }这个做法只适合临时救急正规方案还是AfxBeginThread开线程。工程里的完整版最终用了工作线程配合事件通知 UI 更新。5.3 按钮背景花屏颜色像被油漆泼了现象自绘按钮第一次显示正常但鼠标移过或窗口重绘后按钮区域变成一块块马赛克或绿紫杂色。原因DrawItem里创建的CBitmap是局部变量但StretchBlt完成后位图句柄被释放按钮区域重绘时使用了无效句柄。也可能是CDC memDC和pDC的兼容性问题导致SelectObject返回后位图被提前释放。解决把三种状态位图作为CImageButton的成员变量在OnInitDialog中一次性LoadBitmapDrawItem里只SelectObject不重复加载。同时注意DrawItem中pDC-SaveDC()与RestoreDC()成对调用防止状态污染后面的绘制。void CImageButton::OnInit() { m_bmpNormal.LoadBitmap(IDB_BTN_NORMAL); m_bmpHover.LoadBitmap(IDB_BTN_HOVER); m_bmpDown.LoadBitmap(IDB_BTN_DOWN); }如果你发现重绘后按钮显示的是上次位图的残留检查是不是忘了调用Invalidate()。MFC 在某些场景不会自动重绘按钮需要在鼠标离开按钮时手动Invalidate(FALSE)触发DrawItem。5.4 定时器消息挤压下载完成后进度条还在跳现象下载已经完成进度条已经 100%但界面上速度还在闪烁几秒后才停。原因KillTimer在下载线程或OnUpdateProgress中被调用但定时器回调里还有未处理完的消息或者OnTimer里没有先判断m_nState是否已完成就继续刷新。解决在OnTimer入口加状态判断如果m_nState STATE_COMPLETED || m_nState STATE_FAILED直接返回然后再在UpdateProgressUI中判断文件是否下载完时KillTimer。并且要把KillTimer放在退出下载循环之后而不是循环内部。void CMyDownloadDlg::OnTimer(UINT_PTR nIDEvent) { if (m_nState STATE_COMPLETED || m_nState STATE_FAILED) return; CDialogEx::OnTimer(nIDEvent); }另外下载完成后最好显式调用m_ProgressCtrl.SetPos(m_nTotalLength)确保进度条停在满格而不是停留在最后一次SetPos的值上。6. 进阶玩法把下载器改成可复用的自绘下载类顺手加上断点续传这个工程如果只是拿来做毕业设计或者小工具前面四章够用了。但如果你想把下载功能独立成一个类放进其他 MFC 项目那还需要做几件事。第一是把CInternetSession、CHttpFile、状态机、进度消息全部封进一个CFileDownloader类第二是加入文件校验第三是把断点续传真正落到可用的程度。我一般会这么设计下载器接口类内部维护Start、Pause、Resume、Stop通过回调函数通知外部进度。外部只需要设置 URL、保存路径、超时时间然后实现一个OnProgress(ULONGLONG done, ULONGLONG total)回调。这样界面层不再直接接触 WinInet测试的时候也能脱离 UI 跑单元验证。断点续传这块工程里最需要补的是服务器支持判断。请求头加Range: bytes之后要检查QueryInfo(HTTP_QUERY_STATUS_CODE)返回的是 206 还是 200。返回 200 说明服务器忽略 Range 指令那就不能续传需要从零开始写文件否则会在文件头部追加新内容数据直接损坏。DWORD dwStatus 0; pFile-QueryInfo(HTTP_QUERY_STATUS_CODE, dwStatus); if (dwStatus ! 206 dwStatus ! 200) { // 服务器返回异常状态码 SetErrorState(STATUS_HTTP_ERROR); return; } bool bCanResume (dwStatus 206);收到 206 后文件打开方式要改为modeCreate | modeWrite吗不对续传必须用CFile::modeOpen并SeekToEnd()再把m_nDownloaded设为当前文件字节数。实现如下if (bCanResume) { localFile.Open(strLocalPath, CFile::modeReadWrite); localFile.SeekToEnd(); m_nDownloaded localFile.GetPosition(); } else { localFile.Open(strLocalPath, CFile::modeCreate | CFile::modeWrite); m_nDownloaded 0; }另外文件校验建议在下载完成时做一次GetFileSize对比而不是对比下载字节数。因为服务器返回的内容长度可能比实际文件大比如动态页面或压缩传输。工程里在完成态会再校验一次文件大小和m_nTotalLength不一致则判定为失败并删除半截文件。最后说一个我自己用这套资源的习惯把定时器间隔、超时时间、缓冲区大小、重试次数全部做成外部可调参数而不是写死在代码里。下载工具这个东西不同网络环境差别很大内网 1MB 缓冲区和 64KB 缓冲区看不出区别但公网弱网环境下 64KB 缓冲区明显更容易中断。从那以后我每做一版下载器都会强制自己在 Release 配置下完整跑一遍 1GB 以上文件并把日志打开观察断点续传时 Range 头是否生效。如果日志里读到的状态码不是 206就说明服务器不支持或者请求头构造有问题。这个检查步骤成了我固定习惯省下了太多线上翻车的时间。希望这类工程里藏的细节和经验能帮到你少走我这些弯路。本文还有配套的精品资源点击获取