在無 cURL 擴展、或需精確控制 TCP 層行為(如自定義 Header、Keep-Alive、Chunked 傳輸)時,fsockopen 是 PHP 底層網路編程的核心工具。本文從原理解析、完整封裝類、異常處理到生產環境優化,全方位講解如何用 fsockopen 實現健壯的 HTTP 客戶端。
fsockopen 建立 TCP 連接後,需手動構造符合 RFC 7230 的 HTTP 請求報文:
GET /path?query=1 HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: Custom-Agent\r\n
Connection: close\r\n
\r\n
關鍵點:
0\r\n\r\n<?php
class SocketHttpClient {
private $host;
private $port;
private $timeout;
private $fp;
private $errno;
private $errstr;
public function __construct(string $host, int $port = 80, float $timeout = 10.0) {
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
}
// 發送 GET 請求
public function get(string $path, array $query = [], array $headers = []): string|false {
$queryStr = http_build_query($query);
$url = $path . ($queryStr ? '?' . $queryStr : '');
$request = "GET $url HTTP/1.1\r\n";
$request .= $this->buildHeaders($headers);
$request .= "Connection: close\r\n\r\n";
return $this->execute($request);
}
// 發送 POST 請求
public function post(string $path, array $data = [], array $headers = []): string|false {
$body = http_build_query($data);
$request = "POST $path HTTP/1.1\r\n";
$request .= $this->buildHeaders($headers);
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
$request .= "Content-Length: " . strlen($body) . "\r\n";
$request .= "Connection: close\r\n\r\n";
$request .= $body;
return $this->execute($request);
}
// 支援 JSON POST
public function postJson(string $path, array $data, array $headers = []): string|false {
$body = json_encode($data);
$request = "POST $path HTTP/1.1\r\n";
$request .= $this->buildHeaders($headers);
$request .= "Content-Type: application/json\r\n";
$request .= "Content-Length: " . strlen($body) . "\r\n";
$request .= "Connection: close\r\n\r\n";
$request .= $body;
return $this->execute($request);
}
private function buildHeaders(array $headers): string {
$default = [
'Host' => $this->host,
'User-Agent' => 'PHP-SocketHttpClient/1.0',
'Accept' => '*/*',
];
$merged = $default + $headers;
$str = '';
foreach ($merged as $k => $v) {
$str .= "$k: $v\r\n";
}
return $str;
}
private function execute(string $request): string|false {
$this->fp = @fsockopen($this->host, $this->port, $this->errno, $this->errstr, $this->timeout);
if (!$this->fp) {
error_log("fsockopen failed: [$this->errno] $this->errstr");
return false;
}
stream_set_timeout($this->fp, (int)$this->timeout, (int)(($this->timeout - (int)$this->timeout) * 1e6));
fwrite($this->fp, $request);
$response = '';
$info = stream_get_meta_data($this->fp);
while (!feof($this->fp) && !$info['timed_out']) {
$response .= fread($this->fp, 8192);
$info = stream_get_meta_data($this->fp);
}
fclose($this->fp);
if ($info['timed_out']) {
error_log("Socket timeout reading from {$this->host}:{$this->port}");
return false;
}
return $response;
}
// 解析響應:分離 Header 與 Body
public static function parseResponse(string $response): array {
list($headerBlock, $body) = explode("\r\n\r\n", $response, 2) + ['', ''];
$headers = [];
foreach (explode("\r\n", $headerBlock) as $line) {
if (strpos($line, ':') !== false) {
[$k, $v] = explode(':', $line, 2);
$headers[trim($k)] = trim($v);
}
}
// 處理 Chunked 編碼
if (isset($headers['Transfer-Encoding']) && stripos($headers['Transfer-Encoding'], 'chunked') !== false) {
$body = self::decodeChunked($body);
}
return ['headers' => $headers, 'body' => $body, 'status' => $headers['Status'] ?? ''];
}
private static function decodeChunked(string $data): string {
$result = '';
$pos = 0;
while ($pos < strlen($data)) {
$lineEnd = strpos($data, "\r\n", $pos);
if ($lineEnd === false) break;
$length = hexdec(trim(substr($data, $pos, $lineEnd - $pos)));
if ($length === 0) break;
$pos = $lineEnd + 2;
$result .= substr($data, $pos, $length);
$pos += $length + 2; // skip \r\n
}
return $result;
}
}
// 使用示例
$client = new SocketHttpClient('api.example.com', 443, 15); // HTTPS 需配合 SSL 上下文
// 註:fsockopen 直連 HTTPS 需 ssl:// 前綴與 stream_context_create
// 見下文「HTTPS 支援」章節
// 修改構造函數或連接邏輯
$host = 'ssl://api.example.com'; // 或 'tls://'
$port = 443;
$context = stream_context_create([
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
]
]);
$this->fp = @stream_socket_client(
"$host:$port",
$this->errno,
$this->errstr,
$this->timeout,
STREAM_CLIENT_CONNECT,
$context
);
注意:fsockopen 不直接支援 SSL 參數,必須用 stream_socket_client + stream_context_create。
| 異常類型 | 檢測方式 | 處理策略 |
|---|---|---|
| 連接超時 | $this->errno 非 0 | 重試 2-3 次,指數退避 |
| 讀取超時 | stream_get_meta_data()['timed_out'] | 設置 stream_set_timeout,捕獲返回 false |
| 連接重置 | fread 返回空且非 EOF | 檢查 feof,記錄錯誤重試 |
| SSL 握手失敗 | stream_socket_client 返回 false | 檢查 CA 憑證、SNI、協議版本 |
| Chunked 解析異常 | 長度解析失敗 | 回退原始 Body,記錄警告 |
Connection: close,實現 Keep-Alive(需手動管理連接池)stream_set_blocking($fp, 0) + stream_select 實現高併發gethostbyname 預解析並緩存 IP| 場景 | 推薦方案 |
|---|---|
| 常規 HTTP API 調用 | cURL(功能全、穩定、易維護) |
| 無 cURL 擴展環境 | fsockopen 封裝 |
| 需精確控制 TCP/SSL 細節 | fsockopen / stream_socket_client |
| 自定義協議(非 HTTP) | fsockopen |
| 高併發異步請求 | Swoole / OpenSwoole / ReactPHP |
總結:fsockopen 是理解 HTTP 協議、排查網路疑難雜症、特殊環境下的利器。但常規業務開發強烈建議使用 cURL 或 Guzzle 等成熟庫,僅在底層調試、無擴展環境、自定義協議時直接操作 Socket。