使用 readfile() 配合正确的 http 响应头(如 content-type、content-disposition 和 accept-ranges)即可实现 mp4 视频的强制下载与断点续传。核心在于检测客户端发送的 range 请求头,计算字节范围,并通过 fseek() 定位文件指针后分块输出,同时设置 content-range 响应头告知客户端当前传输的数据区间。

如何配置基础响应头以触发浏览器下载行为
要实现强制下载而非直接在浏览器中播放,必须设置 Content-Disposition: attachment 并指定文件名。同时,对于视频文件,需明确声明 Content-Type: video/mp4。若省略这些头部,浏览器可能尝试内联播放;若未正确设置 MIME 类型,部分客户端可能拒绝处理或报错。以下代码展示了最基础的下载逻辑,它会将整个文件一次性发送给客户端,适用于不支持断点续传或无需大文件优化的场景。

<?php
$file = 'video.mp4';
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream'); // 或使用 video/mp4
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
?>
上述代码通过 readfile() 直接读取文件内容并输出到标准输出流。虽然简单,但它无法处理中断后的重新连接,因为每次请求都会从头开始读取整个文件。对于大体积 MP4 文件,这会导致带宽浪费和用户体验下降。因此,需要引入对 HTTP_RANGE 环境变量的解析,以实现分段传输。
立即学习“PHP免费学习笔记(深入)”;
如何解析 Range 头并实现断点续传逻辑
断点续传的关键在于支持 HTTP 1.1 的 Range 请求。当客户端支持续传时,它会发送 Range: bytes=0-1023 这样的头部,表示从第 0 字节到第 1023 字节。服务器需解析该字符串,提取起始和结束位置,使用 fopen() 打开文件,用 fseek() 跳转到指定偏移量,然后循环读取剩余数据直到结束位置。同时,必须返回 206 Partial Content 状态码以及 Content-Range: bytes 0-1023/total_size 头部,否则客户端可能无法识别这是部分数据。

<?php
$file = 'video.mp4';
$size = filesize($file);
$start = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches);
if ($matches) {
$start = intval($matches[1]);
$end = $matches[2] ? intval($matches[2]) : $size - 1;
}
}
header('HTTP/1.1 206 Partial Content');
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . ($end - $start + 1));
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
header('Accept-Ranges: bytes');
$fp = fopen($file, 'rb');
fseek($fp, $start);
while ($start <= $end && !feof($fp)) {
echo fread($fp, min(8192, $end - $start + 1));
flush();
$start += 8192;
}
fclose($fp);
?>
该实现首先检查 $_SERVER['HTTP_RANGE'] 是否存在。如果存在,则通过正则表达式提取字节范围。默认情况下,如果没有 Range 头,服务器将发送完整文件(此时应返回 200 OK,但为简化示例,此处统一处理逻辑,实际生产中需区分 200 和 206)。在循环中,每次读取 8KB 数据并立即输出,调用 flush() 确保数据实时推送给客户端,这对于大文件下载至关重要。最后关闭文件句柄,释放资源。
当 Range 请求超出文件边界时会发生什么
如果客户端请求的字节范围超出了文件的实际大小,例如请求 Range: bytes=1000000-2000000 而文件仅有 500KB,服务器应当如何处理?根据 HTTP 规范,如果范围无效(即起始位置大于文件大小),服务器应返回 416 Requested Range Not Satisfiable 错误,并在响应头中包含 Content-Range: bytes */actual_size。在上述代码中,若未做边界检查,fseek() 可能会失败或产生未定义行为,导致下载错误或空文件。因此,在实际部署前,必须验证 $start 是否小于 $size,否则主动抛出 416 错误,确保客户端能正确识别并放弃无效请求。



















