PHP 8.0 文件下载核心是正确设置响应头触发浏览器下载:1. 基础下载用readfile()配合Content-Type和Content-Disposition;2. 动态文件用php://output流式输出;3. 大文件需支持Range断点续传;4. 注意无前置输出、中文名编码及权限问题。

PHP 8.0 实现文件下载到本地,核心是正确设置 HTTP 响应头,让浏览器识别为“可下载文件”,并输出文件内容。关键不是“保存到服务器”,而是“触发浏览器下载行为”。
1. 基础下载:读取本地文件并强制下载
适用于已存在服务器上的文件(如 PDF、ZIP、图片等)。注意路径安全,避免目录遍历漏洞。
⚠️ 安全提示:不要直接用用户传入的文件名拼接路径,需校验或白名单过滤。
- 使用 readfile() 直接输出文件流,内存友好
- 设置 Content-Type: application/octet-stream 通用二进制类型
- 设置 Content-Disposition: attachment; filename="xxx" 触发下载对话框
- 添加 Content-Transfer-Encoding: binary 和 Expires/Cache-Control 防止缓存问题
示例代码(download.php):
<?php $filePath = '/var/www/files/report.pdf'; // 替换为真实绝对路径
<p>// 检查文件是否存在且可读
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
die('File not found or inaccessible.');
}<p>$fileName = basename($filePath);
$fileSize = filesize($filePath);</p><p>// 清空输出缓冲区(防止之前输出干扰)
if (ob_get_level()) {
ob_end_clean();
}</p><p>// 发送响应头
header('Content-Type: application/octet-stream');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $fileSize);
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');</p><p>// 输出文件内容
readfile($filePath);
exit;
?></p>
2. 下载生成的临时文件(如导出 CSV)
适合动态生成内容(如数据库导出),不落地磁盘,直接输出流。
- 用 fopen('php://output', 'w') 写入内存流
- 配合 fputcsv() 等函数生成结构化数据
- 响应头中 filename 建议用 .csv 或对应扩展名
CSV 导出示例:
<?php header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="users-export-' . date('Y-m-d') . '.csv"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
<p>$output = fopen('php://output', 'w');
// 写入表头
fputcsv($output, ['ID', 'Name', 'Email']);
// 写入数据行(模拟)
fputcsv($output, [1, '张三', 'zhang@example.com']);
fputcsv($output, [2, '李四', 'li@example.com']);
fclose($output);
exit;
?>
3. 大文件下载优化(支持断点续传)
对超大文件(如 >100MB),建议支持 Range 请求,提升用户体验和兼容性(如网络中断后可续传)。
- 检查 HTTP_RANGE 请求头
- 计算起始/结束字节,用 fseek() + fread() 分段输出
- 返回 206 Partial Content 状态码及对应 headers
简化版支持 Range 的片段(需配合完整逻辑):
// ... 文件存在性检查后
$fp = fopen($filePath, 'rb');
$fileSize = filesize($filePath);
$range = '';
if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches)) {
$start = intval($matches[1]);
$end = isset($matches[2]) ? intval($matches[2]) : $fileSize - 1;
if ($end >= $fileSize) $end = $fileSize - 1;
$length = $end - $start + 1;
<pre class="brush:php;toolbar:false;">header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
header('Content-Length: ' . $length);
fseek($fp, $start);} else { header('Content-Length: ' . $fileSize); }
header('Accept-Ranges: bytes'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . rawurlencode(basename($filePath)) . '"');
// 分块读取输出(防内存溢出) $chunkSize = 8192; while (!feof($fp) && (connection_status() == CONNECTION_NORMAL)) { echo fread($fp, $chunkSize); flush(); } fclose($fp); exit; ?>
4. 注意事项与常见问题
-
输出前不能有任何输出:包括空格、BOM、echo、warning。可用
ob_start()+ob_end_clean()缓冲兜底 -
中文文件名兼容性:用
rawurlencode()(推荐)或mb_convert_encoding()转 UTF-8 后再 urlencode -
PHP 8.0 兼容性:上述代码完全兼容 PHP 8.0+,无需额外适配(
readfile、fopen等函数无变化) - 权限与路径:确保 Web 进程(如 www-data)有文件读取权限;优先用绝对路径,避免相对路径歧义
不复杂但容易忽略:一次请求只做一件事——设置好头,输出内容,然后 exit。别在下载逻辑后继续执行其他脚本。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











