cURL 高级用法
在掌握了 cURL 的基础用法之后,本节将深入讲解 cURL 的高级特性:并发请求(multi_curl)、文件上传下载、回调函数、DNS 缓存、连接池复用等。这些高级技巧能够显著提升应用的性能和灵活性。
前置知识
阅读本节前,建议先了解:cURL 详解
基础概念
为什么需要高级用法
在基础用法中,cURL 每次只能发送一个请求(同步阻塞)。但在实际场景中,我们经常需要:
- 并发请求:同时抓取多个 API 接口,减少总等待时间
- 大文件传输:上传下载 GB 级文件,需要进度监控和断点续传
- 流式处理:边接收边处理数据,避免内存溢出
- 连接复用:多个请求复用同一 TCP 连接,降低握手开销
并发请求(Multi cURL)
curl_multi 基础
curl_multi_* 系列函数允许同时执行多个 cURL 请求:
php
<?php
declare(strict_types=1);
/**
* 并发抓取多个 URL
*/
function fetchUrlsConcurrently(array $urls): array
{
$handles = [];
$multiHandle = curl_multi_init();
// 创建所有 cURL 句柄
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_ENCODING => '',
]);
curl_multi_add_handle($multiHandle, $ch);
$handles[$i] = $ch;
}
// 执行并发请求
$active = null;
do {
$status = curl_multi_exec($multiHandle, $active);
} while ($status === CURLM_CALL_MULTI_PERFORM);
// 等待所有请求完成(阻塞直到至少有一个活动连接发生变化)
do {
$status = curl_multi_exec($multiHandle, $active);
if ($active) {
// 等待活动,避免 CPU 空转
curl_multi_select($multiHandle); // PHP 8.4+ 推荐使用 curl_multi_poll
}
} while ($active && $status === CURLM_OK);
// 收集结果
$results = [];
foreach ($handles as $i => $ch) {
$results[$i] = [
'url' => $urls[$i],
'content' => curl_multi_getcontent($ch),
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'error' => curl_error($ch),
'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
];
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $results;
}
// 使用示例
$urls = [
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/posts/2',
'https://jsonplaceholder.typicode.com/posts/3',
'https://jsonplaceholder.typicode.com/posts/4',
'https://jsonplaceholder.typicode.com/posts/5',
];
$startTime = microtime(true);
$results = fetchUrlsConcurrently($urls);
$elapsed = microtime(true) - $startTime;
foreach ($results as $i => $result) {
echo "URL {$i}: HTTP {$result['http_code']}, 耗时 {$result['time']}s" . PHP_EOL;
}
echo "总耗时: {$elapsed}s" . PHP_EOL;PHP 8.4+ curl_multi_poll
PHP 8.4 引入了 curl_multi_poll(),替代 curl_multi_select(),提供更精确的等待控制:
php
<?php
declare(strict_types=1);
// PHP 8.4+
function fetchWithPoll(array $urls): array
{
$multiHandle = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
curl_multi_add_handle($multiHandle, $ch);
$handles[$i] = $ch;
}
$active = null;
do {
curl_multi_exec($multiHandle, $active);
if ($active) {
// curl_multi_poll 比 curl_multi_select 更高效
curl_multi_poll($multiHandle, 1.0); // 最大等待 1 秒
}
} while ($active);
$results = [];
foreach ($handles as $i => $ch) {
$results[$i] = curl_multi_getcontent($ch);
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $results;
}带回调的并发请求
php
<?php
declare(strict_types=1);
class ConcurrentFetcher
{
/**
* 并发请求并按完成顺序处理
*/
public function fetch(array $requests, callable $onComplete): void
{
$multiHandle = curl_multi_init();
$handles = [];
$contexts = [];
foreach ($requests as $i => $request) {
$ch = curl_init($request['url']);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $request['timeout'] ?? 30,
CURLOPT_HTTPHEADER => $request['headers'] ?? [],
CURLOPT_POST => ($request['method'] ?? 'GET') === 'POST',
CURLOPT_POSTFIELDS => json_encode($request['data'] ?? []),
]);
curl_multi_add_handle($multiHandle, $ch);
$handles[$i] = $ch;
$contexts[(int) $ch] = [
'index' => $i,
'url' => $request['url'],
];
}
$active = null;
do {
$status = curl_multi_exec($multiHandle, $active);
if ($status !== CURLM_OK) {
break;
}
// 检查已完成的请求
$info = curl_multi_info_read($multiHandle);
while ($info !== false) {
if ($info['msg'] === CURLMSG_DONE) {
$ch = $info['handle'];
$idx = (int) $ch;
$context = $contexts[$idx] ?? ['index' => 0, 'url' => ''];
$result = [
'index' => $context['index'],
'url' => $context['url'],
'content' => curl_multi_getcontent($ch),
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'error' => $info['result'] !== CURLM_OK
? curl_error($ch)
: null,
'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
];
// 调用回调处理结果
$onComplete($result);
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
unset($handles[$context['index']]);
}
$info = curl_multi_info_read($multiHandle);
}
if ($active) {
curl_multi_select($multiHandle);
}
} while ($active);
curl_multi_close($multiHandle);
}
}
// 使用示例
$fetcher = new ConcurrentFetcher();
$requests = [
['url' => 'https://jsonplaceholder.typicode.com/posts/1'],
['url' => 'https://jsonplaceholder.typicode.com/posts/2'],
['url' => 'https://jsonplaceholder.typicode.com/posts/3'],
];
$fetcher->fetch($requests, function (array $result): void {
echo "已完成 #{$result['index']}: HTTP {$result['http_code']}" . PHP_EOL;
echo "URL: {$result['url']}" . PHP_EOL;
if ($result['error']) {
echo "错误: {$result['error']}" . PHP_EOL;
}
});并发限制(并发池)
php
<?php
declare(strict_types=1);
/**
* 限制并发数量的批量请求
*/
class ConcurrencyPool
{
private int $concurrency;
public function __construct(int $concurrency = 5)
{
$this->concurrency = $concurrency;
}
/**
* 分批并发请求
*/
public function fetchAll(array $urls): array
{
$allResults = [];
$chunks = array_chunk($urls, $this->concurrency);
foreach ($chunks as $chunk) {
$results = $this->fetchBatch($chunk);
$allResults = array_merge($allResults, $results);
}
return $allResults;
}
private function fetchBatch(array $urls): array
{
$multiHandle = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_ENCODING => '',
]);
curl_multi_add_handle($multiHandle, $ch);
$handles[$i] = $ch;
}
$active = null;
do {
curl_multi_exec($multiHandle, $active);
if ($active) {
curl_multi_select($multiHandle);
}
} while ($active);
$results = [];
foreach ($handles as $i => $ch) {
$results[$i] = [
'url' => $urls[$i],
'content' => curl_multi_getcontent($ch),
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'error' => curl_error($ch),
];
curl_multi_remove_handle($multiHandle, $ch);
curl_close($ch);
}
curl_multi_close($multiHandle);
return $results;
}
}
// 示例:100 个 URL,每次最多 10 个并发
$pool = new ConcurrencyPool(10);
$urls = array_fill(0, 100, 'https://jsonplaceholder.typicode.com/posts/1');
$results = $pool->fetchAll($urls);
echo "成功: " . count(array_filter($results, fn($r) => $r['http_code'] === 200)) . PHP_EOL;文件上传
上传文件(multipart/form-data)
php
<?php
declare(strict_types=1);
/**
* 上传文件到服务器
*/
function uploadFile(
string $url,
string $filePath,
string $fileField = 'file',
array $extraFields = [],
array $headers = []
): array {
if (!file_exists($filePath)) {
throw new RuntimeException("文件不存在: {$filePath}");
}
// 使用 CURLFile 对象(PHP 5.5+)
$cfile = new CURLFile(
$filePath,
mime_content_type($filePath),
basename($filePath)
);
$postData = array_merge(
$extraFields,
[$fileField => $cfile]
);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 300, // 上传通常需要更长时间
CURLOPT_HTTPHEADER => array_merge([
'Accept: application/json',
], $headers),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
'error' => $error ?: null,
];
}
// 使用示例
$result = uploadFile(
'https://api.example.com/upload',
'/path/to/document.pdf',
'document',
['description' => '项目报告']
);多文件上传
php
<?php
declare(strict_types=1);
function uploadMultipleFiles(
string $url,
array $files,
array $extraFields = []
): array {
$postData = $extraFields;
foreach ($files as $field => $path) {
if (!file_exists($path)) {
throw new RuntimeException("文件不存在: {$path}");
}
$postData[$field] = new CURLFile(
$path,
mime_content_type($path),
basename($path)
);
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 300,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
];
}
// 示例:同时上传图片和文档
$result = uploadMultipleFiles(
'https://api.example.com/upload/multi',
[
'avatar' => '/path/to/avatar.jpg',
'document' => '/path/to/report.pdf',
],
['user_id' => 123]
);PUT 方式上传文件
php
<?php
declare(strict_types=1);
function uploadFileViaPut(
string $url,
string $filePath,
string $mimeType = 'application/octet-stream'
): array {
if (!file_exists($filePath)) {
throw new RuntimeException("文件不存在: {$filePath}");
}
$fileSize = filesize($filePath);
$fh = fopen($filePath, 'r');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PUT => true,
CURLOPT_INFILE => $fh,
CURLOPT_INFILESIZE => $fileSize,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_UPLOAD => true,
CURLOPT_HTTPHEADER => [
'Content-Type: ' . $mimeType,
'Content-Length: ' . $fileSize,
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
fclose($fh);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
'error' => $error,
];
}文件下载
基本下载
php
<?php
declare(strict_types=1);
/**
* 下载文件到指定路径
*/
function downloadFile(string $url, string $savePath): void
{
$dir = dirname($savePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$fp = fopen($savePath, 'w');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_FILE => $fp, // 将响应写入文件
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_BINARYTRANSFER => true, // 二进制传输
CURLOPT_TIMEOUT => 300,
]);
$success = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
fclose($fp);
if (!$success || $httpCode !== 200) {
@unlink($savePath);
throw new RuntimeException(
"下载失败 [HTTP {$httpCode}]: {$error}"
);
}
}
// 使用示例
try {
downloadFile(
'https://example.com/large-file.zip',
'/tmp/downloads/file.zip'
);
echo "下载完成" . PHP_EOL;
} catch (RuntimeException $e) {
echo "下载失败: " . $e->getMessage() . PHP_EOL;
}带进度回调的下载
php
<?php
declare(strict_types=1);
/**
* 带进度显示的文件下载
*/
function downloadWithProgress(
string $url,
string $savePath,
?callable $onProgress = null
): void {
$dir = dirname($savePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$fp = fopen($savePath, 'w');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => function (
$ch,
int $downloadSize,
int $downloaded,
int $uploadSize,
int $uploaded
) use ($onProgress): int {
if ($onProgress !== null) {
$percent = 0;
if ($downloadSize > 0) {
$percent = (int) (($downloaded / $downloadSize) * 100);
}
$onProgress($downloaded, $downloadSize, $percent);
}
return 0; // 返回 0 继续传输,非 0 则中止
},
]);
$success = curl_exec($ch);
curl_close($ch);
fclose($fp);
if (!$success) {
throw new RuntimeException("下载失败");
}
}
// 使用示例
downloadWithProgress(
'https://example.com/large-file.zip',
'/tmp/downloads/file.zip',
function (int $downloaded, int $total, int $percent): void {
$downloadedMB = round($downloaded / 1024 / 1024, 2);
$totalMB = round($total / 1024 / 1024, 2);
echo "\r进度: {$percent}% ({$downloadedMB}/{$totalMB} MB)";
if ($percent >= 100) {
echo PHP_EOL;
}
}
);断点续传
php
<?php
declare(strict_types=1);
/**
* 支持断点续传的文件下载
*/
function downloadWithResume(
string $url,
string $savePath,
?callable $onProgress = null
): void {
$dir = dirname($savePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// 检查已下载的字节数
$resumeFrom = 0;
if (file_exists($savePath)) {
$resumeFrom = filesize($savePath);
}
$fp = fopen($savePath, $resumeFrom > 0 ? 'ab' : 'wb');
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_NOPROGRESS => false,
CURLOPT_BINARYTRANSFER => true,
];
// 设置 Range 头实现断点续传
if ($resumeFrom > 0) {
$options[CURLOPT_RESUME_FROM] = $resumeFrom;
echo "从 {$resumeFrom} 字节处继续下载..." . PHP_EOL;
}
if ($onProgress !== null) {
$options[CURLOPT_PROGRESSFUNCTION] = function (
$ch,
int $downloadSize,
int $downloaded,
int $uploadSize,
int $uploaded
) use ($onProgress, $resumeFrom): int {
$totalDownloaded = $resumeFrom + $downloaded;
$percent = 0;
if ($downloadSize > 0) {
$percent = (int) (($totalDownloaded / ($resumeFrom + $downloadSize)) * 100);
}
$onProgress($totalDownloaded, $resumeFrom + $downloadSize, $percent);
return 0;
};
}
curl_setopt_array($ch, $options);
$success = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
fclose($fp);
if (!$success) {
throw new RuntimeException("下载失败: {$error}");
}
// 206 = Partial Content(断点续传成功)
if ($httpCode !== 200 && $httpCode !== 206) {
throw new RuntimeException("下载失败,HTTP 状态码: {$httpCode}");
}
}回调函数详解
CURLOPT_WRITEFUNCTION 自定义写入
php
<?php
declare(strict_types=1);
/**
* 使用自定义写入函数处理流式响应
*/
function streamRequest(
string $url,
callable $onChunk,
int $chunkSize = 4096
): void {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_WRITEFUNCTION => function ($ch, string $data) use ($onChunk, $chunkSize): int {
$onChunk($data);
return strlen($data); // 必须返回写入的字节数
},
CURLOPT_FOLLOWLOCATION => true,
]);
$success = curl_exec($ch);
curl_close($ch);
if (!$success) {
throw new RuntimeException("流式请求失败");
}
}
// 使用示例:流式处理 JSON Lines 响应
streamRequest('https://api.example.com/stream', function (string $chunk): void {
$lines = explode("\n", trim($chunk));
foreach ($lines as $line) {
if (empty($line)) {
continue;
}
$data = json_decode($line, true);
if ($data !== null) {
echo "收到记录: " . json_encode($data, JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
}
});CURLOPT_READFUNCTION 自定义读取(上传流)
php
<?php
declare(strict_types=1);
/**
* 从生成器动态产生上传数据
*/
function streamUpload(string $url, \Generator $dataGenerator): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_PUT => true,
CURLOPT_READFUNCTION => function ($ch, int $maxLength) use ($dataGenerator): string {
if (!$dataGenerator->valid()) {
return '';
}
$chunk = $dataGenerator->current();
$dataGenerator->next();
return $chunk;
},
CURLOPT_INFILESIZE => -1, // 未知大小
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/octet-stream',
'Transfer-Encoding: chunked',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
];
}
// 生成器示例:逐行产生 CSV 数据
function csvGenerator(): \Generator
{
yield "id,name,email\n";
yield "1,张三,zhangsan@example.com\n";
yield "2,李四,lisi@example.com\n";
yield "3,王五,wangwu@example.com\n";
}
$result = streamUpload('https://api.example.com/import', csvGenerator());CURLOPT_HEADERFUNCTION
php
<?php
declare(strict_types=1);
function requestWithHeaderCallback(string $url): array
{
$headers = [];
$statusCode = 0;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADERFUNCTION => function ($ch, string $headerLine) use (&$headers, &$statusCode): int {
$len = strlen($headerLine);
// 解析 HTTP 状态行
if (preg_match('/^HTTP\/\S+\s+(\d+)/', $headerLine, $matches)) {
$statusCode = (int) $matches[1];
return $len;
}
// 解析头部键值对
$colonPos = strpos($headerLine, ':');
if ($colonPos !== false) {
$key = strtolower(trim(substr($headerLine, 0, $colonPos)));
$value = trim(substr($headerLine, $colonPos + 1));
$headers[$key] = $value;
}
return $len;
},
]);
$body = curl_exec($ch);
curl_close($ch);
return [
'status_code' => $statusCode,
'headers' => $headers,
'body' => $body,
];
}连接复用与 DNS 缓存
连接复用
php
<?php
declare(strict_types=1);
/**
* 连接复用示例:多个请求共用同一 TCP 连接
*/
function reuseConnection(): void
{
$baseUrl = 'https://jsonplaceholder.typicode.com';
$ch = curl_init();
// 所有请求共用同一个句柄(连接将被复用)
$endpoints = ['posts/1', 'posts/2', 'posts/3', 'users/1', 'comments/1'];
foreach ($endpoints as $endpoint) {
curl_setopt($ch, CURLOPT_URL, $baseUrl . '/' . $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
echo "{$endpoint}: HTTP {$httpCode}" . PHP_EOL;
}
curl_close($ch);
}DNS 缓存
php
<?php
declare(strict_types=1);
function requestWithDnsCache(string $url): string
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
// DNS 缓存超时(秒),-1 表示永远缓存
CURLOPT_DNS_CACHE_TIMEOUT => 600,
// DNS 服务器列表
// CURLOPT_RESOLVE => [
// 'example.com:443:93.184.216.34',
// ],
]);
$response = curl_exec($ch);
curl_close($ch);
return $response !== false ? $response : '';
}实战示例
完整的 HTTP 客户端类
php
<?php
declare(strict_types=1);
class AdvancedHttpClient
{
private string $baseUrl;
private array $defaultOptions;
private array $defaultHeaders;
private ?\CurlHandle $persistentHandle = null;
public function __construct(
string $baseUrl,
array $options = [],
array $headers = []
) {
$this->baseUrl = rtrim($baseUrl, '/');
$this->defaultOptions = array_merge([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HEADER => false,
], $options);
$this->defaultHeaders = array_merge([
'Accept: application/json',
'User-Agent: AdvancedHttpClient/2.0',
], $headers);
}
/**
* 发送单个请求
*/
public function request(
string $method,
string $uri,
array $data = [],
array $query = [],
array $headers = []
): Response {
$url = $this->baseUrl . '/' . ltrim($uri, '/');
if (!empty($query)) {
$url .= '?' . http_build_query($query);
}
$ch = curl_init();
$httpHeaders = array_merge($this->defaultHeaders, $headers);
$options = $this->defaultOptions;
$options[CURLOPT_URL] = $url;
$options[CURLOPT_CUSTOMREQUEST] = strtoupper($method);
$options[CURLOPT_HTTPHEADER] = $httpHeaders;
if (!empty($data) && in_array(strtoupper($method), ['POST', 'PUT', 'PATCH'])) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
$httpHeaders[] = 'Content-Type: application/json';
}
// 响应头回调
$responseHeaders = [];
$options[CURLOPT_HEADERFUNCTION] = function ($ch, string $header) use (&$responseHeaders): int {
$len = strlen($header);
if (preg_match('/^([^:]+):\s*(.+)$/', trim($header), $matches)) {
$responseHeaders[strtolower($matches[1])] = trim($matches[2]);
}
return $len;
};
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
$effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$error = curl_errno($ch) > 0 ? curl_error($ch) : null;
curl_close($ch);
return new Response(
$httpCode,
$body,
$responseHeaders,
$totalTime,
$effectiveUrl,
$error
);
}
/**
* 并发请求
*/
public function requestMulti(
array $requests,
int $maxConcurrency = 5
): array {
$results = [];
$chunks = array_chunk($requests, $maxConcurrency);
foreach ($chunks as $chunk) {
$results = array_merge($results, $this->executeBatch($chunk));
}
return $results;
}
private function executeBatch(array $requests): array
{
$multi = curl_multi_init();
$handles = [];
foreach ($requests as $i => $req) {
$ch = curl_init();
$url = $this->baseUrl . '/' . ltrim($req['uri'], '/');
curl_setopt_array($ch, array_merge(
$this->defaultOptions,
[
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => strtoupper($req['method'] ?? 'GET'),
CURLOPT_HTTPHEADER => array_merge($this->defaultHeaders, $req['headers'] ?? []),
]
));
if (!empty($req['data'] ?? null)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($req['data']));
}
curl_multi_add_handle($multi, $ch);
$handles[$i] = $ch;
}
$active = null;
do {
curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi);
}
} while ($active);
$results = [];
foreach ($handles as $i => $ch) {
$results[$i] = [
'body' => curl_multi_getcontent($ch),
'httpCode' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
'error' => curl_error($ch),
];
curl_multi_remove_handle($multi, $ch);
curl_close($ch);
}
curl_multi_close($multi);
return $results;
}
}
/**
* 响应对象
*/
class Response
{
public function __construct(
public readonly int $statusCode,
public readonly string|false $body,
public readonly array $headers,
public readonly float $totalTime,
public readonly string $effectiveUrl,
public readonly ?string $error
) {}
public function isSuccessful(): bool
{
return $this->statusCode >= 200 && $this->statusCode < 300;
}
public function json(): mixed
{
return $this->body !== false ? json_decode($this->body, true) : null;
}
public function header(string $name): ?string
{
return $this->headers[strtolower($name)] ?? null;
}
}注意事项
内存消耗
php
<?php
declare(strict_types=1);
// 错误做法:大文件直接读入内存
curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('huge-file.bin'));
// 正确做法:使用 PUT + CURLOPT_INFILE
$fp = fopen('huge-file.bin', 'r');
curl_setopt($ch, CURLOPT_PUT, true);
curl_setopt($ch, CURLOPT_INFILE, $fp);
curl_setopt($ch, CURLOPT_INFILESIZE, filesize('huge-file.bin'));PHP 8.0+ CurlHandle 类型
PHP 8.0 将 cURL 句柄从资源类型改为 CurlHandle 对象:
php
<?php
declare(strict_types=1);
// PHP 8.0+ 可以进行类型声明
function fetch(string $url, \CurlHandle $ch = null): string
{
$ch = $ch ?? curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
return $result !== false ? $result : '';
}最佳实践
- 并发请求:对无依赖关系的多个 API 调用使用
curl_multi_* - 大文件传输:使用
CURLOPT_INFILE/CURLOPT_FILE避免内存溢出 - 进度监控:下载大文件时通过
CURLOPT_PROGRESSFUNCTION显示进度 - 断点续传:使用
CURLOPT_RESUME_FROM实现续传 - 连接复用:同一目标域名的请求复用句柄
- 并发控制:设置合理的并发上限,避免被服务器限流
下一节
继续学习:Socket 编程