☰
内网大文件断点续传实战:WebUploader分片上传与前后端协作
2026/10/18 1:42:42 网站建设 项目流程

上个月有个朋友找我调一个上传问题,他们单位的网络是完全隔离的,任何外网资源都访问不了,业务系统里经常要传几个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', // 优先 html5,flash 兜底 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 虽然老了,但把它用在大文件断点续传这个场景,只要理解了分片、幂等、合并和清理这几个关键环节,它完全可以做得比许多现代上传组件更稳。隔离内网的网络环境相对可控,反而是最适合把断点续传调稳定的地方。希望这篇文章能帮你少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询