局域网大文件分块上传实战:用WebUploader实现稳定断点续传
局域网里传个大文件尤其那种几个G的视频、工程包、数据库备份最烦人的不是网速而是传到一半报错、服务端直接拒绝、进度条卡死在99%。你要说用网盘中转内网环境又不一定通外网拿U盘拷又显得太原始。这时候我就想起了那个“老古董”——百度免费上传组件WebUploader。别嫌它老在内网环境做分块上传它可能是最省事、最不用花钱的方案。这篇文章我就把在局域网的坑、原理、具体配置和实测过程完整梳理一遍给准备在自己内网搭上传服务的同学一个能直接抄作业的参考。WebUploader是百度FEX团队开源的一个上传组件核心能力就是切片上传、并发控制、断点续传而且是纯前端实现不依赖后端SDK你只要按约定接收分块然后合并就行。在局域网没有外网依赖的场景下把整个组件包放到本地服务器一次性配置好之后所有内网机器都能通过浏览器访问并使用过程中完全不碰公网。1. 为什么在局域网传大文件需要“分块”而不是直接传整个文件很多人觉得局域网带宽高、延迟低直接把整个文件扔上去不就完了。这个说法在传几十MB的小文件时确实成立一旦文件上到1GB、5GB甚至更大问题就接踵而至而且往往不是带宽的问题。1.1 服务端和网关限制才是真瓶颈局域网也有Web服务器、反向代理、网关。以最常见的Nginx为例默认的client_max_body_size只有1MB你往上传大文件第一个撞到的就是这堵墙浏览器直接收到413错误。就算你改了Nginx配置很多后端框架、中间件还有自己的请求体大小限制比如Java的Tomcat、Node的Express、PHP的post_max_size每一层都可能卡住你。分块上传的本质就是把一个大请求拆成多个小请求。每个分块很小打散到各层限制之下于是Nginx、后端框架都拦不住你了。这是分块最直接也最有价值的作用。1.2 失败重试的成本完全不同整个文件上传时只要网络抖一下、服务器重启一下、浏览器标签页误关全部归零又得从头传。在局域网虽然网络相对稳定但也不是绝对不会断特别是一堆人共用网络、有人疯狂下载东西的时候无线网络下大文件上传中断是常见事。分块之后每个分块独立上传失败只重传那个分块不需要重来。一个1GB的文件切成200个5MB的分块即使最后几个分块失败了重传的代价也只有5MB省时省力这才是分块上传在工程实践上最核心的价值。1.3 内存占用和并发控制传统上传是把整个文件读入内存再提交文件一大浏览器卡死服务端内存也吃紧。分块上传可以按块读取前端内存开销小得多服务端接完一个块就落盘、释放内存几十个人同时传大文件也扛得住。所以分块不是炫技它是工程上绕不开的诉求。WebUploader把这块做了完整封装我们直接用就行。2. 选型思考为什么用百度这套老组件而不是自己写或换新框架现在前端上传组件不少像vue-simple-uploader、uppy、plupload等等个顶个的现代功能也强。但我在局域网场景下反而更倾向于WebUploader原因是基于真实环境里踩过的坑。2.1 内网环境的特殊性决定了选型逻辑局域网项目比如企业OA、内部资料系统、医院影像系统、学校教务平台有一个共同点技术栈陈旧、浏览器环境复杂、还要能离线部署。很多内网机器还停留在Win7的IE11、老版本Chrome甚至部分窗口终端用的是国产浏览器兼容模式。新框架基本抛弃了这些环境而WebUploader基于HTML5和Flash双轨支持兼容性极好Flash作为一种兜底方案在老环境里依然能工作。当然Flash已经全面淘汰我在实际部署时绝大多数场景还是走HTML5通道。WebUploader打包后的文件全部是本地静态资源没有任何外部CDN依赖完全满足内网离线部署要求。2.2 组件维护与否不影响内网使用确实WebUploader官方已经很多年没更新了GitHub仓库基本处于冻结状态。但一个成熟组件的价值在于它解决了90%的通用问题剩下的10%你来补就行。在局域网这种相对封闭的环境里不出外网、不升级浏览器、不变更业务需求一个稳定的老版本反而更安全。你不用担心它突然因为某个依赖更新而崩掉。这就像生产环境用老内核没人敢随便升级一个道理。2.3 什么情况下建议换现代方案也不是说WebUploader就是万能的。如果你要传超大文件几十GB以上、要做秒传需要计算文件MD5、要支持文件夹上传这些WebUploader做起来比较吃力。另外如果你的项目是从零开始且浏览器环境统一是Chrome内核那直接用原生File.slice()加并发请求或者用uppy会更舒服。但如果你要的是“快速、免费、能跑、不折腾”WebUploader是当下最合适的选择。3. WebUploader分块上传的核心机制拆解把WebUploader在连接上时的完整分块链路理清楚才能正确配置后端接口。它不是什么黑魔法核心就三件事切块、传块、合并。3.1 分块参数是怎么计算的WebUploader在HTML5模式下使用File.slice()对文件进行切片。关键配置就这几个var uploader WebUploader.create({ swf: /static/Uploader.swf, server: /api/upload, chunked: true, chunkSize: 5 * 1024 * 1024, // 5MB concurrency: 3, threads: 3 });chunked: true表示开启分块上传。chunkSize是分块大小我建议局域网内部用5MB。concurrency和threads是并发上传的线程数局域网内建议2~3太大会把服务器带宽挤爆。关于分块大小的选择需要简单算一下。假设你们内网环境是100Mbps以太网理论下行12.5MB/s上行通常低于下行大概3~6MB/s。一个5MB的分块传完需要1秒左右即使并发3个也不会给网络造成太大压力。如果分块太小比如1MB请求数暴增5倍后端要处理的连接数、临时文件数更多反而拖慢速度。如果分块太大比如50MB又回到了整传的弊端。所以综合权衡5MB是通用好用的值。3.2 前端到后端的字段约定WebUploader每次上传一个分块时POST请求里除了文件本身还会带上分块元信息。默认字段大致如下字段名含义示例file分块文件内容二进制chunk当前分块索引0, 1, 2...chunks总分块数12name原始文件名设计稿最终版.zipguid当前文件唯一标识需要在before-send-file回调中自定义默认是文件名称等后端必须拿到chunk和chunks前者告诉后端“这是第几块”后者告诉后端“总共多少块”。合并的时候才能保证顺序正确。3.3 断点续传到底要不要做严格意义上讲WebUploader本身没有内置“断点续传”的完整实现它只提供了chunked机制。断点续传的完整链路是本地记录已上传的分块重新初始化时跳过已传分块。WebUploader的实现思路有两种简单方案前端把已上传分块索引存在localStorage里下次进入页面时读取调用后端的“查询分块状态”接口然后把未传的分块继续传。后端方案后端保存分块时检查是否已有同名同索引的分块有就直接跳过。前端无需关心状态只需要重新发起所有分块后端自动跳过。在局域网场景下我强烈建议用第二种。因为局域网里用户不多文件五花八门在后端做幂等检查是最可靠的前端代码也简单只需要在before-send-file里生成一个guid作为文件唯一标识后端按guid chunk命名分块文件即可。这样即使断了、刷新了、换电脑了同一用户只要guid不变重新上传时后端会跳过已经接收的分块进度直接从断点继续。3.4 合并分块时的顺序与完整性校验所有分块传完后前端会请求server地址并带上参数md5之类的动作标识WebUploader的默认行为是最后一个分块上传完成后自动再往后端发一次请求告知“可以合并了”。这个“合并通知”和后端合并动作要配套前端是这样设置uploader.on(uploadAccept, function(file, response) { // response是后端返回的JSON if (response.status success) { // 全部完成 return true; } return false; });后端收到所有分块后按chunk索引从小到大读取二进制内容依次写入一个完整文件。合并时必须用流式写入不要一次性把所有分块读入内存再合并否则在服务器上内存直接爆炸。合并完成后再校验文件大小如果分块大小偏差过大说明有分块损坏直接报错重传。4. 局域网环境下的完整实操前端配置 后端接口 网络适配理论讲完现在进入实战。以下是一套我验证过的完整实现包含前端页面、Node.js后端和局域网访问配置。4.1 前端页面配置从引入组件到跑通第一个分块首先把WebUploader的静态文件放到项目的public/uploader目录下。核心文件包括webuploader.js、webuploader.css、Uploader.swf。页面里引入后初始化一个上传实例!DOCTYPE html html head meta charsetUTF-8 title局域网大文件上传工具/title link relstylesheet href/static/uploader/webuploader.css script src/static/uploader/webuploader.js/script /head body div iduploader div idthelist classuploader-list/div div idpicker选择文件/div button idctlBtn classbtn btn-default开始上传/button /div script var uploader WebUploader.create({ swf: /static/uploader/Uploader.swf, server: /api/upload/chunk, pick: #picker, accept: { title: All Files, extensions: zip,rar,7z,tar,gz,mp4,mov,avi,iso,db,bak,sql,json,doc,docx,xls,xlsx,pdf }, auto: false, chunked: true, chunkSize: 5 * 1024 * 1024, concurrency: 3, threads: 3, duplicate: true }); uploader.on(fileQueued, function(file) { // 生成文件唯一标识用于后续断点续传 file.guid Date.now().toString(36) _ file.name; }); uploader.on(uploadProgress, function(file, percentage) { console.log(file.name 进度: Math.round(percentage * 100) %); }); uploader.on(uploadSuccess, function(file, response) { if (response.status ! success) { alert(上传失败 response.message); } }); uploader.on(uploadError, function(file, reason) { console.error(file.name 上传出错: reason); }); $(#ctlBtn).on(click, function() { uploader.upload(); }); /script /body /html这里有几个容易被忽视的点单独说一下。如果页面需要重复选择同一个文件必须加duplicate: true否则文件名相同的文件会被WebUploader自动去重第二次选择同一个文件没反应。pick的按钮是触发文件选择的不设置这个入口页面无法打开文件选择框。server地址必须是后端真实接口且后端需要支持跨域如果前端页面和后端不在同一个域但局域网内通常把页面和后端部署在同一台服务器上不存在跨域问题。4.2 后端接口设计用Node.js实现分块接收与合并后端我拿Node.js Express multer来演示因为这个组合轻量、易读。如果你用的是Java、Go、PHP核心逻辑是一样的接收文件流、按索引落盘、全部就绪后合并。先初始化项目mkdir upload-server cd upload-server npm init -y npm install express multer创建server.jsconst express require(express); const multer require(multer); const fs require(fs); const path require(path); const app express(); const PORT 8080; // 分块临时目录和合并完成目录 const CHUNK_DIR path.join(__dirname, chunks); const FILE_DIR path.join(__dirname, files); if (!fs.existsSync(CHUNK_DIR)) fs.mkdirSync(CHUNK_DIR, { recursive: true }); if (!fs.existsSync(FILE_DIR)) fs.mkdirSync(FILE_DIR, { recursive: true }); // multer配置保存时临时文件名我们不关心去重、识别后面做 const storage multer.diskStorage({ destination: function(req, file, cb) { cb(null, CHUNK_DIR); }, filename: function(req, file, cb) { // 临时文件名guid chunk索引 const guid req.body.guid || default; const chunk req.body.chunk || 0; cb(null, ${guid}_${chunk}); } }); const upload multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 } }); app.post(/api/upload/chunk, upload.single(file), (req, res) { const { guid, chunk, chunks, name } req.body; if (!guid || chunk undefined || chunks undefined || !name) { return res.status(400).json({ status: error, message: 分块参数不完整 }); } // 收到分块校验大小是否符合预期可选 const savedPath path.join(CHUNK_DIR, ${guid}_${chunk}); const targetSize Number(req.body.chunkSize) || 5 * 1024 * 1024; if (req.file.size targetSize 1024) { fs.unlinkSync(savedPath); return res.status(400).json({ status: error, message: 分块大小异常 }); } // 合并判断检查是否所有分块已到位 const allReceived []; for (let i 0; i Number(chunks); i) { allReceived.push(fs.existsSync(path.join(CHUNK_DIR, ${guid}_${i}))); } const complete allReceived.every(Boolean); if (complete) { // 所有分块就绪执行合并 mergeChunks(guid, name) .then(() { res.json({ status: success, message: 上传完成 }); }) .catch(err { res.status(500).json({ status: error, message: err.message }); }); } else { res.json({ status: running, message: 分块上传中, received: allReceived.filter(Boolean).length }); } }); function mergeChunks(guid, name) { return new Promise((resolve, reject) { const targetPath path.join(FILE_DIR, ${Date.now()}_${name}); const output fs.createWriteStream(targetPath); // 按索引顺序读取分块并合并用流式避免内存爆掉 let index 0; function appendNext() { const chunkPath path.join(CHUNK_DIR, ${guid}_${index}); if (!fs.existsSync(chunkPath)) { output.end(); // 清理分块文件 for (let i 0; i index; i) { fs.unlink(path.join(CHUNK_DIR, ${guid}_${i}), () {}); } resolve(); return; } const stream fs.createReadStream(chunkPath); stream.pipe(output, { end: false }); stream.on(end, () { index; appendNext(); }); stream.on(error, reject); } appendNext(); }); } app.use(express.static(path.join(__dirname, public))); app.listen(PORT, 0.0.0.0, () { console.log(Upload server running at http://0.0.0.0:${PORT}); });整个过程的核心逻辑就在mergeChunks里所有分块到齐后从第0块开始按顺序轮流读管道式的写入目标文件全部写完就清理分块临时文件。实测下来多个分块并发到达和多线程顺序读取都没有问题文件完整性有保障。4.3 局域网网络适配IP直连、端口放行和浏览器配置前端和后端都搭好后局域网访问还有三道坎要过一是监听地址。后端服务不能只监听127.0.0.1必须监听0.0.0.0否则其他机器访问不到。如果用了Windows系统还需要在防火墙里放行对应端口比如8080。二是通过IP访问。服务启动后其他机器浏览器里访问http://服务器IP:8080。建议把前端页面和后端服务做成同一个端口省去跨域麻烦。如果你的服务器经常换IP或者想让内网用户更好记可以在Windows的hosts文件或路由器DHCP里绑定固定的IP映射。三是代理与缓存问题。如果局域网里某些机器设置了系统代理例如某个部门统一配置了上网代理浏览器访问内网IP时代理会拦截请求导致上传失败。这种情况要设置“不使用代理访问”的内网地址段或者在浏览器代理设置里把内网IP加入例外列表。另外上传接口的响应头建议加上Cache-Control: no-store避免某些浏览器误缓存POST请求的响应造成进度状态串号。5. 局域网调试过程中的高频问题与避坑实录把我在真实环境中遇到的问题整理一下每一条都踩过坑。有些问题可能短期不出现但在某个用户、某台机器上突然爆发排查思路提前掌握能省很多时间。5.1 上传到一半报错进度条卡住不动出现这类问题先别怀疑代码排查顺序应该是网络 - 服务端日志 - 分块状态。局域网里最容易被忽略的是交换机或路由器对连接数的限制。如果后端concurrency设置过高比如8大量并发分块连接可能导致设备性能瓶颈出现部分连接被重置、超时。把concurrency降到2或3同时把chunkSize调大一点比如10MB连接数减少一半症状会明显缓解。另一个原因是服务器的临时目录满了。分块是逐个落盘的一个5GB的文件会产生成百上千个分块临时文件默认在系统临时目录或项目目录下磁盘空间不足时写入失败上传就中断。建议单独划定一个大分区存放分块临时文件并写个定时任务清理超过24小时未合并的残留分块。5.2 413 Request Entity Too Large 错误这个问题大概率出在反向代理上。你改了后端限制但Nginx还卡着。需要在Nginx配置里加上client_max_body_size 10m;注意这里的10m指的是单个分块的大小不是整个文件。分块后单个请求最大就是分块大小所以设置为10MB即可。如果你没走Nginx而是直连Node服务那检查Express的body-parser或multer的limits配置。5.3 分块合并后的文件损坏或合并后文件名乱码合并出的文件损坏基本是两种原因一是分块顺序错乱合并时按字符串排序而不是按数字索引排序导致chunk_10排在了chunk_2前面。二是某些分块上传失败但后端没检测到少了一块合并时文件缺失但流式写入不会报错导致文件少了内容。解决方案很简单合并前检查每个分块是否存在并且大小一致。文件名的处理建议统一用encodeURIComponent在前端编码后端解码存储避免中文名在跨平台传输时出现乱码。5.4 局域网内某些电脑打开页面白屏或无法上传白屏优先检查浏览器兼容模式。有些国产浏览器默认用兼容模式渲染WebUploader的HTML5功能在这个模式下会失效。建议在页面头部强制指定内核meta namerenderer contentwebkit meta http-equivX-UA-Compatible contentIEedge,chrome1第二行代码的作用是让IE内核优先使用最高版本模式避免WebUploader在IE9以下环境里的HTML5接口不可用。5.5 上传过程中其他电脑无法访问服务器或网络卡顿局域网内多人同时上传网络会被打满。简单算一下如果10个人同时上传每人并发3个分块每个分块5MB估算总瞬时流量约150MB/s1000Mbps的内网也会被打满。这是很多局域网项目忽略的容量规划问题。解决办法有两个方向一是限制上传速度WebUploader支持uploader.option(fileVal)和formData配置但单文件限速能力较弱想限速可以在Nginx层配置limit_rate按IP限速。二是限制并发数全局并发不要超过5否则后端压力大、网络也扛不住。我的建议是并发设3、分块大小5MB这个组合在绝大多数办公网环境下都能稳定跑完。5.6 WebUploader在小程序端点击无反应的问题在前面的热搜词里看到了“uview 上传组件u-upload,在小程序端点击无反应”这个问题和WebUploader在小程序端是两条线。uview是uni-app生态的组件小程序端文件选择逻辑和Web完全不同WebUploader本身只适配Web浏览器不适合小程序。如果你要在小程序里做分块上传需要基于wx.uploadFile自己封装或者使用uni-app的分块上传API。这里提一句避免有人把这两个概念混在一起。6. 局域网分块上传的性能调优思路到底要不要做秒传和文件校验有人问既然都用了WebUploader能不能再进一步把“秒传”也做出来。秒传的技术原理是前端计算文件MD5把MD5传给后端后端检查这个MD5是否已有相同文件有就直接返回“上传完成”否则再走分块逻辑。听起来很美但局域网里做秒传性价比不高。首先是成本问题。要计算MD5前端需要把整个文件读一遍。一个5GB的文件前端计算MD5可能要花几分钟期间CPU占用高浏览器可能卡顿。而局域网内上传一个5GB文件按照10MB/s的速度算也就8分多钟。花几分钟算MD5只为了“可能”省8分钟还不一定命中重复文件不太划算。其次是重复文件的概率。企业内部通常不会频繁上传同一个大文件秒传的价值远不如断点续传。所以我在局域网项目中一般不做秒传只做断点续传。但如果确实需要校验文件完整性可以这么做分段计算MD5把每个分块的MD5传给后端记录合并时再用同样的分段方式计算合并后文件的MD5前后对比一致则完成不一致则定位到具体分块重传。这个方案兼顾了完整性和性能但实现复杂度较高非必要不推荐。7. 从局域网扩展到更大范围的思路内网穿透之外的选择整个过程都是在纯局域网内进行完全不依赖外网。但有一个常见场景是办公网和服务器不在同一网段或者服务器在机房员工在办公区。这时候访问方式就不再只是IP直连还涉及路由、VLAN、DNS等网络层面的打通。方案一在路由层打通把服务器所在网段和办公网段通过三层交换机路由打通员工直接访问服务器IP。重点是要确保端口不被防火墙拦截另外为了避免IP变化导致前端配置失效最好给服务器设置静态IP。方案二用DNS内网解析如果你的内网有DNS服务给服务器配置一个内网域名比如upload.local员工访问域名即可不用记IP。这个方案在服务器IP变动时尤其有用只要改DNS记录就行前端页面和用户都不用改。方案三如果你只是想临时给外部人员传文件那属于互联网场景不在本文讨论范围内。纯内网环境不建议引入任何出网的通道这既涉及信息安全合规也没必要把简单问题复杂化。把基础网络搞通之后WebUploader这套方案自然就跟着生效了。它不关心你是同一个局域网还是跨网段互通只要HTTP请求能到达服务器分块上传的机制都一样。我个人在实际部署中的体会是WebUploader虽然老但它把分块上传最核心的机制封装得很干净后端你唯一要做的事就是接收分块、按序合并这反而是它的优势。相比一些“全家桶”上传组件它简单到一眼能看穿出问题了好排查这在内网环境里太重要了。最后再分享一个小技巧把分块临时目录和合并后文件的目录分开放合并完成后临时目录实时清理这样哪怕并发上传几十个文件磁盘也不会被撑爆。这套方案我已经稳定跑了一年多还在继续服役你可以放心用。