发送服务器请求时,fsockopen 和 curl 哪个更好?
结论
在 Automated Recurring Billing 应用中发送 HTTP/HTTPS 请求,通常应优先选择 cURL。
cURL 已经处理了 HTTPS、证书校验、超时、重定向、代理、请求头和状态码等常见问题,代码也更容易维护。fsockopen() 更接近底层 TCP Socket。用它发送 HTTP 请求时,需要自行拼装请求、解析响应并处理各种网络细节。除非涉及特殊协议或需要直接控制底层连接,否则没有必要使用它。
如果支付服务商提供官方 PHP SDK,优先使用官方 SDK。SDK 通常以 cURL 等 HTTP 客户端为基础,并进一步封装签名、认证、重试和错误处理。
为什么更推荐 cURL
这两个工具解决的问题不在同一层级:
fsockopen()用于建立 Socket 连接。通过它发送 HTTP 请求,需要手动处理请求行、Headers、Body、TLS、分块响应和连接关闭等细节。- cURL 是成熟的网络传输客户端,直接支持 HTTP/HTTPS,并提供完整的请求配置和诊断功能。
对于周期扣费,cURL 有这些优势:
- 原生支持 HTTPS,可以校验证书和主机名。
- 可以设置连接超时和总超时,防止请求一直阻塞。
- 能直接读取 HTTP 状态码。
- 支持 JSON、表单、Basic Auth、Bearer Token、客户端证书等常见认证方式。
- 可以区分网络错误与业务接口错误。
- 便于接入日志、重试和监控。
fsockopen() 依赖较少,提供的底层控制也更多,还能连接非 HTTP 的 TCP 服务。不过,普通支付 API 通常用不到这些能力,自己处理底层细节反而更容易出错。
使用 cURL 发送请求
下面以向支付服务接口提交 JSON 请求为例。URL、字段和认证方式需要替换为实际服务商提供的内容。
<?php
$url = 'https://api.example.com/v1/subscriptions';
$payload = [
'customer_id' => 'customer_123',
'plan_id' => 'monthly_plan',
'request_id' => bin2hex(random_bytes(16)),
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PAYMENT_API_TOKEN'),
'Content-Type: application/json',
'Accept: application/json',
'Idempotency-Key: ' . $payload['request_id'],
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$errorCode = curl_errno($ch);
$errorMessage = curl_error($ch);
curl_close($ch);
throw new RuntimeException(
"Network request failed ({$errorCode}): {$errorMessage}"
);
}
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
try {
$response = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new RuntimeException(
"Server returned invalid JSON, HTTP status: {$statusCode}",
0,
$e
);
}
if ($statusCode < 200 || $statusCode >= 300) {
$message = $response['message'] ?? 'Unknown API error';
throw new RuntimeException(
"API request failed, HTTP status: {$statusCode}, message: {$message}"
);
}
// 继续处理成功响应
这段代码依次检查:
curl_exec()是否遇到连接、DNS、TLS 或超时错误。- HTTP 状态码是否在
2xx范围内。 - 响应内容是否为有效 JSON。
- 接口返回的业务结果是否符合预期。
不能只检查 curl_exec() 是否返回 false。服务器返回 400、401 或 500 时,cURL 仍可能已经正常完成网络请求。
fsockopen 为什么更麻烦
使用 fsockopen() 调用 HTTPS 接口时,大致需要手动构造以下请求:
<?php
$host = 'api.example.com';
$path = '/v1/subscriptions';
$body = json_encode([
'customer_id' => 'customer_123',
'plan_id' => 'monthly_plan',
], JSON_THROW_ON_ERROR);
$socket = fsockopen(
'ssl://' . $host,
443,
$errorCode,
$errorMessage,
10
);
if ($socket === false) {
throw new RuntimeException(
"Connection failed ({$errorCode}): {$errorMessage}"
);
}
$request =
"POST {$path} HTTP/1.1\r\n" .
"Host: {$host}\r\n" .
"Content-Type: application/json\r\n" .
"Accept: application/json\r\n" .
"Content-Length: " . strlen($body) . "\r\n" .
"Connection: close\r\n" .
"\r\n" .
$body;
fwrite($socket, $request);
$response = '';
while (!feof($socket)) {
$response .= fgets($socket, 8192);
}
fclose($socket);
这段代码还不足以用于生产环境。实际使用时,还要妥善处理:
- TLS 证书和主机名校验;
- HTTP 状态行及响应头解析;
Transfer-Encoding: chunked;- 压缩响应;
- 重定向;
- 读取与写入超时;
- 部分写入;
- 持久连接和不同的 HTTP 版本。
调用普通 HTTP API 时,不建议自行实现这些逻辑。
周期扣费请求的注意事项
支付请求不能只看是否发送成功,还要避免重复扣款和状态不一致。
- 使用支付平台支持的幂等键,例如
Idempotency-Key。重试同一个业务操作时,应复用原来的键。 - 只有明确判断请求适合重试时才进行重试。连接超时不代表服务端一定没有处理请求,应通过幂等机制或查询接口确认状态。
- 设置合理的连接超时和总超时。
- 不要关闭
CURLOPT_SSL_VERIFYPEER,也不要将CURLOPT_SSL_VERIFYHOST设置为0。 - 将 API 密钥存放在环境变量或密钥管理服务中,不要写进源码或日志。
- 不要记录银行卡号、CVV、完整 Token 等敏感数据。
- 使用支付服务商提供的订阅和 Token 化能力,避免自行保存银行卡信息。
- Webhook 必须验证签名,并能处理重复投递。数据库更新也要具备幂等性。
- HTTP 请求成功不代表扣款成功。最终结果应以接口返回的业务状态和经过验证的 Webhook 为准。
普通 PHP 项目应选择 cURL、基于 cURL 的成熟 HTTP 客户端或官方 SDK。只有目标服务不是 HTTP,或者确实需要直接控制底层 Socket 时,才考虑 fsockopen()。
备注:内容仅供参考。
