Skip to content

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 : '';
}

最佳实践

  1. 并发请求:对无依赖关系的多个 API 调用使用 curl_multi_*
  2. 大文件传输:使用 CURLOPT_INFILE/CURLOPT_FILE 避免内存溢出
  3. 进度监控:下载大文件时通过 CURLOPT_PROGRESSFUNCTION 显示进度
  4. 断点续传:使用 CURLOPT_RESUME_FROM 实现续传
  5. 连接复用:同一目标域名的请求复用句柄
  6. 并发控制:设置合理的并发上限,避免被服务器限流

下一节

继续学习:Socket 编程

参考链接