PHP cURL 详解

78次阅读
没有评论

本文系统介绍 PHP cURL 扩展:基础概念、核心 API、常用选项、GET/POST/JSON/文件上传、并发(curl_multi)、错误与调试、性能与安全最佳实践,并提供可运行示例与命令。

目录

  • 基础与启用检查
  • 核心 API 流程
  • 常用选项与场景
  • GET/POST/JSON 示例
  • 文件上传与下载
  • 并发请求(curl_multi)
  • 错误处理与调试
  • 性能与安全最佳实践
  • 完整示例与运行
  • 并发下载示例与重试封装
    • 使用 curl_multi 并发下载到磁盘
    • 简易重试封装(指数退避 + 可重试状态)
  • FAQ

基础与启用检查

  • cURL 是客户端网络请求库,支持 HTTP(S)、FTP 等协议。
  • PHP 通过 ext/curl 扩展提供封装。检查是否启用:
    <?php
    echo function_exists('curl_init') ? "cURL enabled\n" : "cURL disabled\n";
    phpinfo(); // 查看 curl 版本与特性
  • 在 macOS/Linux 常用包管理安装或启用扩展;在 Docker/容器中使用镜像自带或安装。

核心 API 流程

  • 基本流程:curl_init()curl_setopt() 配置 → curl_exec() 执行 → curl_close() 释放。
  • 返回与资源:curl_exec() 成功返回响应体(当 CURLOPT_RETURNTRANSFER 为 true),失败返回 false。
  • 获取额外信息:curl_getinfo($ch) 返回状态码、时间、大小等指标。

示例:

$ch = curl_init();
curl_setopt_array($ch, [
  CURLOPT_URL => 'https://httpbin.org/get',
  CURLOPT_RETURNTRANSFER => true,   // 返回字符串而非直接输出
  CURLOPT_TIMEOUT => 10,            // 总超时秒
]);
$body = curl_exec($ch);
if ($body === false) {
  throw new RuntimeException('cURL error: '.curl_error($ch), curl_errno($ch));
}
$info = curl_getinfo($ch);
curl_close($ch);

常用选项与场景

  • URL 与方法:CURLOPT_URLCURLOPT_CUSTOMREQUEST 指定 GET/POST/PUT/DELETE 等。
  • 请求体:
    • CURLOPT_POSTCURLOPT_POSTFIELDS(数组或字符串);数组会使用 application/x-www-form-urlencoded 或 multipart(含文件)。
    • 发送 JSON:设置 POSTFIELDSjson_encode($data);并加 Header。
  • Header:CURLOPT_HTTPHEADER 设定 KV;示例:['Content-Type: application/json']
  • SSL:
    • CURLOPT_SSL_VERIFYPEERCURLOPT_SSL_VERIFYHOST 默认应开启校验。
    • CURLOPT_CAINFO 指定 CA 证书;必要时更新 CA 包。
  • 重定向:CURLOPT_FOLLOWLOCATION 控制是否跟随;CURLOPT_MAXREDIRS 限制次数。
  • 代理:CURLOPT_PROXYCURLOPT_PROXYUSERPWD 设置代理与认证。
  • 超时:CURLOPT_TIMEOUT(总超时)、CURLOPT_CONNECTTIMEOUT(连接)、CURLOPT_LOW_SPEED_TIME/LIMIT(低速阈值)。
  • 压缩:CURLOPT_ENCODING 设置 gzip,deflate 自动解压。
  • Keep-Alive:Connection: keep-alive,并重用 handle;或使用 HTTP/2。

GET/POST/JSON 示例

// GET
$ch = curl_init('https://httpbin.org/get?foo=bar');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
curl_close($ch);

// POST 表单
$ch = curl_init('https://httpbin.org/post');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => [ 'name' => 'alice', 'age' => 20 ],
  CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
curl_close($ch);

// POST JSON
$ch = curl_init('https://httpbin.org/post');
$payload = json_encode(['name' => 'alice', 'age' => 20], JSON_UNESCAPED_UNICODE);
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => $payload,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'Accept: application/json',
  ],
  CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
curl_close($ch);

文件上传与下载

  • 上传:通过 CURLOPT_POSTFIELDS 使用 CURLFile 指定文件;或数组中使用 new CURLFile($path)
  • 下载到文件:使用 CURLOPT_FILE/CURLOPT_WRITEFUNCTION 将响应写入句柄。

示例:

// 上传文件
$ch = curl_init('https://httpbin.org/post');
$file = new CURLFile('/path/to/file.png', 'image/png', 'file.png');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => [ 'file' => $file, 'desc' => 'upload test' ],
  CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
curl_close($ch);

// 下载到磁盘
$fp = fopen('out.bin', 'wb');
$ch = curl_init('https://httpbin.org/bytes/1024');
curl_setopt_array($ch, [
  CURLOPT_FILE => $fp,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_TIMEOUT => 15,
]);
curl_exec($ch);
if (curl_errno($ch)) { fprintf(STDERR, "error: %s\n", curl_error($ch)); }
curl_close($ch);
fclose($fp);

并发请求(curl_multi)

  • 使用 curl_multi_init() 管理多个 curl 句柄并发执行,提升吞吐。
  • 关键 API:curl_multi_add_handlecurl_multi_execcurl_multi_selectcurl_multi_getcontentcurl_multi_remove_handlecurl_multi_close

示例(并发 GET):

$urls = [
  'https://httpbin.org/get?i=1',
  'https://httpbin.org/get?i=2',
  'https://httpbin.org/get?i=3',
];
$mh = curl_multi_init();
$chs = [];
foreach ($urls as $u) {
  $ch = curl_init($u);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_multi_add_handle($mh, $ch);
  $chs[] = $ch;
}

$running = null;
do {
  $mrc = curl_multi_exec($mh, $running);
  if ($mrc > 0) { usleep(1000); }
  curl_multi_select($mh, 0.1);
} while ($running > 0);

$results = [];
foreach ($chs as $ch) {
  $results[] = curl_multi_getcontent($ch);
  curl_multi_remove_handle($mh, $ch);
  curl_close($ch);
}
curl_multi_close($mh);

错误处理与调试

  • 错误码:curl_errno($ch),错误信息:curl_error($ch)
  • 调试日志:CURLOPT_VERBOSE 打印详细调试信息到 STDERR;或使用 CURLOPT_STDERR 指定文件句柄。
  • 状态码与响应头:curl_getinfo($ch, CURLINFO_RESPONSE_CODE),自定义回调 CURLOPT_HEADERFUNCTION 解析头部。

性能与安全最佳实践

  • 复用 Handle:避免频繁 init/close,可重用连接(Keep-Alive)。
  • 合理超时:设定连接/总超时,设置低速限制避免卡死。
  • 限制重定向:防止开放重定向带来的安全风险。
  • 严格证书校验:开启 SSL_VERIFYPEER/VERIFYHOST,维护 CA 包;必要时配置 CURLOPT_CAINFO
  • 敏感信息:Token/密码不写入日志;请求头与 URL 参数中避免泄露。
  • 代理与网络:必要时配置 CURLOPT_PROXY 与认证;在受限网络中预设合理失败重试策略。

完整示例与运行

curl_examples.php

<?php
function get_json(string $url, array $headers = []): array {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => array_merge($headers, ['Accept: application/json']),
    CURLOPT_TIMEOUT => 10,
  ]);
  $body = curl_exec($ch);
  if ($body === false) throw new RuntimeException(curl_error($ch), curl_errno($ch));
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  curl_close($ch);
  if ($code >= 400) throw new RuntimeException("HTTP $code");
  return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}

function post_json(string $url, $data, array $headers = []): array {
  $ch = curl_init($url);
  $payload = json_encode($data, JSON_UNESCAPED_UNICODE);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_HTTPHEADER => array_merge($headers, [
      'Content-Type: application/json',
      'Accept: application/json',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
  ]);
  $body = curl_exec($ch);
  if ($body === false) throw new RuntimeException(curl_error($ch), curl_errno($ch));
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  curl_close($ch);
  if ($code >= 400) throw new RuntimeException("HTTP $code");
  return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}

// demo
try {
  $r1 = get_json('https://httpbin.org/get?ping=pong');
  $r2 = post_json('https://httpbin.org/post', ['hello' => 'world']);
  print_r([$r1['args'], $r2['json']]);
} catch (Throwable $e) {
  fwrite(STDERR, $e->getMessage()."\n");
}

运行:

php curl_examples.php

并发下载示例与重试封装

使用 curl_multi 并发下载到磁盘

<?php
function multi_download(array $urls, string $outDir, float $timeout = 30.0): array {
  if (!is_dir($outDir)) mkdir($outDir, 0777, true);
  $mh = curl_multi_init();
  $chs = [];
  $fps = [];

  foreach ($urls as $i => $url) {
    $fname = sprintf('%s/file_%03d.bin', rtrim($outDir, '/'), $i+1);
    $fp = fopen($fname, 'wb');
    $ch = curl_init($url);
    curl_setopt_array($ch, [
      CURLOPT_FILE => $fp,
      CURLOPT_FOLLOWLOCATION => true,
      CURLOPT_CONNECTTIMEOUT => 10,
      CURLOPT_TIMEOUT => $timeout,
      CURLOPT_SSL_VERIFYPEER => true,
      CURLOPT_SSL_VERIFYHOST => 2,
    ]);
    curl_multi_add_handle($mh, $ch);
    $chs[$fname] = $ch;
    $fps[$fname] = $fp;
  }

  $running = null;
  do {
    $mrc = curl_multi_exec($mh, $running);
    if ($mrc === CURLM_CALL_MULTI_PERFORM) continue;
    if ($running) curl_multi_select($mh, 0.2);
  } while ($running > 0 && $mrc === CURLM_OK);

  $results = [];
  foreach ($chs as $fname => $ch) {
    $err = curl_error($ch);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_multi_remove_handle($mh, $ch);
    curl_close($ch);
    fclose($fps[$fname]);
    $results[$fname] = ($err === '' && $code >= 200 && $code < 300) ? 'ok' : ("error: $err, code=$code");
    if ($err !== '' || $code >= 400) { @unlink($fname); }
  }
  curl_multi_close($mh);
  return $results;
}

// demo
$urls = [
  'https://httpbin.org/bytes/1024',
  'https://httpbin.org/bytes/2048',
  'https://httpbin.org/bytes/4096',
];
$res = multi_download($urls, __DIR__.'/downloads');
print_r($res);

运行:

php multi_download.php
ls -lh downloads

简易重试封装(指数退避 + 可重试状态)

<?php
function request_with_retry(callable $doRequest, int $maxAttempts = 3, int $baseDelayMs = 200, array $retryOnHttp = [429, 500, 502, 503, 504]) {
  $attempt = 0;
  while (true) {
    $attempt++;
    try {
      [$body, $code] = $doRequest(); // 返回 [string $body, int $status]
      if ($code >= 200 && $code < 300) return [$body, $code];
      if ($attempt >= $maxAttempts || !in_array($code, $retryOnHttp, true)) return [$body, $code];
    } catch (Throwable $e) {
      if ($attempt >= $maxAttempts) throw $e;
    }
    $delay = $baseDelayMs * (2 ** ($attempt - 1));
    usleep($delay * 1000);
  }
}

// 使用示例:封装一次 cURL 请求
$do = function () {
  $ch = curl_init('https://httpbin.org/status/503');
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
  ]);
  $body = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  if ($body === false) {
    $err = new RuntimeException(curl_error($ch), curl_errno($ch));
    curl_close($ch);
    throw $err;
  }
  curl_close($ch);
  return [$body, $code];
};

[$body, $code] = request_with_retry($do, 4, 250);
echo "final code=$code\n";

建议:在生产环境中结合固定最大等待、随机抖动(jitter)与全局超时,避免惊群与长尾。

FAQ

  • HTTPS 报证书错误:更新系统 CA 或指定 CURLOPT_CAINFO;不要关闭校验以“解决”。
  • 超时不生效:区分连接与总超时;DNS 解析可能阻塞,考虑 CURLOPT_NOSIGNAL 与低速限制。
  • 代理认证失败:检查 CURLOPT_PROXYUSERPWD 格式 user:pass 与代理协议(HTTP/SOCKS)。
  • JSON 编码问题:使用 JSON_UNESCAPED_UNICODEJSON_THROW_ON_ERROR 获取一致编码与异常处理。
  • 重用连接:避免每次调用都新初始化,复用 handle 或用持久化封装。

正文完
 0
评论(没有评论)

YanQS's Blog