原生PHP实现蓝奏云大文件分片上传实战
做网站和资源分发这些年,我经手过的文件大小早就从几 MB 涨到了几个 GB。安装包、离线文档、视频教程、数据库备份……每次给用户甩下载链接的时候,蓝奏云都是我排第一梯队的选手。原因很简单:国内节点直连,下载不限速,分享页干净,登录拿货都顺。但蓝奏云也有个老生常谈的软肋——单文件体积限制。手里捏着一个 1.2GB 的安装包想传上去,网页端直接拒绝。最初我也没辙,后来干脆用 PHP 写了一套分片上传方案:本地把大文件切开,逐片模拟网页上传请求,全部传完再把分享链接整理出来。这套方案在我自己维护的资源站上跑了小半年,稳定性比想象中好。这篇文章就把完整思路、原生 PHP 源码和实测过程都扒开讲一遍,适合正在做网盘工具、资源分发或 PHP 上传功能开发的你参考。
1. 为什么蓝奏云需要分片上传方案
1.1 蓝奏云的定位与单文件限制
蓝奏云这个产品的定位一直很清晰:轻量分享。它没打算跟那些主打大容量存储的网盘硬碰硬,核心卖点是下载速度快、不强制登录、分享链接能长期有效。可这个“轻量”也直接体现在上传限制上。网页端和客户端默认对单文件大小有硬门槛,普通账户实测下来,超过 100MB 的文件会被拦在入口之外,有些文件类型甚至限制得更死。
这个限制放在日常传文档、传小压缩包、传安装包碎片还好。但凡是遇到“必须整体交付”的场景就很尴尬:
- 1GB 以上的软件发行包
- 几十 GB 的虚拟主机备份
- 培训课程的视频合集
- 数据库导出后的 SQL 大文件
这些内容不是不能传到别的平台,而是蓝奏云在下载体验上有不可替代的优势。当时我在做一款内部工具的分发,要求用户拿到的下载地址必须是国内直连、免登录、不减速的。比了一圈,还是蓝奏云最合适。那问题就剩一个:如何突破单文件大小限制,把大文件顺利送上去?
1.2 分片上传方案的整体架构
这个问题的答案,其实早在“断点续传”“秒传”这些概念里就写好了:把一个文件拆成多个小块,逐个传输,最后在目标端还原。最常见的是“服务端合并”模式——客户端把文件切分后上传到服务器,服务器接收全部切片后组装回原文件。这种模式适合自己掌控服务端的情况。
但蓝奏云不是自己的服务器,它不接受“切片合并”这种玩法,上传接口收到的就是一个个独立的文件。所以这套方案实际采用的是“逻辑分片”思路:
- 用 PHP 脚本在本地把大文件切成指定大小的小块
- 每一块作为一个独立文件,通过网页版上传接口提交到蓝奏云
- 上传成功后,每个分片对应一个独立的分享链接
- 下载侧拿到链接集合后,按序号逐个下载或直接合并还原
这种方式看起来很“土”,但胜在完全不用动服务端,也不需要破解任何平台逻辑。蓝奏云看到的是 N 个合法的普通文件上传请求,完全符合平台预期。唯一需要下功夫的,是脚本怎么切分、怎么上传、怎么保证全部传完不遗漏。
我选择用原生 PHP 来实现还有一个实际原因:资源站所在的服务器本来就有 PHP 环境,不需要额外装 Python 或 Go 的运行库。PHP 的 curl 扩展成熟稳定,CLI 模式下跑长时间任务也不会有内存限制问题。脚本可以扔到任何一台带 PHP 的机器上直接跑,维护成本极低。
2. 原生 PHP分片上传方案的核心实现
2.1 分片大小的选择逻辑
分片大小是整个方案里最需要拿捏的参数,不是拍脑袋定的。定太小,比如 5MB,一个 1.2GB 的文件会产生 240 多个分片,上传请求数量激增,不仅让总耗时变长,还更容易触发平台对高频请求的临时限制。定太大,比如 90MB,单片非常接近蓝奏云的上限,一旦上传接口对大小有额外浮动判断,整片直接失败,重试成本极高。
我按自己的网络环境和平台表现反复试了几轮,得出的经验区间是 20MB 到 50MB。默认推荐 50MB,理由如下:
- 远低于网页端限制,给接口校验留出余量
- 单片传输时间适中,失败重试的代价可控
- 1GB 文件只需要 20 次左右请求,日志清晰,后续核对也方便
分片数量的计算逻辑很简单,直接对文件大小除以分片大小向上取整:
以 1.2GB 的文件为例,50MB 分片会产生 25 个分片。如果网络条件比较差,比如在跨网宽带上跑,可以把 $chunkSize 降到 20MB,用分片数量换单次传输成功率。这个参数改一行就能生效,建议实际跑批量任务之前,先用一个小文件做一次全链路演练。
2.2 上传请求的源码解析
原生 PHP 实现这套方案的核心,不是文件切分本身,而是“怎么把一个文件分片以合法请求的形式提交到蓝奏云的上传接口”。网页端上传文件时,浏览器会发起一个 multipart/form-data 类型的 POST 请求,里面既包含文件二进制内容,也包含若干表单字段、Cookie 和必要的请求头。PHP 脚本要做的,就是用 curl 把这个请求完整复刻出来。
先看封装好的上传类骨架:
很多人卡在“模拟上传请求”这一步,是因为不知道这些参数从哪来。实际上不需要把所有参数都手动构造,网页端上传页面里本身就藏着绝大部分字段。用浏览器开发者工具抓一次上传过程,把请求里的表单字段对照页面源码看一下,就能发现字段来源。脚本里专门写一个 getUploadParams() 方法,每次上传前动态去页面提取,就是为了避免平台调整参数之后脚本立刻失效。
这里有个很关键的细节:CURLFile 的第三个参数是文件名,这个值会直接影响蓝奏云里显示的最终文件名。如果这里传的是分片的临时路径名,比如 chunk_0001.part,那蓝奏云上就会出现一堆奇奇怪怪的名字。正确做法是传一个有意义的文件名,比如 setup_1.2g_full.part001,方便后续排序和识别。
2.3 主循环与断点续传设计
上传单个分片并不难,真正考验脚本功底的是主循环的健壮性。整个文件有几十个分片,随便一个分片因为网络波动失败,如果不处理,整个任务就要从头再来。这跟下载工具里的断点续传是一个道理。
我在主循环里加了“已上传分片记录”机制:
这套逻辑里最值钱的不是循环本身,而是 .done 记录文件。脚本跑到一半中断了,重新启动后会自动跳过已经完成的分片,不用重新花老长时间传前面那些。对动辄几十个分片的大文件来说,这个设计能省下大量重复劳动。
为什么推荐用原生 PHP 而不是上框架?因为这种脚本根本没有路由、模板、ORM 的需求,框架自带的依赖注入和中间件机制只会增加内存占用和启动耗时。原生 PHP 配合 curl 扩展,几十行代码就能解决问题,性能更好,调试也更直接。
3. 实测过程:把 1.2GB 安装包拆成 25 片传上去
3.1 运行前的准备
先说环境。这套脚本在 Windows 和 Linux 上都能跑,但生产环境建议放在 Linux 服务器上,原因后面讲。需要确认 PHP 已安装并开启了 curl 和 openssl 扩展:
如果输出里没有这两个扩展,需要先安装。Debian/Ubuntu 系统一般执行 apt install php-curl 即可,Windows 下则在 php.ini 里打开对应 extension。
然后是登录凭证。蓝奏云网页端上传需要登录态,最直接的获取方式是用浏览器登录账户,打开开发者工具,在 Network 面板里找到任意一个请求,复制请求头里的 Cookie 字符串。这个字符串有效期通常能维持几天,但建议每次批量上传前都重新拿一次。
测试文件我选了一个 1.2GB 的软件安装包。按 50MB 分片计算,总共 25 个分片。开始之前先算好并记录源文件的 md5 值:
这个值不是给蓝奏云校验的,而是上传完成后下载回来做完整性比对的依据。没有这一步,后面验证时只能凭感觉判断文件是否损坏。
3.2 脚本执行与结果记录
执行命令很简单:
脚本运行后输出如下:
整个过程大约 14 分钟,中途没有出现失败分片,一次跑完。这符合我这边普通家庭宽带上行速度的正常水平。如果你所在的网络环境上传速度只有几 Mbps,建议把分片调小,同时把 curl 超时时间拉长,避免单片传输时间过长导致连接被服务端断开。
上传完成后,每个分片在蓝奏云里都是一个独立文件,排列在同一目录下。脚本随后会输出一串分享短链。把短链按分片序号整理成下载清单,这一步我是让脚本直接生成一个 HTML 文件,按文件名排序把所有链接放进去,用户拿到后逐一点击下载即可。
3.3 完整性与可复现性验证
传输结束不等于事情结束。我把所有分片下载回本地,在命令行里做拼接校验。Linux 下执行合并:
然后比对 md5:
比对结果与源文件完全一致,说明分片上传过程没有数据损坏。这个验证必须做,因为分片文件一个接一个传,任何一片丢失或内容错误,最终合并结果都会有偏差。不要以为蓝奏云网页上传就一定可靠,网络层的偶发错误谁都无法完全避免。
整个流程跑下来,本质上就是把一次“不允许的大上传”变成若干次“允许的小上传”,平台侧逻辑完全没碰,数据也没有经过第三方中转,属于非常干净的实现路径。
4. 实测中踩过的坑与排查实录
4.1 Cookie 失效与登录态过期
这是我遇到过最频繁的问题。脚本跑着跑着,突然连续出现上传失败,查看响应内容发现返回的是“未登录”或“请先登录”之类的提示。原因很简单:Cookie 过期了,或者账户在别处登录把旧登录态顶掉了。
解决方案分两层。第一层,在脚本中检测响应内容中的关键词,一旦发现未登录提示,立即停止上传并输出错误信息,不要傻乎乎地继续重试。第二层,封装一个登录方法,在 main 循环开始前自动用账号密码换取新的登录态。如果实在不想处理登录接口,就保持“每次跑任务前手动刷新 Cookie”的习惯,也能用。
4.2 超时中断导致的分片缺失
PHP CLI 模式默认没有 max_execution_time 限制,但 curl 自身有超时设置。我第一次跑 90MB 分片的时候,单片上传耗时超过 120 秒,curl 直接报超时错误,分片实际上已经传到服务器了,但脚本这边收到了超时信号,准备重传同一片。
这听起来不严重,但实际会带来重复文件问题。蓝奏云接收到同名文件,默认行为不一定是覆盖,有可能生成新文件,最后目录里出现两个相同分片,排序还错乱。避免这个问题的办法有两个:
- 分片大小控制在 50MB 以内,让单片上传时间显著低于超时阈值
- 上传前检测目录是否已存在同名文件,如果有就先删除或复用
CURLOPT_TIMEOUT 我最终设置为 300 秒,配合 50MB 分片,几乎不会再出现超时误判。
4.3 上传接口参数变动
平台方偶尔会调整上传请求的参数名或校验逻辑。如果脚本里把所有字段名都写死,一旦平台改动,脚本立刻报废。我踩过一次这个坑,排查半天才发现是请求里少了一个平台新加的校验字段。
规避方案就是前面提到的:每次上传前先 GET 一次上传页面,用正则把页面里的隐藏字段全提取出来,动态构造 POST 请求。即使平台加字段,只要它还在页面上,脚本就能自动适应。用浏览器抓包对比页面源码,就能建立起“页面字段到请求参数”的映射关系,这块理解了,以后改接口都不慌。
4.4 文件名编码与分享页排序
分片文件如果直接叫 1.part、2.part,排序会非常混乱,尤其是超过 10 个分片后,10.part 会排到 2.part 前面。解决方案就是统一用固定宽度编号命名:
例如 setup.part001 到 setup.part025。这样在蓝奏云文件管理页和最终下载页里,分片顺序都一目了然。
中文文件名也有坑。PHP 在不同系统下的文件名编码表现不一致,Windows 下容易出现 GBK 与 UTF-8 混用。我的建议是最外层包装成发布用的英文名,源文件中文名留在本地记录,尽量避免把中文名直接塞给上传接口。
4.5 排查速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 所有分片全部失败 | Cookie 过期 | 重新登录,更新 Cookie |
| 偶发单片失败 | 网络波动或服务端限流 | 增加重试,失败后 sleep 5-10 秒 |
| 重复分片文件 | 超时误判或同名文件 | 上传前检查文件是否存在 |
| 响应提示非法请求 | 缺少动态参数 | 重新抓包,确认上传页面字段 |
| 合并后 md5 不一致 | 分片遗漏 | 重新核对分片列表顺序 |
5. 可扩展的方向与个人心得
5.1 从命令行脚本到 Web 管理面板
脚本跑通以后,我很快就发现纯命令行不够方便。毕竟不是每次都想 SSH 到服务器敲命令,于是给它套了一层极简的 Web 前端:一个 PHP 页面,列出本地待上传目录下的所有大文件,点击按钮即可触发后台任务,前端用轮询显示进度条。核心上传类完全复用,只多了任务队列和日志模块。
队列的设计要点是上传任务的串行化。蓝奏云对高频并发请求是有风控的,同时传三个大文件,每个文件又有几十个分片,并发滥用很容易触发临时封禁。所以在 Web 面板里,任务队列严格执行 FCFS(先来先服务),同一时刻只允许一个文件在上传进程里。
5.2 扩展到备份与分发自动化
这个分片上传脚本最实用的衍生场景是定时备份。我在服务器上写了个 cron 任务,每天凌晨把网站目录、数据库导出文件打包,如果大小超过 100MB,就自动走分片上传逻辑推到蓝奏云。这样即使服务器本地磁盘出问题,备份文件也有一个异地的安全副本。
定时任务场景下,建议在分片上传后加入通知机制。脚本执行完成后,通过邮件或 Webhook 把本次上传生成的新链接推送出来。否则备份文件每天产生几十条新链接,靠人工去盯完全不现实。
5.3 关于平台规则和实际效果的几点反思
整套方案跑了大半年,有几点体会很深。分片上传不是“绕过平台限制”,而是改变文件交付方式,让大文件以平台允许的形态完成分发。使用时还是要注意分寸:只上传自己拥有版权的合规资源,控制上传频率,避免瞬间大量请求对平台造成压力。合规操作下,这套方案才能长期稳定地运转下去。
如果让我重头再做一次,我会在一开始就把分片序号设计成三位固定宽度,并且在文件名里直接写源文件的 md5 前 8 位。这样后续核对分片完整性时,不用打开文件内容,光看文件名就能判断归属。这个习惯我后来沿用到所有生成文件的脚本里,省了无数次排错时间。上传完成后,也请务必保留一份 .done 记录文件。分片列表就是整个任务的索引,丢了它,等于几十个文件在网盘里成了一堆无主的数据。