
上个月有个朋友找我调一个上传问题他们单位的网络是完全隔离的任何外网资源都访问不了业务系统里经常要传几个G的工程文件、视频和压缩包前端用的是 WebUploader。现象很典型文件一超过 1GB 就大概率失败传一半断掉之后只能从头再来后台日志翻半天也看不出个所以然。我当时就说这大概率不是 WebUploader 的问题而是把断点续传当成前端组件的事了实际上它需要前后端一起配合才能完成。这篇文章就围绕“在无外网的局域网环境中怎么用 WebUploader 把大文件断点续传这件事做扎实”来写。我会把分片原理、前端配置、服务端接收合并、以及实测中容易踩的坑都串一遍。适合正在维护老后台系统、又必须面对内网大文件传输场景的开发者看也适合准备选型上传组件但被“WebUploader 太老”这个印象劝退的团队参考。1. 局域网里大文件上传失败的原因先想明白断点续传解决的是什么1.1 不是网速的问题是会话可靠性的问题很多人第一反应是“内网带宽不够”但实际跑到千兆局域网时带宽根本不缺。真正的问题几乎都出在会话可靠性上一个几G的文件被当成一个整体上传中间任何一个环节断一下整个任务就直接废掉。我梳理过内网上传失败的几类典型原因Web服务器或反向代理的连接超时。比如 Nginx 默认的proxy_read_timeout只有 60 秒前端可能已经传了大半网关那边直接把连接掐了。浏览器标签页被误关、办公终端休眠唤醒、网络闪断导致客户端会话失效。服务端进程在处理大请求时内存暴涨被系统杀掉已传数据全部丢失。内网终端装了安全管控软件对长连接有激进的空闲策略一段时间没有新数据就主动断开连接。这些场景和网速没有直接关系根本矛盾在于“一个大事务”比“多个小事务”脆弱得多。断点续传的实质就是把大事务拆成一组可独立提交、可记录状态的小事务。每传完一个分片就产生了永久性成果即使后面全断也只是损失最后一个分片代价完全可控。1.2 为什么 WebUploader 这个老组件还是离不开WebUploader 在社区里流行了很多年大量后台系统、企业 OA、内部管理平台都在用很多定制维护的旧项目换组件成本非常高。它的兼容策略是 HTML5 优先、Flash 兜底早期内网终端还有一些老版本浏览器WebUploader 反而比那些只支持现代浏览器的组件更稳。但它有一个比较尴尬的事实虽然组件本身支持分片上传chunked但不像后来的上传库那样内置完善的“服务端已传分片查询”机制。要做真正的断点续传单纯靠前端是不够的必须后端也参与进来把分片状态持久化并且处理幂等。所以说白了这个功能不是“配一下参数”就完事而是要理解分片上传的完整链路再自己补上状态管理那一环。2. WebUploader 的断点续传机制分片、状态记录与服务端配合2.1 分片大小怎么定chunked 与 chunkSize 的取舍WebUploader 默认是不分片的。要开启断点续传第一步就是设置chunked: true然后给chunkSize指定合适的值。分片大小没有标准答案我按实际使用场景给一组参考网络环境建议分片大小并发线程数备注隔离局域网千兆2MB ~ 4MB3 ~ 5分片大一点分片数量少服务端压力小普通办公网百兆1MB ~ 2MB3平衡单片时长和断点恢复粒度公网弱网512KB ~ 1MB1 ~ 2分片越小单次失败重传成本越低这里有一个容易被忽略的点分片数量。很多人在内网把分片大小设成 512KB觉得恢复粒度更细但对一个 5GB 的文件来说这意味着 1 万多个分片。每个分片一次 HTTP 请求1 万个请求对服务端的文件句柄、日志、临时目录都是不小的压力而且每片的固定开销请求头、响应头、日志写入加起来非常可观。我的习惯是局域网环境直接用 2MB 到 4MB 分片线程数控制在 3。注意线程数不是越大越好尤其当多个终端同时上传时并发分片请求叠加起来很容易把服务端的连接池和磁盘 IO 打满。2.2 页面刷新后怎么恢复进度服务端幂等是关键WebUploader 自带的能力是“停止后重新 upload()”同一个文件对象在内存里还会保留已经完成的分片会被跳过。但页面一刷新内存状态就没了文件对象需要重新选择所有分片都会重新发一遍。所以真正的断点续传必须依赖服务端已经落盘的分片数据。推荐一个非常实在的做法服务端接收分片时如果发现这个分片文件已经存在直接返回成功不再重复接收。前端刷新页面后重新选择同一个文件其余分片请求到达服务端时会被快速“跳过”只有真正缺失的分片才会重新传输。这样实现的好处是不需要去改 WebUploader 内部的分片跳跃逻辑也不需要费劲去伪造分片成功状态只需保证接口幂等即可。这也是我在内网系统里实测下来最稳的方案。2.3 秒传与校验MD5 到底要不要算WebUploader 有 md5 插件可以对文件分片计算 Hash用来做“秒传”和完整性校验。但在大文件场景里这个功能要慎重——一个 5GB 的文件前端计算完整 MD5 可能要几分钟甚至十几分钟期间用户看到的进度条一直停在“计算 MD5”状态体验非常糟糕。在隔离内网、带宽不是瓶颈的场景下我更建议采用“伪文件指纹 事后全量校验”的组合用“文件大小 最后修改时间 文件名”生成一个业务 ID作为分片目录名。分片全部上传合并完成后在服务端计算一次完整文件的 Hash和前端记录的 Hash 做比对不一致就重传。这样做的好处是上传过程中没有任何长时间等待前端点完选择文件立刻开始分片传输。安全要求更高的环境可以在服务端对关键参数做签名防止分片序号被篡改。具体我再放到后面讲。3. 局域网部署 WebUploader 的完整配置与代码实现3.1 资源全部本地化别留外网依赖隔离内网的第一条铁律就是所有静态资源必须本地化。WebUploader 依赖的 JS、CSS、SWF 文件绝对不能走公网 CDN否则页面在无外网环境里直接白屏。部署时的做法是把整个 webuploader 相关目录拷贝到内网静态资源服务器上在页面里使用相对路径引用。特别注意 SWF 文件如果业务系统还有低版本浏览器需要 Flash 兜底SWF 路径必须写对而且 Flash 权限设置不能要求“访问网络”因为 SWF 和业务页面同域时一般直接授予“同域访问”即可。另外提一句如果内网终端基本都是现代浏览器我建议直接禁用 Flash 降级只走 HTML5 模式。Flash 本身已经停止维护安全风险不低隔离内网里没必要为了极少数古董浏览器承担这个风险。老机器就换个现代浏览器比维护 Flash 模式省心得多。3.2 前端核心配置项逐条拆解下面这份配置是我在隔离内网环境中实际用过的可以作为起点var uploader WebUploader.create({ // 上传地址内网直接用 IP 端口少走一层域名解析 server: http://10.10.x.x:8080/upload, // 本地 SWF 路径若关闭 flash 可省略 swf: ../vendor/webuploader/Uploader.swf, // 选择按钮 pick: #picker, // 开启分片 chunked: true, chunkSize: 2 * 1024 * 1024, // 2MB threads: 3, // 并发 3 个分片 // 文件过滤 accept: { title: 所有文件, extensions: zip,rar,tar,gz,mp4,avi,dat,log, mimeTypes: *.* }, // 单文件最大 10GB fileSizeLimit: 10 * 1024 * 1024 * 1024, // 队列中最多 10 个文件 fileNumLimit: 10, // 允许选择相同文件 duplicate: true, // 关闭图片压缩避免非图片资源被误处理 compress: false, // 附加参数每次分片请求都会带上 formData: { token: getToken(), source: lan-upload }, // 上传字段名服务端用这个字段接收分片文件 fileVal: file, method: POST, // 优先 html5flash 兜底 runtimeOrder: html5,flash });每个配置都有它的目的重点解释几个server建议直接用 IP 端口不要用域名。隔离内网的 DNS 有时候配置不全或者解析超时域名解析失败会直接导致上传请求发不出去。dupicate: true很重要。默认 WebUploader 会拦截文件名和大小完全相同的文件但在断点续传场景下刷新页面后重新选择同一个文件是正常操作不开这个选项会让你根本选不进去。compress: false是给非图片上传场景的。WebUploader 默认带有图片压缩插件如果上传的是大压缩包或视频关闭压缩可以省掉不必要的内存和计算开销。formData里的token每次分片请求都会携带服务端要依靠它做身份校验。后面我还会讲 token 过期的问题。选完文件之后的常用事件处理uploader.on(fileQueued, function(file) { // 文件进入队列做一次服务端分片状态查询 checkChunkStatus(file); }); uploader.on(uploadProgress, function(file, percentage) { // percentage 是 0 到 1 的浮点数更新进度条 updateProgress(file.id, percentage); }); uploader.on(uploadSuccess, function(file, response) { // 服务端合并完成回调里返回最终文件信息 markAsSuccess(file.id, response); }); uploader.on(uploadError, function(file, reason) { // 某一个分片上传失败WebUploader 会自动重试 logError(file.id, reason); }); uploader.on(uploadComplete, function() { // 队列中所有文件处理完成 notifyUploadComplete(); });这里要说明一点uploadProgress的百分比只能反映当前这一轮上传的分片进度不能反映整个文件的真实进度。如果刷新页面后有一部分分片已存在服务端快速返回成功前端看到的现象是进度条从 0 很快跳到很高这是正常的不是 bug。3.3 服务端分片接收与合并的参考实现服务端是整个断点续传方案的核心。我用 Node.js 写一个简化的参考实现重点展示“分片幂等接收”和“按顺序合并”两个关键逻辑// 依赖 formidable 或 busboy 解析 multipart const formidable require(formidable); const fs require(fs); const path require(path); const crypto require(crypto); const UPLOAD_TMP_DIR /data/upload_tmp; function getChunkFile(fileId, chunkIndex) { return path.join(UPLOAD_TMP_DIR, fileId, chunkIndex .part); } // 上传接口POST /upload async function handleUpload(req, res) { const form new formidable.IncomingForm(); // 分片文件都走磁盘临时目录别塞内存 form.uploadDir UPLOAD_TMP_DIR; form.keepExtensions false; const [fields, files] await form.parse(req); const fileId fields.fileId; // 前端生成的伪文件指纹 const chunkIndex Number(fields.chunk); // 当前分片序号 const chunks Number(fields.chunks); // 总分片数 const ext path.extname(fields.name || ); const chunkFile getChunkFile(fileId, chunkIndex); const chunkTmp files.file.path; // 关键如果分片已存在直接返回成功不重复接收 if (fs.existsSync(chunkFile)) { fs.unlinkSync(chunkTmp); // 清理刚上传的临时分片 return res.json({ ok: true, skip: true, index: chunkIndex }); } // 确保分片目录存在 fs.mkdirSync(path.dirname(chunkFile), { recursive: true }); fs.renameSync(chunkTmp, chunkFile); // 检查是否所有分片都齐了 const allReady Array.from({ length: chunks }, (_, i) fs.existsSync(getChunkFile(fileId, i)) ).every(Boolean); if (!allReady) { return res.json({ ok: true, ready: false, index: chunkIndex }); } // 所有分片齐全开始合并 const finalName fileId ext; const finalPath path.join(/data/upload_done, finalName); const writeStream fs.createWriteStream(finalPath); for (let i 0; i chunks; i) { const part getChunkFile(fileId, i); const buf fs.readFileSync(part); writeStream.write(buf); fs.unlinkSync(part); // 合并完成一个分片就删一个 } writeStream.end(() { // 清理空的临时目录 fs.rmdirSync(path.join(UPLOAD_TMP_DIR, fileId), { recursive: true }); // 这里可以再计算最终文件的 MD5/SHA256用于完整性校验 const hash crypto.createHash(sha256); const stream fs.createReadStream(finalPath); stream.on(data, d hash.update(d)); stream.on(end, () { res.json({ ok: true, ready: true, filePath: finalPath, sha256: hash.digest(hex) }); }); }); }这段代码把“分片存在即跳过”和“分片齐全自动合并”放在了一起。实际生产环境里我还会在合并前按分片索引从小到大排序并校验分片文件大小是否都一致、缺失时做记录这里就不展开太多。Java 端的思路是一样的分片文件按{fileId}_{index}.part命名放在固定临时目录。合并时用RandomAccessFile或FileChannel按分片索引直接写入对应偏移最后统一清理临时文件。如果用的是 Tomcat注意maxPostSize或者请求体大小的限制否则分片请求还没到业务代码就被容器拦了。4. 修完代码之后的实测踩坑超时、磁盘、并发与权限4.1 反向代理和 Web 服务器超时前端和服务端代码都写对了不代表上传就能稳定跑。如果请求要走 Nginx 反向代理最容易踩的就是各种默认超时。我遇到过最典型的场景服务端明明接收正常但前端uploadError频繁报错日志里没有任何异常。最后查下来是 Nginx 的proxy_read_timeout只有 60 秒纯内网环境传 2MB 分片按理说很快但某些分片恰好赶上服务端在做全量 Hash 或者别的滞后任务响应就超了。针对大文件上传的 Nginx 配置建议如下# 上传接口所在 location location /upload { # 允许上传任意大小文件如果分片足够小这个值可以保守一点 client_max_body_size 0; # 关闭代理缓冲避免整个分片被 Nginx 暂存后再转发 proxy_request_buffering off; # 拉长读写超时 proxy_read_timeout 300s; proxy_send_timeout 300s; # 关闭 Nginx 对请求体的缓冲直接透传给后端 proxy_pass http://backend_upload; }proxy_request_buffering off这个配置值得强调一下。默认开启时Nginx 会先把整个请求体接收完再转发给后端对于大分片场景会占用大量磁盘空间而且延迟很大。关闭之后请求体会被流式转发内网下性能提升很明显。如果用的不是 Nginx 而是其他网关思路也是一样的关闭缓冲、拉长超时、别限制请求体大小。4.2 临时分片目录爆炸与清理策略分片文件都落盘后临时目录的膨胀速度远超预期。几个终端同时传 10GB 级文件临时目录一夜之间吃掉几百 GB 是常有的事。我踩过一次比较深的坑临时文件写满了磁盘结果所有分片上传全部失败而前端只显示uploadError后台查了半天才发现是磁盘满了。从那以后我做了两件事上传前检查磁盘剩余空间低于阈值直接拒绝新的上传任务并给前端返回明确提示。写一个定时清理任务清理超过 48 小时未完成的分片。逻辑很简单遍历上传临时目录按目录的最近修改时间或元数据记录判断是否过期过期就整目录删除。清理任务还要注意如果一个文件正在上传它的分片目录一直在更新不能被误删。所以最稳妥的方式不是看文件本身的修改时间而是把“最近一次分片活动时间”记录到元数据里比如目录下写一个.meta文件每次分片到达时更新时间戳。4.3 并发上传数与服务端承载的平衡WebUploader 的threads参数控制的是单个上传任务的并发分片数但多个终端同时上传时并发量是叠加的。假设 20 个终端同时各开 5 个线程服务端就要同时扛 100 个分片请求这对文件描述符、临时文件写入、网络吞吐都是考验。我实测下来内网服务器配置一般的情况下建议控制全局的同时上传任务数量。可以简单地在服务端做一个信号量或计数器接收分片请求时如果当前正在处理的文件数超过上限比如 20 个直接返回“系统繁忙请稍后重试”前端拿到这个响应后会自动重试。这样比不打任何控制、让服务端被冲垮要好得多。另外threads设为 3 时的体验已经很好。追求“更快”把它调到 10往往会让服务端磁盘 IO 成为瓶颈整体吞吐量反而不升。4.4 隔离网络下的身份校验与防重放隔离内网不等于可以不鉴权。上传接口如果裸奔任何人只要知道接口地址就能往服务器上扔任意文件这在安全要求高的环境里是绝对不允许的。我的做法是用户登录系统后由服务端签发一个带有效期的上传令牌每次分片请求必须携带。令牌有效期不宜太短。大文件上传可能持续几个小时令牌有效期设 10 分钟就完蛋了。我一般设 8 到 12 小时或者采用签发短期令牌 上传任务绑定企业标识的组合方案。关键参数做签名。前端除了传文件本身还会传fileId、chunk、chunks等业务参数这些参数如果可被篡改分片顺序错乱后合并出的文件就损坏了。简单做法是把这些参数拼上一个密钥做 HMAC服务端验签不通过就拒绝请求。这一步很多人会忽略但隔离内网里更要把好这一关因为一旦出问题整个网络里的数据安全都会受影响。5. 我在内网环境实测后的几个建议文章最后分享几条我在真实隔离内网环境里反复验证过的经验都是直接能用的分片大小选择先用 2MB。如果内网是千兆2MB 分片 3 并发基本是性能和稳定性的平衡点单个 5GB 文件大概 15 到 20 分钟传完且不容易触发任何中间设备超时。如果你的终端还会休眠建议配合系统电源策略保持传输期间不休眠。前端一定保留一个“重新上传”按钮。刷新页面后让用户重新选择同一个文件靠服务端幂等自动跳过已传分片。这个交互对用户来说非常简单直观比做一个复杂的“继续传输”入口省事得多而且更不易出错。上线前用真实文件做一次全流程测试。至少准备一个大文件3GB 以上模拟以下场景上传到一半刷新页面、关闭浏览器重新打开、上传过程中断网重连、并发 5 个终端同时上传大文件。这些场景如果都能顺利恢复或重传再往上加并发压力测试基本就不会出大问题了。服务端日志要带上 fileId 和分片索引。排查问题时如果只有 “上传失败” 没有分片号定位会非常痛苦。我习惯在每个分片请求的日志里输出fileId chunkIndex chunks 文件大小 耗时这样用户反馈问题时后台可以迅速定位到具体是哪个分片、卡在哪个环节。WebUploader 虽然老了但把它用在大文件断点续传这个场景只要理解了分片、幂等、合并和清理这几个关键环节它完全可以做得比许多现代上传组件更稳。隔离内网的网络环境相对可控反而是最适合把断点续传调稳定的地方。希望这篇文章能帮你少走几步弯路。