连接处理
概述
PHP 的连接处理功能允许检测客户端是否断开连接,并控制脚本在客户端断开后的行为。connection_aborted() 检测连接状态,ignore_user_abort() 设置脚本是否在客户端断开后继续执行。这些功能在处理长时间运行的脚本(如文件导出、批量处理)时非常重要。
适用场景
- 大文件下载进度处理
- 长时间后台任务
- 数据库备份操作
- 邮件群发任务
基础概念
核心函数
| 函数 | 功能 | 返回值 |
|---|---|---|
connection_aborted() | 检测客户端是否断开 | int (0/1) |
connection_status() | 获取连接状态 | int |
ignore_user_abort() | 设置断开是否中止脚本 | int |
register_shutdown_function() | 注册关闭回调函数 | void |
header('Connection: close') | 提前关闭连接 | - |
连接状态常量
| 常量 | 值 | 说明 |
|---|---|---|
CONNECTION_NORMAL | 0 | 正常连接 |
CONNECTION_ABORTED | 1 | 用户断开 |
CONNECTION_TIMEOUT | 2 | 超时 |
FastCGI 注意
在使用 PHP-FPM 或 FastCGI 时,连接处理行为可能与 mod_php 不同。ignore_user_abort() 和 connection_aborted() 的行为可能受 FastCGI 配置影响。
语法与代码示例
基本连接检测
php
<?php
// 检测用户是否断开连接
set_time_limit(0);
// 忽略用户断开,脚本继续执行
ignore_user_abort(true);
echo "开始处理...\n";
for ($i = 0; $i < 100; $i++) {
// 检测连接状态
if (connection_aborted()) {
// 用户已断开,可以选择停止或继续
error_log("用户在步骤 {$i} 断开连接");
// break; // 如果需要停止
}
// 执行耗时操作
sleep(1);
echo "步骤 {$i} 完成\n";
// 发送输出刷新缓冲区以检测断开
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
echo "处理完成\n";ignore_user_abort
php
<?php
// ignore_user_abort(true) - 用户断开后脚本继续执行
// ignore_user_abort(false) - 用户断开后脚本立即中止(默认)
$oldValue = ignore_user_abort(true);
echo "之前设置: {$oldValue}\n";
// 注册关闭函数,即使脚本被中止也会执行
register_shutdown_function(function () {
error_log("脚本关闭,连接状态: " . connection_status());
});
// 模拟长时间任务
for ($i = 0; $i < 60; $i++) {
sleep(1);
}提前关闭连接
php
<?php
// 先关闭输出缓冲
while (ob_get_level()) {
ob_end_clean();
}
// 发送响应头和内容
header('Content-Type: application/json');
header('Connection: close'); // 通知客户端连接已关闭
echo json_encode(['status' => 'accepted', 'message' => '任务已开始处理']);
// 确保内容发送到客户端
$size = ob_get_length() ?: 0;
header("Content-Length: {$size}");
ob_end_flush();
flush();
// 设置脚本继续执行
ignore_user_abort(true);
set_time_limit(0);
// 后台继续处理任务
sleep(5);
error_log("后台任务完成");
// 执行后续处理...
// 这些操作对用户是不可见的Connection: close 模式
php
<?php
/**
* 异步任务处理模式
* 先响应客户端,再执行耗时任务
*/
function asyncProcess(callable $callback): void
{
// 关闭所有输出缓冲
while (ob_get_level()) {
ob_end_clean();
}
// 发送响应
header('HTTP/1.1 202 Accepted');
header('Content-Type: application/json');
header('Connection: close');
header('Content-Length: 2');
echo '{}';
// 确保立即发送
flush();
// 继续执行后台任务
ignore_user_abort(true);
set_time_limit(0);
$callback();
}
// 使用
asyncProcess(function () {
// 模拟耗时任务
$start = microtime(true);
sleep(10);
$elapsed = round(microtime(true) - $start, 2);
error_log("后台任务完成,耗时: {$elapsed}s");
});实战示例
大文件导出
php
<?php
declare(strict_types=1);
class LargeExportHandler
{
/**
* 流式导出 CSV,支持连接断开检测
*/
public function exportCsv(string $query, string $filename): void
{
set_time_limit(0);
ignore_user_abort(false); // 用户断开则停止
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: no-cache');
// 输出 BOM
echo "\xEF\xBB\xBF";
// 分批查询数据库
$offset = 0;
$limit = 1000;
while (true) {
if (connection_aborted()) {
error_log("用户断开连接,导出中止于 offset={$offset}");
break;
}
$rows = $this->fetchBatch($query, $offset, $limit);
if (empty($rows)) {
break;
}
foreach ($rows as $row) {
fputcsv(STDOUT, $row);
}
$offset += $limit;
// 刷新输出缓冲
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
error_log("导出完成: {$offset} 条记录");
}
private function fetchBatch(string $query, int $offset, int $limit): array
{
// 数据库查询实现
return [];
}
}
// 使用
$exporter = new LargeExportHandler();
$exporter->exportCsv('SELECT * FROM orders', 'orders_' . date('Ymd') . '.csv');任务队列处理器
php
<?php
declare(strict_types=1);
class BackgroundTaskRunner
{
/**
* 启动后台任务
*/
public static function run(callable $task): void
{
// 先关闭所有缓冲
while (ob_get_level()) {
ob_end_clean();
}
// 发送空响应
http_response_code(200);
header('Content-Type: application/json');
header('Connection: close');
header('Content-Length: 17');
echo '{"status":"ok"}';
flush();
// 确保脚本继续运行
ignore_user_abort(true);
set_time_limit(0);
// 关闭当前会话(如果使用了 session)
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
// 执行后台任务
try {
$task();
} catch (Throwable $e) {
error_log("后台任务错误: {$e->getMessage()}");
}
}
}
// 使用
BackgroundTaskRunner::run(function () {
$start = time();
// 发送邮件
$recipients = ['user1@example.com', 'user2@example.com', 'user3@example.com'];
foreach ($recipients as $email) {
mail($email, '通知', '内容');
sleep(2);
}
$elapsed = time() - $start;
error_log("邮件群发完成,耗时 {$elapsed}s");
});注意事项
输出缓冲的影响
php
<?php
// connection_aborted() 需要脚本尝试输出后才能检测到断开
// 如果有输出缓冲,需要先 flush
// 不好的做法(无法及时检测断开)
for ($i = 0; $i < 100; $i++) {
// 什么都不输出
if (connection_aborted()) { ... }
sleep(1);
}
// 好的做法(输出空格或定期 flush)
for ($i = 0; $i < 100; $i++) {
echo " "; // 发送一些数据触发检测
if (ob_get_level() > 0) ob_flush();
flush();
if (connection_aborted()) { ... }
sleep(1);
}register_shutdown_function
php
<?php
// 关闭函数始终会执行,无论是否调用了 exit() 或超时
register_shutdown_function(function () {
// 清理资源
$status = connection_status();
if ($status === CONNECTION_TIMEOUT) {
error_log("脚本超时退出");
} elseif ($status === CONNECTION_ABORTED) {
error_log("用户断开连接");
} else {
error_log("脚本正常结束");
}
});最佳实践
1. 长任务应使用队列
php
<?php
// 不好:在 HTTP 请求中执行长任务
ignore_user_abort(true);
set_time_limit(0);
// 执行 10 分钟的任务...
// 好:将任务放入队列,立即返回
$taskId = pushToQueue('export', ['type' => 'csv', 'filters' => $filters]);
echo json_encode(['task_id' => $taskId]);
// 独立 worker 进程处理
// php worker.php2. 连接处理的正确使用
php
<?php
// 确保资源清理
$lock = acquireLock('export_lock');
register_shutdown_function(function () use ($lock) {
releaseLock($lock);
});
// 执行任务...进阶用法
调试与测试技巧
php
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
php
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
php
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
php
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');