yii response 支持断点续传需手动处理 range 头:解析 http_range、计算合法字节范围、设置 accept-ranges/content-range 响应头,并用 stream() 分片输出;sendfile() 不支持因强制 200 状态与完整输出。

Yii Response 怎么设置 Accept-Ranges 和处理 Range 头
Yii 默认的 Response::sendFile() 不支持断点续传,它直接输出整个文件、设 Content-Length、不响应 Range 请求。要支持断点续传,必须手动控制响应头和分片输出,绕过 sendFile()。
核心是三件事:检查 Range 头、计算字节范围、用 Response::stream() 分段写入。Yii 没有内置 Range 解析工具,得自己 parse $_SERVER['HTTP_RANGE']。
- 先判断请求是否带
Range头:isset($_SERVER['HTTP_RANGE']) - 用正则提取字节范围,例如
bytes=0-1023→ 起始 0,结束 1023 - 文件总大小用
filesize($path)获取,确保范围不越界(min($end, $fileSize - 1)) - 设置关键响应头:
Accept-Ranges: bytes、Content-Range: bytes $start-$end/$fileSize、Content-Length: $length、状态码206 Partial Content
为什么不能直接用 Response::sendFile()
sendFile() 内部调用的是 PHP 的 readfile() 或 fpassthru(),它一次性输出全部内容,并强制设置 Content-Length 和 200 OK 状态。一旦响应头已发送,就无法再改状态码或加 Content-Range —— 这直接堵死了断点续传路径。
更隐蔽的问题是:某些 Web 服务器(如 Nginx)在启用 sendfile 时会拦截 PHP 的文件输出,导致自定义 Range 响应被覆盖或忽略。所以即使你在 Yii 里写了逻辑,也可能被中间层吃掉。
- 禁用
sendFile()是必须的,哪怕只是临时绕过 - 若部署在 Nginx,需确认未开启
fastcgi_buffering off或类似透传配置 - Apache 下要注意
mod_headers是否允许设置Content-Range
Response::stream() 怎么安全分片读取大文件
直接 fopen() + fseek() + fread() 最可控,但要注意资源释放和错误退出。Yii 的 stream() 只是帮你注册回调函数,不自动管理文件句柄。
典型做法是把文件打开、定位、读取封装进一个闭包,传给 $response->stream()。重点不是“怎么流”,而是“怎么不出错”:
- 务必用
fopen($path, 'rb'),二进制模式避免 Windows 下换行符干扰 -
fseek($fp, $start, SEEK_SET)后检查返回值,失败就抛异常或返回 416 - 每次
fread()建议不超过 8192 字节,太大会撑爆内存;循环读直到达到指定长度 - 读完必须
fclose($fp),否则文件句柄泄漏(尤其高并发时) - 响应前调用
$response->clear()清掉可能残留的 headers / content
示例关键片段:
use yii\web\Response;<br>$response = \Yii::$app->response;<br>$response->clear();<br>$response->statusCode = 206;<br>$response->headers->set('Accept-Ranges', 'bytes');<br>$response->headers->set('Content-Range', "bytes $start-$end/$fileSize");<br>$response->headers->set('Content-Length', $length);<br>$response->stream(function () use ($path, $start, $length) {<br> $fp = fopen($path, 'rb');<br> if (!$fp || fseek($fp, $start) !== 0) {<br> http_response_code(500);<br> exit;<br> }<br> $sent = 0;<br> while ($sent echo fread($fp, min(8192, $length - $sent));<br> $sent += ob_get_length();<br> ob_flush();<br> flush();<br> }<br> fclose($fp);<br>});
客户端没发 Range 头但服务端返回了 206?
这是常见误操作:代码里写了 206 和 Content-Range,但没校验请求头,结果所有请求都走断点逻辑。浏览器收到 206 却没 Range,会直接报错或拒绝解析。
必须严格区分两种路径:有 Range → 返回 206 + 分片;无 Range → 返回 200 + 全量 + Accept-Ranges: bytes(让客户端知道你支持续传)。
- 永远用
if (isset($_SERVER['HTTP_RANGE']))做第一道判断 - 不要在无
Range时设置Content-Range,这个 header 只能出现在206响应中 -
Accept-Ranges: bytes可以且应该在所有响应中设置(包括200),它是能力声明,不是行为承诺
断点续传真正的复杂点不在 PHP 侧,而在边界条件:多段 Range(bytes=0-100,200-300)、重叠范围、超长文件(>2GB)、NFS 或远程存储挂载路径下的 fseek 行为差异——这些很少被测试覆盖,上线后才暴露。











