Skip to content

连接处理

概述

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_NORMAL0正常连接
CONNECTION_ABORTED1用户断开
CONNECTION_TIMEOUT2超时

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.php

2. 连接处理的正确使用

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 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 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');

参考链接