本文目录导读:

在 PHP 中实现流式上传(Streaming Upload),主要目的是减少内存占用,特别是在处理大文件时,不必将整个文件一次性读入内存。
PHP 提供了多种方式实现流式上传,核心思路是读取一段,处理/发送一段,然后丢弃。
以下是几种常见的实现方式,按场景分类:
流式接收(服务器端处理上传)
如果你是在写服务器端代码,接收客户端上传的大文件,核心是配合 php://input 流进行读取。
<?php
// 处理 PUT 请求或非标准表单上传的场景
// 目标文件路径
$targetFile = '/path/to/save/uploaded_file.bin';
// 打开输入流(只读)
$input = fopen('php://input', 'r');
// 打开输出流(写入目标文件)
$output = fopen($targetFile, 'w');
if ($input && $output) {
// 每次读取 1MB(1024 * 1024 字节)
$chunkSize = 1024 * 1024;
while (!feof($input)) {
// 从输入流读取一个块
$chunk = fread($input, $chunkSize);
// 写入到输出流
fwrite($output, $chunk);
// 可选:清空缓冲区,避免阻塞
flush();
}
fclose($input);
fclose($output);
echo "文件上传成功";
} else {
http_response_code(500);
echo "无法打开输入/输出流";
}
注意:如果使用 multipart/form-data 标准表单上传,PHP 会处理到临时文件,此时用 move_uploaded_file() 即可,不涉及流式读取。
流式发送(API 请求转发)
如果你需要把上传的文件从 PHP 转发到另一个服务(例如第三方 API),避免在中间层占用大量内存。
<?php
// 假设文件已经上传到服务器临时目录 $temp_path
$temp_path = $_FILES['file']['tmp_name'];
$target_url = 'https://example.com/api/upload'; // 目标 API
$curl = curl_init($target_url);
$cfile = new CURLFile($temp_path, $_FILES['file']['type'], $_FILES['file']['name']);
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ['file' => $cfile],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
// 关键:设置超时为较长时间,避免大文件超时
CURLOPT_TIMEOUT => 300,
]);
$response = curl_exec($curl);
curl_close($curl);
// 处理 $response...
关键点:在使用 CURLFile 时,cURL 扩展会自己处理文件流的发送,不会把整个文件加载到 PHP 内存中。
流式下载(将文件流式输出到浏览器)
如果你需要将一个大文件(如视频、备份文件)流式输出到浏览器,而不占用内存:
<?php
$filepath = '/path/to/large/video.mp4';
// 处理 Range 请求(支持视频拖拽)略...
header('Content-Type: video/mp4');
header('Content-Length: ' . filesize($filepath)); // 注意:如果支持 Range,需要动态计算长度
// 关键:关闭输出缓冲
while (ob_get_level() > 0) {
ob_end_flush();
}
$handle = fopen($filepath, 'rb');
while (!feof($handle)) {
// 读取 1MB
echo fread($handle, 1024 * 1024);
// 强制将缓冲区输出给客户端
flush();
// 如果客户端断开了连接
if (connection_aborted()) {
break;
}
}
fclose($handle);
使用现成的库(推荐)
如果项目允许引入第三方库,使用 Guzzle 是个好选择,它原生支持流。
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
$client = new Client();
// 方式一:使用 Psr7\Utils::streamFor 包装资源
$stream = fopen('/path/to/large/file.txt', 'r');
$request = new Request('POST', 'https://api.example.com/upload', [],
\GuzzleHttp\Psr7\Utils::streamFor($stream)
);
$response = $client->send($request);
fclose($stream);
注意事项与最佳实践
| 要点 | 说明 |
|---|---|
| 内存限制 | 调整 memory_limit 可能会限制,但流式上传不应该依赖它。 |
| 超时设置 | 大文件处理务必设置较长的 max_execution_time(CLI 下默认不限)。 |
| 错误处理 | 检查 fopen 返回,处理好权限问题。 |
| 安全 | 不要使用 $_FILES 中的原始文件名直接保存,需进行过滤,防路径注入。 |
move_uploaded_file 例外 |
如果空间允许,官方推荐的 move_uploaded_file() 性能最高,因为它本质是 rename() 操作,不涉及流复制,仅在需要处理时用流。 |
fcgi 环境 |
在使用 fread + echo 时,flush() 可能不生效,需要配合 fastcgi_finish_request() 或在框架层处理。 |
总结建议
- 如果是接收标准 POST 上传:不需要流式,直接用
move_uploaded_file。 - PHP 是中间代理:推荐 cURL +
CURLFile。 - PHP 是接收方:使用
php://input流。 - 如果是纯 CLI 脚本:使用
fopen+fread循环,并注意yield或进程管理。
好的编码习惯是:能用流操作,就不用一次性加载,这是处理大文件的不二法门。