fsockopen 是 PHP 底層 Socket 操作函數,適用於:
生產建議: 優先使用 GuzzleHttp、Symfony HttpClient 或原生
file_get_contents()(配合 stream_context),代碼更健壯、支持 HTTPS/HTTP2/自動重定向/超時控制。
<?php
/**
* 使用 fsockopen 發送 HTTP 請求並獲取響應體
*
* @param string $url 目標 URL (支持 http/https)
* @param array $options 選項: method, headers, body, timeout, ssl_verify
* @return string 響應體
* @throws RuntimeException 連接/協議錯誤
*/
function httpRequestBySocket(string $url, array $options = []): string
{
$opts = array_merge([
'method' => 'GET',
'headers' => [],
'body' => '',
'timeout' => 10.0,
'ssl_verify' => true, // HTTPS 是否驗證證書
], $options);
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
throw new InvalidArgumentException('Invalid URL: ' . $url);
}
$host = $parsed['host'];
$port = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80);
$path = ($parsed['path'] ?? '/') . (isset($parsed['query']) ? '?' . $parsed['query'] : '');
// 構造 Host 頭 (HTTP/1.1 必須)
$headers = $opts['headers'];
$headers['Host'] = $host;
if ($opts['body'] !== '' && !isset($headers['Content-Length'])) {
$headers['Content-Length'] = strlen($opts['body']);
}
if ($opts['body'] !== '' && !isset($headers['Content-Type'])) {
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
// 建立連接 (支持 SSL/TLS)
$schemePrefix = ($parsed['scheme'] === 'https') ? 'ssl://' : '';
$target = $schemePrefix . $host . ':' . $port;
$flags = STREAM_CLIENT_CONNECT;
$context = stream_context_create([
'ssl' => [
'verify_peer' => $opts['ssl_verify'],
'verify_peer_name' => $opts['ssl_verify'],
'allow_self_signed' => !$opts['ssl_verify'],
]
]);
$fp = stream_socket_client($target, $errno, $errstr, $opts['timeout'], $flags, $context);
if (!$fp) {
throw new RuntimeException("Connect failed [$errno]: $errstr");
}
stream_set_timeout($fp, $opts['timeout']);
// 發送請求行
$request = "{$opts['method']} $path HTTP/1.1\r\n";
// 發送頭部
foreach ($headers as $k => $v) {
$request .= "$k: $v\r\n";
}
$request .= "\r\n"; // 空行結束頭部
// 發送體
$request .= $opts['body'];
if (fwrite($fp, $request) === false) {
fclose($fp);
throw new RuntimeException('Write request failed');
}
// 讀取響應
$response = '';
$headerPart = '';
$inHeader = true;
$contentLength = null;
$chunked = false;
while (!feof($fp)) {
$line = fgets($fp, 8192);
if ($line === false) break;
if ($inHeader) {
$headerPart .= $line;
if (strpos($line, 'Content-Length:') !== false) {
$contentLength = (int)trim(explode(':', $line, 2)[1]);
}
if (stripos($line, 'Transfer-Encoding: chunked') !== false) {
$chunked = true;
}
// 頭部結束標記
if ($line === "\r\n" || $line === "\n") {
$inHeader = false;
}
} else {
$response .= $line;
}
}
fclose($fp);
// 處理 Chunked 編碼 (簡易版,生產建議用成熟解析器)
if ($chunked) {
$response = decodeChunked($response);
} elseif ($contentLength !== null) {
$response = substr($response, 0, $contentLength);
}
return $response;
}
/** 簡易 Chunked 解碼 */
function decodeChunked(string $data): string
{
$out = '';
$pos = 0;
$len = strlen($data);
while ($pos < $len) {
$lineEnd = strpos($data, "\r\n", $pos);
if ($lineEnd === false) break;
$chunkSize = hexdec(trim(substr($data, $pos, $lineEnd - $pos)));
$pos = $lineEnd + 2;
if ($chunkSize === 0) break; // 最後一塊
$out .= substr($data, $pos, $chunkSize);
$pos += $chunkSize + 2; // 跳過數據和 \r\n
}
return $out;
}
// ========== 使用示例 ==========
try {
// GET 請求
$html = httpRequestBySocket('https://httpbin.org/get');
echo "GET 響應長度: " . strlen($html) . "\n";
// POST XML
$xml = '<order><id>123</id></order>';
$result = httpRequestBySocket('https://httpbin.org/post', [
'method' => 'POST',
'headers' => ['Content-Type' => 'application/xml'],
'body' => $xml,
]);
echo "POST 響應: $result\n";
// 攜帶 Cookie
$withCookie = httpRequestBySocket('https://httpbin.org/cookies', [
'headers' => ['Cookie' => 'session=abc123; user=john'],
]);
echo "Cookie 響應: $withCookie\n";
} catch (Throwable $e) {
error_log('Socket HTTP Error: ' . $e->getMessage());
}
?>
虛擬主機環境下,服務器依靠 Host 頭路由請求,缺失將返回 400 Bad Request。
fsockopen 不直接支持 SSL,需用 stream_socket_client('ssl://host:443') 並配置 stream_context_create(['ssl' => [...]) 實現證書驗證、SNI 等。
Connection: close 斷開連接判斷結束(HTTP/1.0 行為)。分別控制**連接超時** (stream_socket_client 第 4 參數) 和**讀寫超時** (stream_set_timeout),避免死掛。
發送 Connection: keep-alive 並複用 $fp 可提升性能,但需嚴格按 Content-Length/Chunked 讀取完整響應,否則管道會亂序。
| 現象 | 原因 | 解決 |
|---|---|---|
| 返回亂碼/壓縮內容 | 服務端返回 gzip/deflate,未解壓 | 請求頭加 Accept-Encoding: identity 禁用壓縮,或用 gzinflate()/gzdecode() 解壓 |
| HTTPS 證書驗證失敗 | 自簽證書/CA 不信任 | 開發環境 'verify_peer'=>false,生產**必須**配置正確 CA 憑證路徑 |
| POST 數據服務端收不到 | 缺少 Content-Length 或 Content-Type | 手動計算 strlen($body) 設置 Content-Length,設置正確 Content-Type |
| 中文參數亂碼 | URL 編碼/Body 編碼不一致 | GET 參數用 urlencode(),POST Body 按 Content-Type 編碼 (JSON 用 utf-8) |
| 大文件下載記憶體爆炸 | 將整個響應存入變量 | 邊讀邊寫文件/輸出,不要累積 $response 字符串 |
<?php
// 現代寫法 (Guzzle 7+)
$client = new \GuzzleHttp\Client(['timeout' => 10]);
$response = $client->post('https://api.example.com', [
'headers' => ['Content-Type' => 'application/xml'],
'body' => $xml,
'verify' => true, // 自動處理證書、重定向、Chunked、gzip、keep-alive、異步、重試...
]);
$data = $response->getBody()->getContents();
?>
**核心差距**:成熟客戶端處理了**重定向、Cookie 罐、連接池、異步並發、中介軟體擴展、PSR-18 標準化**,手寫 Socket 純屬造輪子。