
idm 等多线程下载管理器会并发发起多个请求,导致 php 动态生成 zip 的脚本被重复执行、资源争用甚至阻塞,最终使下载耗时翻倍。本文提供可落地的检测与规避方案,确保普通用户与 idm 用户均获得稳定响应。
idm 等多线程下载管理器会并发发起多个请求,导致 php 动态生成 zip 的脚本被重复执行、资源争用甚至阻塞,最终使下载耗时翻倍。本文提供可落地的检测与规避方案,确保普通用户与 idm 用户均获得稳定响应。
Internet Download Manager(IDM)等工具默认启用“分段下载(HTTP Range requests)”机制,通过发送多个并行 GET 请求(常带 Range: bytes=0-, Range: bytes=1000- 等头)来加速大文件传输。但您的 PHP 脚本并非为并发设计:它每次请求都重新生成 ZIP 文件(含多次 cURL 请求、磁盘写入、ZipArchive::addFromString 操作),且共享同一临时文件名(Test.zip)。当 IDM 发起 2–4 个并发请求时,就会出现以下典型问题:
- 多个 PHP 进程同时尝试 fopen(Test.zip, 'c') → 文件锁冲突或覆盖;
- 同一 ZIP 文件被多个进程反复打开/写入/关闭 → ZipArchive::close() 失败或 ZIP 损坏;
- 5 份报告被重复生成 3–4 次 → CPU、内存、网络 I/O 倍增,总耗时显著上升(如您观察到的 20s + 20s)。
✅ 根本解决思路:识别并拦截非主请求(即 IDM 的预检/分片请求),仅允许首次完整请求执行 ZIP 构建逻辑。
✅ 推荐方案:基于 HTTP 头智能判断主请求
IDM 在发起分片请求前,通常先发送一个无 Range 头的 试探性 HEAD 或 GET 请求(用于获取 Content-Length 和 Accept-Ranges),随后才发带 Range 的子请求。而真实浏览器点击下载时,仅发出唯一一次无 Range 的 GET 请求。因此,我们可通过以下组合条件精准识别“合法主请求”:
// 检查是否为 IDM / 多线程工具的非主请求(应直接拒绝)
if (isset($_SERVER['HTTP_RANGE']) ||
isset($_SERVER['HTTP_IF_RANGE']) ||
(isset($_SERVER['HTTP_USER_AGENT']) &&
preg_match('/IDM|Internet Download Manager|NetAnts|FlashGet/i', $_SERVER['HTTP_USER_AGENT']))) {
// 关键:对分片请求或已知下载工具,返回 403 或重定向到静态资源
http_response_code(403);
die('Direct download not allowed for segmented clients.');
}⚠️ 注意:不要仅依赖 User-Agent(易伪造且部分 IDM 可伪装),必须优先检查 HTTP_RANGE —— 这是 HTTP 协议级分片下载的明确信号。
立即学习“PHP免费学习笔记(深入)”;
✅ 进阶优化:服务端 ZIP 流式生成(避免临时文件)
当前代码将 ZIP 写入磁盘再 readfile(),加剧 I/O 竞争。更健壮的做法是内存中构建并直接输出流,彻底消除文件锁风险:
<?php
header('Content-Disposition: attachment; filename="Reports.zip"');
header('Content-Type: application/zip');
header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
header('Expires: 0');
header('Pragma: public');
// 检查并发请求(关键防御)
if (isset($_SERVER['HTTP_RANGE'])) {
http_response_code(403);
die();
}
// 使用 php://output 直接输出,不落地文件
$zip = new ZipArchive();
if ($zip->open('php://output', ZipArchive::CREATE) !== TRUE) {
http_response_code(500);
die('ZIP creation failed.');
}
// 预生成所有报告内容(避免在 addFromString 中阻塞)
$reports = [];
foreach ([1,2,3,4,5] as $id) {
$path = dirname($_SERVER['HTTP_REFERER']) . '/myreport.php';
$ch = curl_init($path);
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => ['id' => $id],
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_NOSIGNAL => 1,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
]);
$content = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || $content === false) {
$zip->close();
http_response_code(500);
die("Failed to fetch report $id");
}
$reports["Report $id.pdf"] = $content;
}
// 批量写入 ZIP 流
foreach ($reports as $name => $data) {
$zip->addFromString($name, $data);
}
$zip->close(); // 自动 flush 到 php://output
exit;⚠️ 重要注意事项
- 禁用输出缓冲干扰:确保 ob_start() 未在脚本前开启,或显式调用 ob_end_clean()(您原代码中的 ob_clean() 正确,但需确认无其他缓冲层);
- 超时与内存控制:ZIP 构建过程可能较长,建议设置 set_time_limit(300) 和 ini_set('memory_limit', '512M');
- 安全性加固:$_SERVER['HTTP_REFERER'] 不可靠,应改用绝对路径或配置白名单;$id 参数需严格校验(如 filter_var($id, FILTER_VALIDATE_INT));
- 替代方案:若 ZIP 生成频率高,建议改为预生成 + 定时清理(如 cron 每5分钟打包,PHP 仅 readfile() 静态 ZIP),彻底规避并发问题。
通过以上改造,IDM 用户将因首个无 Range 请求被正常响应而完成下载,后续分片请求则被 403 拒绝,避免重复工作;真实用户体验不变,服务器负载显著降低 —— 实现兼容性与性能的双赢。











