PHP curl_multi_getcontent函数

定义和用法

curl_multi_getcontent() 函数用于获取cURL多句柄中单个句柄的响应内容。这个函数在并行请求处理完成后,用于提取每个请求的返回数据。

提示: 此函数应该在请求执行完成后调用,通常与curl_multi_info_read()配合使用来获取已完成请求的内容。

语法

string curl_multi_getcontent ( resource $ch )

参数

参数 描述 类型 必需
ch curl_init() 返回的cURL句柄,该句柄必须已添加到多句柄并执行完成 resource

返回值

返回cURL句柄的响应内容字符串。如果请求失败或没有设置CURLOPT_RETURNTRANSFER选项,返回FALSE

内容获取工作流程

1
curl_multi_exec()
执行多句柄
2
curl_multi_info_read()
读取完成信息
3
curl_multi_getcontent()
获取响应内容
4
数据处理
解析和应用

示例

示例 1:基本用法 - 获取并行请求内容

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

// 创建多个cURL句柄
$handles = [];
$urls = [
    'https://httpbin.org/json',
    'https://httpbin.org/xml',
    'https://httpbin.org/html'
];

foreach ($urls as $i => $url) {
    $handles[$i] = curl_init();
    curl_setopt_array($handles[$i], [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true, // 必须设置为true才能获取内容
        CURLOPT_TIMEOUT => 30
    ]);
    curl_multi_add_handle($mh, $handles[$i]);
}

// 执行并行请求
$running = null;
do {
    curl_multi_exec($mh, $running);
    curl_multi_select($mh, 0.1);
} while ($running > 0);

echo "所有请求执行完成,开始获取内容...\n\n";

// 获取每个请求的内容
foreach ($handles as $i => $handle) {
    // 使用curl_multi_getcontent获取响应内容
    $content = curl_multi_getcontent($handle);

    if ($content !== false) {
        $info = curl_getinfo($handle);
        $contentType = $info['content_type'] ?? 'unknown';
        $contentLength = strlen($content);

        echo "请求 {$i} 内容获取成功:\n";
        echo "  URL: {$urls[$i]}\n";
        echo "  内容类型: {$contentType}\n";
        echo "  内容长度: {$contentLength} 字节\n";
        echo "  前100字符: " . substr($content, 0, 100) . "...\n\n";
    } else {
        $error = curl_error($handle);
        echo "请求 {$i} 内容获取失败: {$error}\n\n";
    }

    // 清理资源
    curl_multi_remove_handle($mh, $handle);
    curl_close($handle);
}

curl_multi_close($mh);
@endphp

示例 2:实时处理完成请求的内容

@php
class RealTimeContentProcessor {
    private $multiHandle;
    private $handles = [];
    private $results = [];

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

    public function addRequest($url, $id = null) {
        if ($id === null) {
            $id = uniqid('req_');
        }

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true, // 关键:必须设置为true
            CURLOPT_TIMEOUT => 30
        ]);

        $this->handles[$id] = $ch;
        curl_multi_add_handle($this->multiHandle, $ch);

        return $id;
    }

    public function execute() {
        echo "开始执行并行请求...\n";

        $active = null;

        do {
            // 执行多句柄
            $status = curl_multi_exec($this->multiHandle, $active);

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

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

        } while ($active > 0);

        // 处理可能剩余的任何请求
        $this->processCompletedRequests(true);

        echo "所有请求处理完成!\n";
        return $this->results;
    }

    private function processCompletedRequests($final = false) {
        // 处理所有已完成的请求
        while ($info = curl_multi_info_read($this->multiHandle)) {
            $handle = $info['handle'];
            $id = array_search($handle, $this->handles, true);

            if ($id !== false) {
                $this->processSingleContent($id, $handle, $info);
                unset($this->handles[$id]);
            }
        }
    }

    private function processSingleContent($id, $handle, $info) {
        // 使用curl_multi_getcontent获取响应内容
        $content = curl_multi_getcontent($handle);
        $curlInfo = curl_getinfo($handle);
        $error = curl_error($handle);

        $result = [
            'success' => ($info['result'] === CURLE_OK && $content !== false),
            'content' => $content,
            'info' => $curlInfo,
            'error' => $error,
            'content_length' => strlen($content),
            'processed_at' => microtime(true)
        ];

        // 根据内容类型进行不同处理
        $this->processContentByType($id, $result);

        // 存储结果
        $this->results[$id] = $result;

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

        echo "请求 {$id} 处理完成 (" . round($curlInfo['total_time'], 2) . "秒)\n";
    }

    private function processContentByType($id, &$result) {
        $contentType = $result['info']['content_type'] ?? '';
        $content = $result['content'];

        if (strpos($contentType, 'application/json') !== false) {
            // JSON内容处理
            $data = json_decode($content, true);
            if (json_last_error() === JSON_ERROR_NONE) {
                $result['parsed_data'] = $data;
                $result['data_type'] = 'json';
                echo "  [{$id}] JSON数据解析成功,包含 " . count($data) . " 个字段\n";
            } else {
                $result['parse_error'] = 'JSON解析失败: ' . json_last_error_msg();
            }
        } elseif (strpos($contentType, 'application/xml') !== false || strpos($contentType, 'text/xml') !== false) {
            // XML内容处理
            $xml = simplexml_load_string($content);
            if ($xml !== false) {
                $result['parsed_data'] = $xml;
                $result['data_type'] = 'xml';
                echo "  [{$id}] XML数据解析成功\n";
            } else {
                $result['parse_error'] = 'XML解析失败';
            }
        } elseif (strpos($contentType, 'text/html') !== false) {
            // HTML内容处理
            $result['data_type'] = 'html';
            $title = preg_match('/ <title>(.*?)<\/title>/i', $content, $matches) ? $matches[1] : '无标题';
            $result['page_title'] = $title;
            echo "  [{$id}] HTML页面标题: {$title}\n";
        } else {
            $result['data_type'] = 'raw';
            echo "  [{$id}] 原始内容,类型: {$contentType}\n";
        }
    }

    public function getResults() {
        return $this->results;
    }

    public function close() {
        foreach ($this->handles as $handle) {
            if (is_resource($handle)) {
                curl_multi_remove_handle($this->multiHandle, $handle);
                curl_close($handle);
            }
        }

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

        $this->handles = [];
        $this->results = [];
    }

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

// 使用实时内容处理器
$processor = new RealTimeContentProcessor();

// 添加不同类型的API请求
$requests = [
    'json_api' => 'https://httpbin.org/json',
    'xml_api' => 'https://httpbin.org/xml',
    'html_page' => 'https://httpbin.org/html',
    'uuid_api' => 'https://httpbin.org/uuid'
];

foreach ($requests as $id => $url) {
    $processor->addRequest($url, $id);
}

// 执行并处理所有请求
$results = $processor->execute();

// 显示统计信息
echo "\n内容处理统计:\n";
$successCount = 0;
$totalSize = 0;

foreach ($results as $id => $result) {
    if ($result['success']) {
        $successCount++;
        $totalSize += $result['content_length'];
        echo "✓ {$id}: {$result['data_type']}, {$result['content_length']} 字节\n";
    } else {
        echo "✗ {$id}: 失败 - {$result['error']}\n";
    }
}

echo "\n总结: {$successCount}/" . count($requests) . " 成功, 总数据量: " . round($totalSize / 1024, 2) . " KB\n";

$processor->close();
@endphp

示例 3:大数据量处理和内存优化

@php
class LargeContentHandler {
    private $multiHandle;
    private $handles = [];
    private $outputDir;

    public function __construct($outputDir = null) {
        $this->multiHandle = curl_multi_init();
        $this->outputDir = $outputDir ?: sys_get_temp_dir() . '/curl_downloads';

        // 创建输出目录
        if (!is_dir($this->outputDir)) {
            mkdir($this->outputDir, 0755, true);
        }
    }

    public function addDownload($url, $filename = null, $id = null) {
        if ($id === null) {
            $id = uniqid('dl_');
        }

        if ($filename === null) {
            $filename = $id . '_' . basename(parse_url($url, PHP_URL_PATH) ?: 'download');
        }

        $outputPath = $this->outputDir . '/' . $filename;

        $ch = curl_init();

        // 对于大文件,使用文件流而不是内存存储
        $fileHandle = fopen($outputPath, 'w');
        if ($fileHandle === false) {
            throw new Exception("无法创建文件: {$outputPath}");
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_FILE => $fileHandle, // 直接写入文件,避免内存占用
            CURLOPT_TIMEOUT => 60,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_NOPROGRESS => false,
            CURLOPT_PROGRESSFUNCTION => function($resource, $download_size, $downloaded, $upload_size, $uploaded) use ($id) {
                if ($download_size > 0) {
                    $percent = round(($downloaded / $download_size) * 100);
                    echo "下载进度 [{$id}]: {$percent}% ({$downloaded}/{$download_size} 字节)\n";
                }
                return 0;
            }
        ]);

        $this->handles[$id] = [
            'handle' => $ch,
            'file_handle' => $fileHandle,
            'output_path' => $outputPath,
            'url' => $url
        ];

        curl_multi_add_handle($this->multiHandle, $ch);

        return $id;
    }

    public function execute() {
        echo "开始并行下载...\n";
        echo "输出目录: {$this->outputDir}\n\n";

        $startMemory = memory_get_usage(true);
        $active = null;

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

            // 处理完成的下载
            $this->processCompletedDownloads();

            if ($active > 0) {
                curl_multi_select($this->multiHandle, 0.1);
            }

            // 监控内存使用
            $currentMemory = memory_get_usage(true);
            $memoryUsageMB = round($currentMemory / 1024 / 1024, 2);
            echo "当前内存使用: {$memoryUsageMB} MB\n";

        } while ($active > 0);

        $this->processCompletedDownloads(true);

        $endMemory = memory_get_usage(true);
        $memoryIncrease = round(($endMemory - $startMemory) / 1024 / 1024, 2);
        echo "\n下载完成!内存增加: {$memoryIncrease} MB\n";

        return $this->getDownloadResults();
    }

    private function processCompletedDownloads($final = false) {
        while ($info = curl_multi_info_read($this->multiHandle)) {
            $handle = $info['handle'];
            $id = $this->findDownloadId($handle);

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

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

    private function finalizeDownload($id, $handle, $info) {
        $downloadData = $this->handles[$id];

        // 关闭文件句柄
        if (is_resource($downloadData['file_handle'])) {
            fclose($downloadData['file_handle']);
        }

        $curlInfo = curl_getinfo($handle);
        $error = curl_error($handle);
        $fileSize = file_exists($downloadData['output_path']) ? filesize($downloadData['output_path']) : 0;

        $result = [
            'success' => ($info['result'] === CURLE_OK && $fileSize > 0),
            'file_path' => $downloadData['output_path'],
            'file_size' => $fileSize,
            'url' => $downloadData['url'],
            'error' => $error,
            'http_code' => $curlInfo['http_code'],
            'download_time' => $curlInfo['total_time']
        ];

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

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

        $status = $result['success'] ? '成功' : '失败';
        $size = round($fileSize / 1024, 2) . ' KB';
        echo "下载 {$id}: {$status}, 文件大小: {$size}, 耗时: " . round($result['download_time'], 2) . "秒\n";

        return $result;
    }

    private function getDownloadResults() {
        $results = [];

        // 处理可能剩余的任何句柄
        foreach ($this->handles as $id => $data) {
            if (is_resource($data['file_handle'])) {
                fclose($data['file_handle']);
            }

            $fileSize = file_exists($data['output_path']) ? filesize($data['output_path']) : 0;

            $results[$id] = [
                'success' => false,
                'file_path' => $data['output_path'],
                'file_size' => $fileSize,
                'url' => $data['url'],
                'error' => '未完成下载'
            ];

            curl_multi_remove_handle($this->multiHandle, $data['handle']);
            curl_close($data['handle']);
        }

        $this->handles = [];
        return $results;
    }

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

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

        $this->handles = [];
    }

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

// 使用大内容处理器下载文件
$downloader = new LargeContentHandler();

// 添加多个文件下载(使用小文件作为示例)
$downloads = [
    'image1' => ['url' => 'https://httpbin.org/image/jpeg', 'filename' => 'sample1.jpg'],
    'image2' => ['url' => 'https://httpbin.org/image/png', 'filename' => 'sample2.png'],
    'json_data' => ['url' => 'https://httpbin.org/json', 'filename' => 'data.json']
];

foreach ($downloads as $id => $download) {
    $downloader->addDownload($download['url'], $download['filename'], $id);
}

$results = $downloader->execute();

// 显示下载结果
echo "\n下载结果:\n";
foreach ($results as $id => $result) {
    if ($result['success']) {
        echo "✓ {$id}: 下载成功, 保存到: {$result['file_path']}\n";
    } else {
        echo "✗ {$id}: 下载失败 - {$result['error']}\n";
    }
}

$downloader->close();
@endphp

示例 4:内容缓存与性能优化

@php
class CachedContentFetcher {
    private $multiHandle;
    private $cacheDir;
    private $cacheTtl;

    public function __construct($cacheDir = null, $cacheTtl = 3600) {
        $this->multiHandle = curl_multi_init();
        $this->cacheDir = $cacheDir ?: sys_get_temp_dir() . '/curl_cache';
        $this->cacheTtl = $cacheTtl;

        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
    }

    public function fetchUrls($urls, $forceRefresh = false) {
        $results = [];
        $toFetch = [];

        echo "开始处理 " . count($urls) . " 个URL...\n";

        // 检查缓存
        foreach ($urls as $id => $url) {
            $cacheKey = $this->getCacheKey($url);
            $cacheFile = $this->cacheDir . '/' . $cacheKey;

            if (!$forceRefresh && $this->isCacheValid($cacheFile)) {
                // 使用缓存内容
                $cachedContent = file_get_contents($cacheFile);
                $cacheInfo = json_decode(file_get_contents($cacheFile . '.meta'), true);

                $results[$id] = [
                    'source' => 'cache',
                    'content' => $cachedContent,
                    'cached_at' => $cacheInfo['cached_at'],
                    'url' => $url
                ];

                echo "✓ {$id}: 使用缓存 (" . round(strlen($cachedContent) / 1024, 2) . " KB)\n";
            } else {
                // 需要重新获取
                $toFetch[$id] = $url;
            }
        }

        echo "缓存命中: " . (count($urls) - count($toFetch)) . "/" . count($urls) . "\n";

        // 获取未缓存的内容
        if (!empty($toFetch)) {
            $fetchResults = $this->fetchMultiple($toFetch);

            // 缓存新获取的内容
            foreach ($fetchResults as $id => $result) {
                if ($result['success']) {
                    $this->saveToCache($result['url'], $result['content']);
                    $result['source'] = 'network';
                }
                $results[$id] = $result;
            }
        }

        return $results;
    }

    private function fetchMultiple($urls) {
        $handles = [];
        $results = [];

        echo "开始并行获取 " . count($urls) . " 个URL...\n";

        // 创建并添加句柄
        foreach ($urls as $id => $url) {
            $handles[$id] = curl_init();
            curl_setopt_array($handles[$id], [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_FOLLOWLOCATION => true
            ]);
            curl_multi_add_handle($this->multiHandle, $handles[$id]);
        }

        // 执行并行请求
        $active = null;
        do {
            curl_multi_exec($this->multiHandle, $active);
            curl_multi_select($this->multiHandle, 0.1);
        } while ($active > 0);

        // 获取内容
        foreach ($handles as $id => $handle) {
            $content = curl_multi_getcontent($handle);
            $info = curl_getinfo($handle);
            $error = curl_error($handle);

            $results[$id] = [
                'success' => ($content !== false),
                'content' => $content,
                'url' => $urls[$id],
                'info' => $info,
                'error' => $error,
                'content_length' => strlen($content)
            ];

            curl_multi_remove_handle($this->multiHandle, $handle);
            curl_close($handle);

            $status = $results[$id]['success'] ? '成功' : '失败';
            echo "  {$id}: {$status} (" . round($info['total_time'], 2) . "秒)\n";
        }

        return $results;
    }

    private function getCacheKey($url) {
        return md5($url) . '.cache';
    }

    private function isCacheValid($cacheFile) {
        if (!file_exists($cacheFile) || !file_exists($cacheFile . '.meta')) {
            return false;
        }

        $cacheTime = filemtime($cacheFile);
        return (time() - $cacheTime) < $this->cacheTtl;
    }

    private function saveToCache($url, $content) {
        $cacheKey = $this->getCacheKey($url);
        $cacheFile = $this->cacheDir . '/' . $cacheKey;

        // 保存内容
        file_put_contents($cacheFile, $content);

        // 保存元数据
        $meta = [
            'url' => $url,
            'cached_at' => time(),
            'content_length' => strlen($content),
            'content_type' => 'unknown' // 在实际应用中可以从curl信息获取
        ];
        file_put_contents($cacheFile . '.meta', json_encode($meta));
    }

    public function clearCache($olderThan = null) {
        $files = glob($this->cacheDir . '/*.cache');
        $cleared = 0;

        foreach ($files as $file) {
            if ($olderThan === null || (time() - filemtime($file)) > $olderThan) {
                unlink($file);
                unlink($file . '.meta'); // 同时删除元数据文件
                $cleared++;
            }
        }

        return $cleared;
    }

    public function getCacheStats() {
        $files = glob($this->cacheDir . '/*.cache');
        $totalSize = 0;

        foreach ($files as $file) {
            $totalSize += filesize($file);
        }

        return [
            'file_count' => count($files),
            'total_size' => $totalSize,
            'total_size_mb' => round($totalSize / 1024 / 1024, 2)
        ];
    }

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

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

// 使用缓存内容获取器
$cacheDir = sys_get_temp_dir() . '/demo_curl_cache';
$fetcher = new CachedContentFetcher($cacheDir, 300); // 5分钟缓存

// 要获取的URL列表
$urls = [
    'api1' => 'https://httpbin.org/json',
    'api2' => 'https://httpbin.org/xml',
    'api3' => 'https://httpbin.org/uuid',
    'api4' => 'https://httpbin.org/headers'
];

echo "=== 第一次获取(将缓存内容)===\n";
$results1 = $fetcher->fetchUrls($urls);

echo "\n=== 第二次获取(应使用缓存)===\n";
$results2 = $fetcher->fetchUrls($urls);

// 显示缓存统计
$stats = $fetcher->getCacheStats();
echo "\n缓存统计:\n";
echo "缓存文件数: " . $stats['file_count'] . "\n";
echo "缓存总大小: " . $stats['total_size_mb'] . " MB\n";

// 显示结果来源统计
$sourceCounts = ['cache' => 0, 'network' => 0];
foreach ($results2 as $result) {
    $sourceCounts[$result['source']]++;
}

echo "\n第二次获取来源:\n";
echo "缓存: {$sourceCounts['cache']}\n";
echo "网络: {$sourceCounts['network']}\n";

// 清理缓存示例
// $cleared = $fetcher->clearCache(60); // 清理1分钟前的缓存
// echo "清理了 {$cleared} 个缓存文件\n";

$fetcher->close();
@endphp

关键注意事项

CURLOPT_RETURNTRANSFER

必须设置CURLOPT_RETURNTRANSFER = true,否则curl_multi_getcontent()将返回FALSE

执行时机

必须在请求执行完成后调用,通常在curl_multi_exec()循环结束后或使用curl_multi_info_read()检测到请求完成时。

内存管理

对于大文件,考虑使用CURLOPT_FILE直接写入文件,避免内存占用过高。

资源清理

获取内容后应及时使用curl_multi_remove_handle()curl_close()清理资源。

常见错误与解决方案

错误现象 可能原因 解决方案
返回 FALSE 未设置 CURLOPT_RETURNTRANSFER 或请求未完成 确保设置 CURLOPT_RETURNTRANSFER = true 并在请求完成后调用
内存耗尽 同时处理大量大文件内容 使用 CURLOPT_FILE 直接写入文件,或限制并发数量
内容不完整 在请求完成前调用函数 使用 curl_multi_info_read() 确保请求已完成
句柄无效 句柄已被关闭或移除 在调用 curl_multi_getcontent() 前不要关闭句柄

最佳实践

  • 始终设置 CURLOPT_RETURNTRANSFER - 这是使用 curl_multi_getcontent() 的前提条件
  • 验证请求完成状态 - 使用 curl_multi_info_read() 确认请求已完成
  • 及时处理内容 - 获取内容后立即处理,避免内存堆积
  • 大文件使用流处理 - 对于大文件,使用 CURLOPT_FILE 直接写入磁盘
  • 错误处理 - 检查 curl_multi_getcontent() 的返回值,处理可能的错误
  • 资源清理 - 获取内容后及时清理 cURL 句柄资源
  • 内容缓存 - 对于重复请求,考虑实现缓存机制提高性能

注意事项

重要:
  • curl_multi_getcontent() 只能在设置了 CURLOPT_RETURNTRANSFER = true 的情况下正常工作
  • 函数应该在请求执行完成后调用,否则可能返回不完整的内容或 FALSE
  • 对于大文件内容,考虑内存限制,使用文件流代替内存存储
  • 同一个 cURL 句柄可以多次调用 curl_multi_getcontent(),返回相同的内容
  • 在调用 curl_multi_remove_handle() 后,仍然可以获取该句柄的内容
  • 但在调用 curl_close() 后,将无法再获取内容