保持Ajax连接存活以规避防火墙超时问题
这种防火墙硬超时的问题我太熟了——之前帮团队解决过类似的批量数据计算场景,既然改不了防火墙规则,绝对不能死磕单个长连接,得换个思路。给你几个经过实践验证的可行方案:
可行解决方案
1. 任务异步化 + 定时轮询(最稳妥)
把原来的单长请求拆成两步,彻底避开防火墙超时限制:
- 第一步:发起短请求提交计算任务,服务器立即返回唯一任务ID,同时后台异步启动计算(别让Web进程阻塞)。
- 第二步:前端每隔20秒(小于防火墙30秒阈值)发起短请求,用任务ID查询进度/结果,直到拿到最终数据。
示例代码
前端(JS):
// 第一步:提交计算任务 fetch('/start-calc.php', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ rawData: yourInputData }) }) .then(res => res.json()) .then(taskInfo => { // 第二步:定时轮询结果 const pollTimer = setInterval(() => { fetch(`/check-result.php?taskId=${taskInfo.id}`) .then(res => res.json()) .then(result => { if (result.status === 'done') { clearInterval(pollTimer); // 处理最终计算结果 renderResult(result.data); } else if (result.status === 'failed') { clearInterval(pollTimer); alert('计算出错,请稍后重试'); } // 仍在处理中则继续轮询 }); }, 20000); // 20秒一次,留足缓冲区避免触发超时 });
后端(PHP):
start-calc.php:生成任务ID,启动异步计算进程:
<?php // 生成唯一任务ID $taskId = uniqid('calc_', true); // 用Redis保存任务状态(也可以用数据库) $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->setex("task:$taskId", 3600, json_encode([ 'status' => 'processing', 'input' => json_decode(file_get_contents('php://input'), true) ])); // 启动后台计算进程(避免阻塞Web服务) exec('php background-calc.php ' . escapeshellarg($taskId) . ' > /dev/null 2>&1 &'); echo json_encode(['id' => $taskId]); ?>
background-calc.php:后台执行耗时计算,完成后更新状态:
<?php $taskId = $argv[1]; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $taskData = json_decode($redis->get("task:$taskId"), true); // 模拟耗时计算(比如数据库批量统计、复杂运算) sleep(120); // 计算完成后更新任务状态 $redis->set("task:$taskId", json_encode([ 'status' => 'done', 'data' => '这里是最终计算结果' ])); ?>
check-result.php:查询任务状态接口:
<?php $taskId = $_GET['taskId']; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $taskStatus = $redis->get("task:$taskId"); echo $taskStatus ?: json_encode(['status' => 'not_found']); ?>
2. 长轮询(Long Polling)
和普通轮询不同,长轮询是前端发起请求后,服务器hold住连接直到有结果或接近超时(比如25秒)再返回,前端收到响应后立即发起下一次请求。既能减少请求次数,又能避开防火墙超时。
示例代码
前端:
function longPollTask(taskId) { fetch(`/long-poll.php?taskId=${taskId}`) .then(res => res.json()) .then(result => { if (result.status === 'done') { renderResult(result.data); } else { // 立即发起下一次长轮询 setTimeout(() => longPollTask(taskId), 1000); } }) .catch(err => { // 网络出错后重试 setTimeout(() => longPollTask(taskId), 5000); }); } // 先提交任务,再启动长轮询 fetch('/start-calc.php', { method: 'POST', body: ... }) .then(res => res.json()) .then(taskInfo => longPollTask(taskInfo.id));
后端(long-poll.php):
<?php $taskId = $_GET['taskId']; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); // 设置25秒超时,比防火墙阈值少5秒 $timeout = time() + 25; while (time() < $timeout) { $task = json_decode($redis->get("task:$taskId"), true); if ($task['status'] === 'done' || $task['status'] === 'failed') { echo json_encode($task); exit; } // 每秒查一次,避免CPU空转 sleep(1); } // 超时后返回"处理中",触发前端重新发起请求 echo json_encode(['status' => 'processing']); ?>
3. Server-Sent Events(SSE)
SSE是HTML5原生特性,允许服务器主动向客户端推送数据,连接会自动保持心跳,相比普通Ajax长连接更不容易被防火墙判定为“闲置连接”。不过要注意,部分老防火墙可能对SSE有特殊限制,建议先在测试环境验证。
示例代码
前端:
// 先提交任务 fetch('/start-calc.php', { method: 'POST', body: ... }) .then(res => res.json()) .then(taskInfo => { // 建立SSE连接 const eventSource = new EventSource(`/sse-push.php?taskId=${taskInfo.id}`); eventSource.onmessage = function(event) { const result = JSON.parse(event.data); if (result.status === 'done') { eventSource.close(); renderResult(result.data); } else if (result.status === 'failed') { eventSource.close(); alert('计算失败'); } }; eventSource.onerror = function() { eventSource.close(); // 出错后重试连接 setTimeout(() => {/* 重新建立SSE连接 */}, 5000); }; });
后端(sse-push.php):
<?php header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); $taskId = $_GET['taskId']; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); while (true) { $task = json_decode($redis->get("task:$taskId"), true); if ($task['status'] === 'done' || $task['status'] === 'failed') { echo "data: " . json_encode($task) . "\n\n"; ob_flush(); flush(); exit; } // 每10秒发送心跳,避免连接被判定为闲置 echo ": ping\n\n"; ob_flush(); flush(); sleep(10); } ?>
关键注意事项
- 所有方案都要保证后台计算是异步的,绝对不能让Web服务器进程长时间阻塞,否则会耗尽服务器连接数。
- 任务ID要足够唯一,建议用带时间戳的UUID,避免冲突。
- 要处理任务超时场景,比如计算超过1小时仍未完成,前端要提示用户并终止轮询。
内容的提问来源于stack exchange,提问作者Jack
相关产品推荐
相关产品推荐

