拓冰建站拓冰建站
首页 / 资讯中心 / 正文

分片上传与秒传实战:基于HTML5 File API的PHP大文件方案

1. 理赔勘查视频为什么让常规上传方案当场翻车1.1 单文件体积超出想象从手机拍摄参数反推先说说我为什么盯上这个场景。我在保险科技公司做过核赔系统的后端最头疼的不是核赔规则也不是保单接口而是查勘员手机里那段该死的视频。查勘员出险后去现场围着车拍一圈再拍点发动机、底盘、刹车痕迹随手就是十几分钟甚至半小时的视频。现在手机默认都开1080P甚至4K1080P的码率按8Mbps算一小时大概是3.6GB半小时就是1.8GB。要是查勘员忘了分段录制一条视频轻松奔着2GB往上走。这种文件拿去传服务器谁做谁知道痛。传统网页上传就是一个input typefile加上表单提交PHP那套$_FILES一把梭。但真拿2GB的文件这么干大概率会碰到三个问题一是HTTP连接维持不了那么久中间任意一次网络抖动就断开二是Nginx、PHP-FPM对请求体大小和执行时间都有默认限制文件稍微大点直接被拒三是即便服务器勉强收完中间断了一次对不起前面传的几GB全部作废从头再来。理赔这种场景比普通文件传输更苛刻。查勘员在事故现场拍的视频属于证据材料保险公司要拿它做核赔依据上传失败一次意味着定损周期拉长一天客户投诉就多一封。所以这不是“大文件上传”那种通用需求而是“极不稳定网络环境下超大视频必须尽可能一次成功”的强需求。1.2 弱网与断线场景下的“全量重传”死局查勘员出现场的网络环境根本不归你管。地下车库、郊区道路、山区信号死角4G经常掉到一格偶尔还能切回3G。高端手机还能撑一会Wi-Fi但稳定性也就是那么回事。在这种网络上一个2GB的请求体一次性POST过去成功率无限趋近于零。我印象很深的一次是某合作修理厂的查勘员在客户地下室车库拍了一条2.4GB的视频用手机流量传了一个多小时传到了87%停车场卷帘门一关4G切成了3G连接断裂直接回到0%。他那天的表情我现在都记得。后来我们统计过传统上传方式在弱网环境下的大文件成功率只有百分之十几这还是在加了各种超时补偿之后的数字。“全量重传”这种设计在小型文件上无所谓最多浪费几秒钟但上了GB级别就变成一种灾难。解决办法只有一个方向把大象切碎了过河每次只搬一小块搬失败的成本极小搬完的块不会被丢掉。1.3 分片上传解决的三个核心问题分片上传本质上是在说三件事断点续传、并发加速、失败隔离。先看断点续传。大文件切成一堆几十KB到几十MB的分片每个分片独立请求、独立确认。传输过程中哪一片断了只重传这一片已经成功上传的分片继续保留。上传任务可以被中断也可以从任何时间点恢复不需要重头再来。再看并发加速。网络传输有个特点单条TCP连接不一定能跑满带宽特别是在有丢包和延迟的环境下一条连接往往会受限。把文件切成多个分片用3到6个并发连接同时传能在同等网络条件下把利用率提上去。尤其对4G弱网并发分片比单连接稳定得多。最后是失败隔离。如果整个文件是一次性请求任何一秒的网络抖动都可能毁掉整个任务。但分片之后某一两片失败只影响那几MB的数据重试成本低到可以忽略。分片上传的容错能力就是做保险行业这种恶劣场景上传系统的地基。2. HTML5 File API 分片的前端设计切多大、并发几个、怎么续传2.1 分片大小不是凭感觉定的前端负责用HTML5的File对象把视频切成小块这是整个方案的起点。但很多初学者上来就问分片切多大合适这个不能拍脑袋。分片太小比如256KB1GB文件要切成4096片每片都发一个HTTP请求请求头和响应头的开销占比急剧上升服务器的并发压力也大。分片太大比如64MB一片虽然请求数少了但弱网下单片传输时间长中途断掉重传代价变大。根据我们的实践一般建议在4MB到16MB之间选。这里有几层考量单片大小要小于PHP的upload_max_filesize和Nginx的client_max_body_size否则会被拦截。单片传输时间尽量控制在3到10秒以内方便做失败重试。分片总数尽量控制在200到1000片之间既不会让服务端文件数爆炸也不会让合并逻辑慢到不可接受。我自己一般选8MB也就是8 * 1024 * 1024字节。1080P半小时的视频约1.8GB切成230片左右请求数合理单片几秒传完重试成本也低。如果你主要跑在信号极差、上行带宽只有1Mbps甚至更低的环境建议降到4MB否则一片要传一分钟失败概率会高很多。2.2 文件切片与并发上传的实现框架HTML5的File对象带一个slice()方法用法和数组的slice类似指定起始字节和结束字节返回一个Blob对象。这个Blob可以直接放进FormData发给服务器。这就是“分片”的底层能力不需要装任何第三方库。一个可用的前端结构大概是这样的const CHUNK_SIZE 8 * 1024 * 1024; // 8MB一片 function createChunks(file) { const chunks []; let start 0; while (start file.size) { const end Math.min(start CHUNK_SIZE, file.size); chunks.push(file.slice(start, end)); start end; } return chunks; }切好片之后要做的事是并发上传。同时开几个XMLHttpRequest或fetch把每一片的数据POST到后端。注意不要一下子把所有分片全部发出去那样网络会先拥塞然后集体失败。稳妥的做法是控制并发数比如同时最多跑3到5个。我用一个简单的计数器来控制并发async function uploadChunk(chunk, fileMd5, chunkIndex, totalChunks) { const formData new FormData(); formData.append(file, chunk, video.mp4); formData.append(fileMd5, fileMd5); formData.append(chunkIndex, chunkIndex); formData.append(totalChunks, totalChunks); const res await fetch(/api/upload/chunk, { method: POST, body: formData }); return res.json(); } async function uploadWithConcurrency(chunks, fileMd5, concurrency 4) { let index 0; const workers Array.from({ length: concurrency }, async () { while (index chunks.length) { const current index; try { await uploadChunk(chunks[current], fileMd5, current, chunks.length); } catch (e) { // 记录失败分片稍后重试 failedIndices.add(current); } } }); await Promise.all(workers); }实际项目里还要加进度汇总。进度条不能直接用某一个分片的onprogress来算那会跳来跳去。推荐的做法是把“已成功上传的分片数量”除以“总分片数量”作为总体进度再乘以100%。这个口径不依赖单片的瞬时速度用户看着心里踏实。2.3 断点续传为什么不能只依赖 localStorage前端做断点续传最简单的方式是每成功一片就把这个分片索引存到localStorage里刷新页面后读取跳过已传的索引。这个思路在小文件场景下够用但在理赔勘查这种长任务里有个致命问题——本地存储只属于当前浏览器、当前设备。查勘员手机浏览器清理一次缓存或者换一台手机登录本地的断点记录就全没了。保险公司的内勤和查勘员不可能保证永远同一台设备。所以真正可靠的断点续传必须以服务端为准前端上传前先问一下后端“这个文件的哪些分片已经传过了”后端查临时目录或数据库返回一个索引列表前端只传输缺失的片。这个查询接口在上传任务开始前调用一次即可也可以在用户手动点击“继续上传”时调用。前端本地记录的定位是“减少一次服务端查询的辅助缓存”而不是唯一依据。谁把断点状态完全放在前端谁早晚要出事故。3. PHP后端接收分片请求接口、临时文件与最终合并的实现3.1 分片上传接口的基本形态与校验策略后端的核心工作就两件收分片、合并分片。我用PHP来实现这里的思路与语言无关Java、Go、Node都能平移。先定义分片上传的接口约定。前端每次POST包含四个关键字段字段含义示例fileMd5整个文件的MD5摘要用来标识同一次上传任务a3f2b1...chunkIndex当前分片的序号从0开始45totalChunks全部分片数量230file分片二进制内容video.mp4.part45PHP接口接收到请求后第一步不是急着写文件而是做合法性校验$fileMd5 $_POST[fileMd5] ?? ; $chunkIndex (int)($_POST[chunkIndex] ?? -1); $totalChunks (int)($_POST[totalChunks] ?? 0); if ( !preg_match(/^[a-f0-9]{32}$/, $fileMd5) || $chunkIndex 0 || $totalChunks 0 || $chunkIndex $totalChunks ) { http_response_code(400); echo json_encode([code 400, message invalid params]); exit; }这里要注意两点。第一点是fileMd5必须做格式校验只能允许32位十六进制字符不能图省事直接拿它拼路径否则等于给路径注入开了一扇门。第二点是必须校验chunkIndex和totalChunks的数值关系防止有人上传一个超出范围的索引把合并逻辑打乱。分片文件本身要放到临时目录。我的做法是按fileMd5建一个子目录所有分片都放进这个目录。为什么用MD5做目录名因为不同用户可能同名文件同时上传如果直接拼文件名会互相覆盖。而MD5本身是文件内容的摘要内容相同则目录相同天然做了聚合。$chunkDir /data/upload_tmp/ . $fileMd5 . /; if (!is_dir($chunkDir)) { mkdir($chunkDir, 0755, true); } $chunkPath $chunkDir . $chunkIndex . .part; move_uploaded_file($_FILES[file][tmp_name], $chunkPath);这里用的move_uploaded_file是PHP处理上传文件的标准函数比copy或rename更安全它会校验文件是否真的来自HTTP POST。分片落盘之后可以顺手记一个Redis标记把当前分片索引标记为已完成$redis-sAdd(upload: . $fileMd5, $chunkIndex);这个标记在断点续传查询时非常关键后端能直接在Redis里查出“哪些分片已经传过”然后返回前端。3.2 合并大文件必须用流式读写别让内存爆炸当所有分片都传完之后前端会调用一个“合并”接口或者后端在检测到最后一个分片到位时自动触发合并。合并的逻辑是把0.part、1.part、2.part……按顺序拼接成一个完整的视频文件。新手最容易犯的错误是用file_get_contents把分片整个读进内存再拼起来// 错误示范文件一大内存立刻爆掉 for ($i 0; $i $totalChunks; $i) { $content file_get_contents($chunkDir . $i . .part); file_put_contents($finalPath, $content, FILE_APPEND); }这个写法在分片是4MB、总共几百片时也会出问题。PHP默认内存限制通常只有128MB一片8MB你倒是没超但多片同时缓存到内部缓冲里分分钟超限。2GB的文件合并到一半FPM直接宕掉。正确的做法是流式合并。用fopen打开目标文件然后循环用fread读源分片、用fwrite写入目标文件每次只读一小块比如2MB内存占用保持恒定$target fopen($finalPath, wb); for ($i 0; $i $totalChunks; $i) { $partPath $chunkDir . $i . .part; if (!file_exists($partPath)) { throw new RuntimeException(missing chunk: . $i); } $source fopen($partPath, rb); while (!feof($source)) { $buffer fread($source, 2 * 1024 * 1024); // 每次2MB fwrite($target, $buffer); } fclose($source); unlink($partPath); // 合并完一片就删一片省磁盘 } fclose($target);合并完成后还要做一次完整性校验。最简单的方式是计算合并后的文件MD5与前端上传时传的fileMd5对比。如果不一致说明中间有分片丢失或者损坏要触发整个上传任务重新走一轮。这个过程的时间开销要看文件大小2GB的MD5计算大约需要几秒到十几秒在可接受范围内。3.3 为什么需要异步合并以及常见的队列化方案合并2GB的文件不是瞬间完成的事。即便用流式读写整个过程也可能需要20秒甚至更久。如果合并逻辑跑在PHP-FPM的请求周期里前端那个“合并请求”会一直挂着等20多秒。问题来了FPM的max_execution_time默认为30秒nginx的fastcgi_read_timeout默认60秒超了任何一个连接就被掐断。所以生产环境里最好不要同步合并。我的做法是当最后一个分片到达后后端只做一件事——把“需要合并”的消息丢进Redis队列然后立即返回响应“分片已全部接收正在合成”。真正耗时的合并逻辑交给后台CLI脚本处理// 接口层只负责通知 $redis-lpush(video_merge_queue, json_encode([ fileMd5 $fileMd5, totalChunks $totalChunks, finalPath $finalPath, originalName $originalName ])); echo json_encode([code 200, message creating...]);后台脚本用PHP CLI写一个常驻消费者从队列里取出任务执行合并写入数据库记录。前端这时候可以通过轮询或WebSocket查询合并状态一旦发现“合并完成”就跳转到视频预览页。这个异步化改造是我踩坑之后才做的。一开始图省事直接同步合并上线第一周就收到“某些视频传完但一直不出现”的投诉查下来全是FPM超时把合并进程杀掉了。切到异步队列之后再也没出过这类问题。4. 秒传机制没那么玄指纹、状态接口与重传策略4.1 秒传的三种实现路径“秒传”这个词在分片上传语境下容易被人误读。它其实有三层含义做的深度完全不一样。第一层是文件级秒传。服务端已经存在同内容的文件前端还没有真正上传数据后端检测到MD5相同直接返回上传成功进度条瞬间到100%。这个设计最彻底但前提是内容完全相同。这里特别说一下MD5风险MD5在安全场景里不算强校验但在文件去重这种“非对抗、只防误判”的场景下碰撞概率可以接受。如果你特别较真可以换成SHA-256代价是计算开销更大。第二层是分片级秒传。整个文件的对端不存在但某几个分片之前传过——可能是上次中断留下的也可能另一条完全相同的素材的中间几片。后端通过状态查询接口返回“已有的分片列表”前端只传缺失分片效率远超全量重传。第三层是体验层。不追求真正做到“前面有缓存”而是让用户从点击上传到看到进度条走动的时间尽量短。比如一个大文件切完片后前端立刻发出上一条请求同时后端立刻确认并返回“继续”用户几乎没有等待感这也能被感知成“秒传”。4.2 大文件指纹全量哈希的代价与折中方案既然要判断“文件是否已存在”最严谨的办法是计算整个文件的MD5。前端用spark-md5这类库对文件做全量读取计算但那种方式要把整个文件从头到尾读一遍。2GB文件读一遍加计算便宜的电脑也要十几秒贵的手机也要近十秒。更尴尬的是这个计算期间用户看着进度条卡在0%他的第一反应是“上传坏了”。体验并不好。所以在保险理赔这种大视频场景我推荐一个折中方案先按分片上传每片都有独立的MD5服务端在合并结束后计算整个文件MD5。这样前端的“秒传判断”只在文件已经存在且服务端已有记录时才会发生而大部分情况是用户登录后系统主动检查“历史上是否有同一个理赔案件上传过相同视频”。有则走秒传没有则直接进分片上传流程不等前端算全量哈希。前端如果确实想做文件级秒传也可以采用抽样哈希法选取文件开头、中间、结尾几段各几MB计算拼接后的摘要再加文件名和大小作为辅助判定。这个方案计算量极小命中率在绝大多数场景够用。比如async function sampleHash(file) { const sampleSize 4 * 1024 * 1024; const parts []; // 开头4MB parts.push(file.slice(0, sampleSize)); // 中间4MB const mid Math.floor(file.size / 2); parts.push(file.slice(mid, mid sampleSize)); // 结尾4MB parts.push(file.slice(file.size - sampleSize, file.size)); // 计算拼接后的MD5再带上文件大小 ... }抽样哈希不是绝对精确可能在极端情况下漏判或误判所以它只能作为“秒传前置判断”真正做完整性校验还得靠服务端合并后的全量校验。4.3 从进度条角度设计“秒传”体验理赔用户不是程序员他们不看技术指标只看“这东西到底传没传上去”。所以我特别在意进度条的呈现口径。第一进度条永远不能回退。哪怕某一片上传失败重试前端已经渲染的进度也不该倒回去。显示卡住几秒可以接受回退会让人立刻怀疑系统坏了。第二速度要平滑。网络忽快忽慢时真实瞬时速度可能在5MB/s和500KB/s之间反复横跳。直接显示瞬时速度用户会觉得系统抽风。我的做法是记录最近10秒内成功上传的总字节数除以10得到“滑动平均速度”这样曲线平滑得多。第三提高“开始反馈”的速度。用户点完上传至少要在一两秒内看到进度条被激活哪怕是0.1%。如果前端必须等全量MD5算完这个反馈会延迟很多。所以我把“秒传判断”放在“切片完成之后”而不是“切片之前”切完片立刻发送第一个分片并显示进度条同时后台做抽样哈希秒传判断。绝大多数用户的感知就是“点完按钮立刻看到进度在走”。这个细节没人给你写进需求文档但它是用户评价“你们平台好不好用”的一个隐藏指标。5. 从Demo到生产Nginx、PHP配置、临时目录与安全的那些坑5.1 PHP/FPM/Nginx 的关键配置项对照表分片上传落地的过程中配置层面的坑比逻辑层面的坑多得多。逻辑你还能靠调试慢慢捋配置错了就是线上事故而且经常是“为什么我本地能跑服务器不行”。以8MB分片为例关键配置如下配置项默认值推荐值说明client_max_body_sizenginx1M16m必须大于单片大小否则分片POST会被nginx直接拒绝upload_max_filesizephp.ini2M16m必须大于单片大小post_max_sizephp.ini8M20m必须大于单片大小加上表单字段的开销max_execution_timephp.ini3060只对单次分片接收有意义合并交给CLI不用设太大max_file_uploadsphp.ini2020单片请求只传一个文件保持默认即可client_max_body_size是所有人第一眼都会踩的坑。默认只有1MB你传8MB的分片nginx直接返回413。upload_max_filesize同理。我把推荐值定为16m/20m是为了给“表单字段长度”和“未来调大分片”留余量。这里多说一句不要为了省事把post_max_size设成500M或1G那样等于回到了“大请求一把梭”的老路nginx和PHP的防护形同虚设。5.2 多实例与共享存储分片临时目录的地狱坑如果你们的后端只有一台服务器临时目录随便放没问题。但保险科技系统的架构一般不会那么寒酸至少两台起前面挂负载均衡。这时候就出现了一个经典问题用户先通过服务器A上传了第0到第50片连接断了重新发起请求负载均衡把后面的51到100片送到了服务器B。临时分片目录是服务器本地的/data/upload_tmp/文件MD5/结果A上的文件B看不到B上的文件A也看不到。合并时无论哪台服务器发起都会报“missing chunk”。这个坑的排查也很折磨人单独打一台服务器测一切正常一挂到LB下随机性失败。我当时排查了两天才反应过来问题是“临时目录没有共享”。解决办法有三种用NFS或CIFS把/data/upload_tmp挂载到所有PHP节点保证所有分片落在一个共享文件系统上。成本低但NFS性能一般且单点故障。把分片直接上传到对象存储或云OSSPHP只记录元数据。大厂主流做法成本高一点但可靠性和扩展性都好得多。临时目录保留在各节点本地用Redis做分片索引合并时某个节点通过内网从其他节点拉取缺失分片。复杂度最高不推荐。如果团队没有专门的运维支撑我建议先用NFS共享目录快速解决。等到并发量上来了再迁到对象存储不迟。5.3 安全与合规文件类型校验、目录权限与审计最后必须说安全这块出事就是大事。第一分片目录绝对不能放在Web可访问目录下。万一有人猜到路径直接下载别人上传的fileMd5目录里的分片就能拼出原始视频涉及隐私泄漏。我的做法是放在/data/upload_tmp完全独立在nginx的root之外。第二合并后的视频文件也要做类型校验。不能因为分片名以.part结尾就放松警惕。合并完成后用finfo_file检查MIME类型或者读取文件头判断是否为MP4、MOV等合法视频格式$finfo finfo_open(FILEINFO_MIME_TYPE); $mime finfo_file($finfo, $finalPath); finfo_close($finfo); $allowed [video/mp4, video/quicktime, video/x-msvideo, application/octet-stream]; if (!in_array($mime, $allowed, true)) { unlink($finalPath); throw new RuntimeException(invalid video type: . $mime); }为什么不能只看扩展名攻击者完全可以构造一个PHP一句话木马命成video.mp4再通过分片上传拼出来如果服务器配置不当直接变成webshell。虽然我们的分片接口只接收数据但这道防线必须做。第三要记录审计日志。理赔视频关系保险赔付属于业务证据链的一环谁在什么时间上传了什么文件、用了多长时间、最终文件MD5和大小是多少都应该写入日志。一旦后续发生理赔纠纷这个日志是技术层面能拿出的最客观材料。最后说点实在的分片秒传这套方案我在保险理赔项目里完整落地过。一开始也想过找现成的云服务直接接但出于数据安全和私有化部署的要求最终还是用HTMLPHP在自建服务器上实现了全套。现在查勘员传视频的失败率从原来的百分之八十几降到了个位数系统上线后最大的感受就是内勤不再因为“视频传不上来”给查勘员打电话了。如果你们团队也要做类似的事我建议不要一上来就上组件、上队列、上微服务。先用一个最简单的版本跑通“切片-上传-合并-预览”闭环再一步步加并发控制、断点续传、秒传判断和异步合并。每一步都有自己的坑提前踩一遍总比上线了被人追着骂强。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门