本文目录导读:

在PHP后端实现下载限速,核心思路是控制每次读取文件并输出到客户端的字节数,并在每次输出后暂停一定时间,这需要关闭PHP的输出缓冲,并使用 usleep() 控制时间间隔。
以下是几种实现方式,从简单到复杂:
基础版(最常用)
<?php
/**
* 限速下载文件
* @param string $filePath 文件路径
* @param int $kbps 限速速度,单位 KB/s
*/
function downloadWithLimit($filePath, $kbps = 100) { // 默认 100KB/s
if (!file_exists($filePath)) {
http_response_code(404);
echo "File not found.";
exit;
}
// 清理之前的输出
ob_clean();
flush();
// 设置下载头
$fileName = basename($filePath);
$fileSize = filesize($filePath);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Length: ' . $fileSize);
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Expires: 0');
header('Pragma: public');
// 打开文件
$fp = @fopen($filePath, 'rb');
if (!$fp) {
http_response_code(500);
echo "Failed to open file.";
exit;
}
// 计算每次读取的字节数 (例如每次读取 1024 字节)
$bytesPerSecond = $kbps * 1024; // 转换为字节/秒
$chunkSize = 1024; // 每次读取 1KB
// 计算暂停时间 (微秒)
$usleepTime = ($chunkSize / $bytesPerSecond) * 1000000;
// 发送文件内容
while (!feof($fp) && (connection_status() == CONNECTION_NORMAL)) {
echo fread($fp, $chunkSize);
flush(); // 强制刷新输出缓冲区
// 暂停以控制速度
usleep($usleepTime);
}
fclose($fp);
exit;
}
// 使用示例
downloadWithLimit('/path/to/your/file.zip', 200); // 200KB/s
更精确的速度控制
上面的方法在文件较小时可能不够精确,这个版本通过时间计算来更精确地控制:
<?php
function downloadWithLimitPrecise($filePath, $kbps = 100) {
if (!file_exists($filePath)) {
http_response_code(404);
exit;
}
// 设置头信息(同上)
$fileName = basename($filePath);
$fileSize = filesize($filePath);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Length: ' . $fileSize);
header('Cache-Control: no-cache');
$fp = fopen($filePath, 'rb');
if (!$fp) {
http_response_code(500);
exit;
}
// 关闭输出缓冲
if (ob_get_level()) {
ob_end_clean();
}
$bytesPerSecond = $kbps * 1024;
$chunkSize = 8192; // 8KB chunk, 减少频繁的循环
$startTime = microtime(true);
$totalSent = 0;
while (!feof($fp) && (connection_status() == CONNECTION_NORMAL)) {
$data = fread($fp, $chunkSize);
echo $data;
flush();
$totalSent += strlen($data);
$elapsed = microtime(true) - $startTime;
// 计算应该已经发送了多少字节
$expectedSent = $bytesPerSecond * $elapsed;
// 如果发送过快,就暂停
if ($totalSent > $expectedSent) {
$sleepTime = ($totalSent - $expectedSent) / $bytesPerSecond * 1000000;
usleep($sleepTime);
}
}
fclose($fp);
exit;
}
支持断点续传 + 限速
这个版本可以在支持断点续传的同时进行速度限制:
<?php
function downloadWithResumeAndLimit($filePath, $kbps = 100) {
if (!file_exists($filePath)) {
http_response_code(404);
exit;
}
$fileName = basename($filePath);
$fileSize = filesize($filePath);
$fp = fopen($filePath, 'rb');
if (!$fp) {
http_response_code(500);
exit;
}
// 处理断点续传
$start = 0;
$end = $fileSize - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches);
$start = intval($matches[1]);
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$fileSize");
header('Content-Length: ' . ($end - $start + 1));
} else {
header("Content-Length: $fileSize");
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');
// 关闭输出缓冲
if (ob_get_level()) {
ob_end_clean();
}
$bytesPerSecond = $kbps * 1024;
$chunkSize = 8192;
$bytesSent = 0;
$bytesToSend = $end - $start + 1;
$lastTime = microtime(true);
while (!feof($fp) && $bytesSent < $bytesToSend) {
$remaining = $bytesToSend - $bytesSent;
$readSize = min($chunkSize, $remaining);
$data = fread($fp, $readSize);
echo $data;
flush();
$bytesSent += strlen($data);
// 速度控制
$currentTime = microtime(true);
$elapsed = $currentTime - $lastTime;
if ($elapsed < 1) {
// 在1秒内控制发送量
usleep(($chunkSize / $bytesPerSecond) * 1000000);
} else {
$lastTime = $currentTime;
}
}
fclose($fp);
exit;
}
关键参数优化建议
| 场景 | chunkSize | 建议限速值 | 说明 |
|---|---|---|---|
| 小文件 (<10MB) | 1024 | 100-500 KB/s | 文件小,响应快即可 |
| 大文件 (>100MB) | 8192 | 500-2000 KB/s | 提高吞吐量 |
| 视频/音频 | 16384 | 用户网络带宽的80% | 保证流畅播放 |
| 同时下载用户多 | 4096 | 根据用户数平均分配 | 避免服务器过载 |
注意事项
-
服务器配置:需要在
php.ini设置:max_execution_time = 0 # 取消时间限制 memory_limit = -1 # 取消内存限制 output_buffering = Off # 关闭输出缓冲
-
Nginx/Apache 配置:如果使用反向代理,需要禁用缓冲:
# Nginx proxy_buffering off; fastcgi_buffering off;
-
更适合的实现方式:对于生产环境,建议使用专门的下载管理或流媒体服务器,PHP更适合做下载调度和权限控制,实际传输交给 Nginx 的
X-Accel-Redirect或 Apache 的mod_xsendfile。
动态控制不同用户的速度
<?php
// 根据用户类型获取速度限制
function getUserSpeedLimit($userId) {
// 从数据库或缓存获取
$userSpeeds = [
'vip' => 5000, // VIP 5MB/s
'normal' => 2000, // 普通 2MB/s
'guest' => 500 // 访客 500KB/s
];
$userType = getUserType($userId); // 获取用户类型
return $userSpeeds[$userType] ?? 1000;
}
// 在下载时调用
$speed = getUserSpeedLimit($currentUserId);
downloadWithLimit($filePath, $speed);
选择哪种方法取决于你的具体需求,基础版对于大多数场景已经足够,而精确控制版和断点续传版适用于更专业的下载场景。