PHP curl_multi_add_handle函数

定义和用法

curl_multi_add_handle() 函数用于将一个标准的cURL句柄添加到cURL多句柄中。这是实现并行HTTP请求的关键步骤,只有添加到多句柄的cURL句柄才能通过curl_multi_exec()并行执行。

提示: 单个cURL句柄在添加到多句柄后,不应再单独使用curl_exec()执行,而应通过多句柄统一管理。

语法

int curl_multi_add_handle ( resource $mh , resource $ch )

参数

参数 描述 类型 必需
mh curl_multi_init() 返回的cURL多句柄 resource
ch curl_init() 返回的cURL句柄 resource

返回值

成功时返回 0 (CURLM_OK),失败时返回 1 (CURLM_BAD_HANDLE) 或其他错误代码。

cURL多句柄工作流程

1
curl_multi_init()
初始化多句柄
2
curl_init()
创建单个句柄
3
curl_setopt()
设置选项
4
curl_multi_add_handle()
添加句柄
5
curl_multi_exec()
执行请求
6
curl_multi_remove_handle()
移除句柄

示例

示例 1:基本用法 - 添加单个句柄

@php
// 创建cURL多句柄
$multiHandle = curl_multi_init();

// 创建单个cURL句柄
$ch1 = curl_init();
curl_setopt_array($ch1, [
    CURLOPT_URL => "https://httpbin.org/get",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30
]);

// 将单个句柄添加到多句柄
$result = curl_multi_add_handle($multiHandle, $ch1);

if ($result === CURLM_OK) {
    echo "成功将cURL句柄添加到多句柄\n";
} else {
    echo "添加cURL句柄失败,错误代码: " . $result . "\n";
    exit;
}

// 执行多句柄请求
$running = null;
do {
    curl_multi_exec($multiHandle, $running);
    curl_multi_select($multiHandle, 0.1);
} while ($running > 0);

// 获取结果
$content = curl_multi_getcontent($ch1);
echo "请求完成,内容长度: " . strlen($content) . " 字节\n";

// 清理资源
curl_multi_remove_handle($multiHandle, $ch1);
curl_close($ch1);
curl_multi_close($multiHandle);
@endphp

示例 2:批量添加多个句柄

@php
// 初始化多句柄
$mh = curl_multi_init();

// URL列表
$urls = [
    'https://httpbin.org/json',
    'https://httpbin.org/xml',
    'https://httpbin.org/headers',
    'https://httpbin.org/uuid'
];

$handles = [];
$addedCount = 0;

echo "开始添加 " . count($urls) . " 个cURL句柄到多句柄...\n";

// 为每个URL创建并添加句柄
foreach ($urls as $index => $url) {
    // 创建单个cURL句柄
    $handles[$index] = curl_init();

    // 设置选项
    curl_setopt_array($handles[$index], [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_USERAGENT => 'ParallelRequest/1.0'
    ]);

    // 添加到多句柄
    $addResult = curl_multi_add_handle($mh, $handles[$index]);

    if ($addResult === CURLM_OK) {
        $addedCount++;
        echo "✓ 成功添加句柄 {$index}: {$url}\n";
    } else {
        echo "✗ 添加句柄 {$index} 失败: {$url} (错误代码: {$addResult})\n";
    }
}

echo "\n总共成功添加 {$addedCount}/" . count($urls) . " 个句柄\n";

if ($addedCount > 0) {
    echo "\n开始执行并行请求...\n";

    // 执行所有请求
    $running = null;
    do {
        $status = curl_multi_exec($mh, $running);
        if ($running) {
            curl_multi_select($mh, 0.1);
        }
    } while ($running > 0 && $status === CURLM_OK);

    // 处理结果
    $successCount = 0;
    foreach ($handles as $index => $handle) {
        $content = curl_multi_getcontent($handle);
        $info = curl_getinfo($handle);

        if ($content !== false && $info['http_code'] === 200) {
            $successCount++;
            echo "✓ 请求 {$index} 成功: HTTP {$info['http_code']}, 长度: " . strlen($content) . " 字节\n";
        } else {
            $error = curl_error($handle);
            echo "✗ 请求 {$index} 失败: {$error}\n";
        }

        // 移除句柄
        curl_multi_remove_handle($mh, $handle);
        curl_close($handle);
    }

    echo "\n请求完成: {$successCount}/{$addedCount} 成功\n";
}

// 关闭多句柄
curl_multi_close($mh);
@endphp

示例 3:带错误处理和验证的添加过程

@php
class MultiHandleManager {
    private $multiHandle;
    private $activeHandles = [];

    public function __construct() {
        $this->multiHandle = curl_multi_init();
        if ($this->multiHandle === false) {
            throw new Exception('无法初始化cURL多句柄');
        }
    }

    /**
     * 添加cURL句柄到多句柄
     * @param resource $ch cURL句柄
     * @param string $identifier 句柄标识符
     * @return bool 是否添加成功
     */
    public function addHandle($ch, $identifier = null) {
        // 验证参数
        if (!is_resource($ch) || get_resource_type($ch) !== 'curl') {
            throw new InvalidArgumentException('参数必须是有效的cURL句柄资源');
        }

        if (!is_resource($this->multiHandle)) {
            throw new Exception('多句柄未初始化或已关闭');
        }

        // 生成标识符
        if ($identifier === null) {
            $identifier = 'handle_' . count($this->activeHandles);
        }

        // 检查是否已存在相同标识符
        if (isset($this->activeHandles[$identifier])) {
            throw new InvalidArgumentException("标识符 '{$identifier}' 已存在");
        }

        // 添加句柄到多句柄
        $result = curl_multi_add_handle($this->multiHandle, $ch);

        if ($result === CURLM_OK) {
            $this->activeHandles[$identifier] = $ch;
            echo "✓ 成功添加句柄: {$identifier}\n";
            return true;
        } else {
            $errorMsg = $this->getMultiErrorDescription($result);
            echo "✗ 添加句柄失败: {$identifier} ({$errorMsg})\n";
            return false;
        }
    }

    /**
     * 批量添加多个句柄
     */
    public function addMultipleHandles($handles) {
        $results = [];

        foreach ($handles as $identifier => $ch) {
            try {
                $success = $this->addHandle($ch, $identifier);
                $results[$identifier] = $success;
            } catch (Exception $e) {
                $results[$identifier] = false;
                echo "添加句柄 {$identifier} 时出错: " . $e->getMessage() . "\n";
            }
        }

        return $results;
    }

    /**
     * 执行所有请求
     */
    public function executeAll() {
        if (empty($this->activeHandles)) {
            echo "没有活动的句柄需要执行\n";
            return [];
        }

        echo "开始执行 " . count($this->activeHandles) . " 个并行请求...\n";

        $running = null;
        $startTime = microtime(true);

        do {
            $status = curl_multi_exec($this->multiHandle, $running);

            if ($running) {
                // 等待活动
                curl_multi_select($this->multiHandle, 0.1);
            }

            // 处理错误状态
            if ($status !== CURLM_OK) {
                $errorMsg = $this->getMultiErrorDescription($status);
                echo "多句柄执行错误: {$errorMsg}\n";
                break;
            }

        } while ($running > 0);

        $totalTime = microtime(true) - $startTime;
        echo "所有请求完成,总耗时: " . round($totalTime, 3) . " 秒\n";

        return $this->collectResults();
    }

    /**
     * 收集所有请求结果
     */
    private function collectResults() {
        $results = [];

        foreach ($this->activeHandles as $identifier => $ch) {
            $content = curl_multi_getcontent($ch);
            $info = curl_getinfo($ch);
            $error = curl_error($ch);
            $errno = curl_errno($ch);

            $results[$identifier] = [
                'success' => ($errno === 0 && $content !== false),
                'content' => $content,
                'info' => $info,
                'error' => $error,
                'errno' => $errno,
                'content_length' => strlen($content)
            ];

            // 从多句柄中移除
            curl_multi_remove_handle($this->multiHandle, $ch);
        }

        return $results;
    }

    /**
     * 获取错误代码描述
     */
    private function getMultiErrorDescription($errorCode) {
        $descriptions = [
            CURLM_OK => '操作成功',
            CURLM_BAD_HANDLE => '无效的多句柄',
            CURLM_BAD_EASY_HANDLE => '无效的简单句柄',
            CURLM_OUT_OF_MEMORY => '内存不足',
            CURLM_INTERNAL_ERROR => '内部错误',
            CURLM_BAD_SOCKET => '无效的socket',
            CURLM_UNKNOWN_OPTION => '未知选项',
            CURLM_ADDED_ALREADY => '句柄已添加'
        ];

        return $descriptions[$errorCode] ?? "未知错误代码: {$errorCode}";
    }

    /**
     * 清理资源
     */
    public function close() {
        // 关闭所有单个句柄
        foreach ($this->activeHandles as $ch) {
            if (is_resource($ch)) {
                curl_multi_remove_handle($this->multiHandle, $ch);
                curl_close($ch);
            }
        }

        // 关闭多句柄
        if (is_resource($this->multiHandle)) {
            curl_multi_close($this->multiHandle);
        }

        $this->activeHandles = [];
        echo "所有资源已清理\n";
    }

    public function __destruct() {
        $this->close();
    }
}

// 使用MultiHandleManager
try {
    $manager = new MultiHandleManager();

    // 创建多个cURL句柄
    $handles = [];

    $handles['api_json'] = curl_init('https://httpbin.org/json');
    curl_setopt($handles['api_json'], CURLOPT_RETURNTRANSFER, true);

    $handles['api_xml'] = curl_init('https://httpbin.org/xml');
    curl_setopt($handles['api_xml'], CURLOPT_RETURNTRANSFER, true);

    $handles['api_headers'] = curl_init('https://httpbin.org/headers');
    curl_setopt($handles['api_headers'], CURLOPT_RETURNTRANSFER, true);

    // 添加句柄到多句柄
    $addResults = $manager->addMultipleHandles($handles);

    echo "\n添加结果统计:\n";
    $successCount = count(array_filter($addResults));
    $totalCount = count($addResults);
    echo "成功: {$successCount}/{$totalCount}\n";

    // 执行所有请求
    if ($successCount > 0) {
        $results = $manager->executeAll();

        echo "\n请求结果:\n";
        foreach ($results as $identifier => $result) {
            $status = $result['success'] ? '✓ 成功' : '✗ 失败';
            $size = $result['content_length'] . ' 字节';
            $time = round($result['info']['total_time'], 3) . ' 秒';
            echo "{$identifier}: {$status} | 大小: {$size} | 耗时: {$time}\n";
        }
    }

    // 显式关闭(可选,析构函数也会处理)
    $manager->close();

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
@endphp

示例 4:动态添加句柄与进度监控

@php
class DynamicMultiRequest {
    private $multiHandle;
    private $activeHandles = [];
    private $completedHandles = [];
    private $maxConcurrent;

    public function __construct($maxConcurrent = 5) {
        $this->multiHandle = curl_multi_init();
        $this->maxConcurrent = $maxConcurrent;
    }

    /**
     * 动态添加请求
     */
    public function addRequest($url, $options = [], $id = null) {
        if ($id === null) {
            $id = uniqid('req_');
        }

        $ch = curl_init();

        $defaultOptions = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_NOPROGRESS => false,
            CURLOPT_PROGRESSFUNCTION => function($resource, $download_size, $downloaded, $upload_size, $uploaded) use ($id) {
                if ($download_size > 0) {
                    $progress = round(($downloaded / $download_size) * 100);
                    // 在实际应用中,这里可以更新进度显示
                    echo "进度 [{$id}]: {$progress}%\r";
                }
                return 0;
            }
        ];

        $finalOptions = $options + $defaultOptions;
        curl_setopt_array($ch, $finalOptions);

        // 添加到多句柄
        $result = curl_multi_add_handle($this->multiHandle, $ch);

        if ($result === CURLM_OK) {
            $this->activeHandles[$id] = [
                'handle' => $ch,
                'url' => $url,
                'added_time' => microtime(true)
            ];
            return $id;
        } else {
            curl_close($ch);
            return false;
        }
    }

    /**
     * 执行所有请求(支持动态添加)
     */
    public function execute($pendingUrls = []) {
        $allUrls = $pendingUrls;
        $processedCount = 0;

        echo "开始执行动态并行请求 (最大并发: {$this->maxConcurrent})...\n\n";

        $startTime = microtime(true);

        while (!empty($this->activeHandles) || !empty($allUrls)) {
            // 动态添加新请求(不超过最大并发数)
            while (count($this->activeHandles) < $this->maxConcurrent && !empty($allUrls)) {
                $url = array_shift($allUrls);
                $id = $this->addRequest($url);
                if ($id) {
                    $processedCount++;
                    echo "已添加请求: {$url} [{$processedCount}]\n";
                }
            }

            if (empty($this->activeHandles)) {
                break;
            }

            // 执行当前活动的请求
            $this->executeActiveRequests();

            // 处理完成的请求
            $this->processCompletedRequests();
        }

        $totalTime = microtime(true) - $startTime;

        echo "\n所有请求完成!\n";
        echo "总请求数: {$processedCount}\n";
        echo "总耗时: " . round($totalTime, 2) . " 秒\n";

        return $this->completedHandles;
    }

    private function executeActiveRequests() {
        $running = null;
        do {
            $status = curl_multi_exec($this->multiHandle, $running);

            if ($running) {
                curl_multi_select($this->multiHandle, 0.1);
            }
        } while ($running > 0 && $status === CURLM_OK);
    }

    private function processCompletedRequests() {
        while ($info = curl_multi_info_read($this->multiHandle)) {
            $handle = $info['handle'];
            $id = $this->findHandleId($handle);

            if ($id !== null) {
                $this->completeRequest($id, $handle, $info);
            }
        }
    }

    private function findHandleId($handle) {
        foreach ($this->activeHandles as $id => $data) {
            if ($data['handle'] === $handle) {
                return $id;
            }
        }
        return null;
    }

    private function completeRequest($id, $handle, $info) {
        $content = curl_multi_getcontent($handle);
        $curlInfo = curl_getinfo($handle);
        $error = curl_error($handle);

        $requestData = $this->activeHandles[$id];
        $processingTime = microtime(true) - $requestData['added_time'];

        $this->completedHandles[$id] = [
            'url' => $requestData['url'],
            'success' => ($info['result'] === CURLE_OK),
            'http_code' => $curlInfo['http_code'],
            'processing_time' => $processingTime,
            'content_length' => strlen($content),
            'error' => $error
        ];

        // 从多句柄中移除
        curl_multi_remove_handle($this->multiHandle, $handle);
        curl_close($handle);

        unset($this->activeHandles[$id]);

        $status = $info['result'] === CURLE_OK ? '完成' : '失败';
        echo "请求完成 [{$id}]: {$status} (" . round($processingTime, 2) . "秒)\n";
    }

    public function close() {
        foreach ($this->activeHandles as $data) {
            if (is_resource($data['handle'])) {
                curl_multi_remove_handle($this->multiHandle, $data['handle']);
                curl_close($data['handle']);
            }
        }

        if (is_resource($this->multiHandle)) {
            curl_multi_close($this->multiHandle);
        }

        $this->activeHandles = [];
        $this->completedHandles = [];
    }

    public function __destruct() {
        $this->close();
    }
}

// 使用动态并行请求
$dynamic = new DynamicMultiRequest(3); // 最大并发3个

// 准备URL列表(模拟20个请求)
$urls = [];
for ($i = 1; $i <= 20; $i++) {
    $delay = ($i % 4) + 1; // 延迟1-4秒
    $urls[] = "https://httpbin.org/delay/{$delay}";
}

echo "准备处理 " . count($urls) . " 个URL...\n";

$results = $dynamic->execute($urls);

// 统计结果
$successful = array_filter($results, function($r) {
    return $r['success'];
});

echo "\n最终统计:\n";
echo "成功: " . count($successful) . " 个\n";
echo "失败: " . (count($results) - count($successful)) . " 个\n";

// 显示最快的3个请求
usort($results, function($a, $b) {
    return $a['processing_time'] <=> $b['processing_time'];
});

echo "\n最快的3个请求:\n";
for ($i = 0; $i < min(3, count($results)); $i++) {
    $result = $results[$i];
    echo ($i + 1) . ". " . $result['url'] . " (" . round($result['processing_time'], 2) . "秒)\n";
}

$dynamic->close();
@endphp

常见错误与解决方案

错误情况 错误描述 解决方案
无效的多句柄 传递的多句柄参数不是有效的资源 确保使用curl_multi_init()正确初始化多句柄,并检查资源是否有效
无效的cURL句柄 传递的cURL句柄参数不是有效的资源 确保使用curl_init()正确初始化cURL句柄,并检查资源类型
句柄已添加 同一个cURL句柄被多次添加到多句柄 每个cURL句柄只能添加到一个多句柄一次,使用唯一标识符跟踪
内存不足 系统内存不足,无法添加新句柄 减少并发数量,优化内存使用,或增加系统内存
句柄已执行 尝试添加已经执行过的cURL句柄 为每个请求创建新的cURL句柄,不要重用已执行的句柄

最佳实践

验证资源有效性

在添加句柄前,确保多句柄和cURL句柄都是有效的资源。

使用标识符

为每个句柄分配唯一标识符,便于后续管理和错误追踪。

错误处理

始终检查curl_multi_add_handle()的返回值,处理可能的错误。

资源清理

确保在请求完成后正确移除和关闭所有句柄,防止资源泄漏。

注意事项

重要:
  • cURL句柄在添加到多句柄后,不应再单独使用curl_exec()执行
  • 同一个cURL句柄不能同时添加到多个多句柄中
  • 添加句柄后,必须调用curl_multi_exec()来实际执行请求
  • 请求完成后,必须使用curl_multi_remove_handle()移除句柄
  • 在多句柄关闭前,必须先移除所有已添加的句柄
  • 大量并发请求时,注意系统资源限制和网络带宽