應用場景
- 無 cURL 擴充套件的環境。
- 需精確控制 HTTP 協定細節(自定 Header、Chunked 傳輸、Keep-Alive)。
- 教學理解 HTTP 協定運作。
通用 Socket HTTP Client 函式
<?php
/**
* 使用 fsockopen 發送 HTTP 請求
*
* @param string $url 完整 URL(含 scheme、host、path、query)
* @param array|string $postData POST 欄位陣列或已編碼字串,空字串為 GET
* @param array $headers 額外 Header 陣列 ['Header-Name' => 'Value']
* @param int $timeout 連線逾時秒數
* @return string|false 回應內容(含 Header + Body),失敗回 false
*/
function socketHttpRequest(string $url, $postData = '', array $headers = [], int $timeout = 10)
{
$parsed = parse_url($url);
if (!$parsed) {
return false;
}
$scheme = $parsed['scheme'] ?? 'http';
$host = $parsed['host'] ?? '';
$port = $parsed['port'] ?? ($scheme === 'https' ? 443 : 80);
$path = ($parsed['path'] ?? '/') . ($parsed['query'] ? '?' . $parsed['query'] : '');
// 建立連線(HTTPS 需 ssl:// 前綴且啟用 openssl 擴充套件)
$fp = @fsockopen(($scheme === 'https' ? 'ssl://' : '') . $host, $port, $errno, $errstr, $timeout);
if (!$fp) {
error_log("fsockopen 失敗 [$errno]: $errstr");
return false;
}
stream_set_timeout($fp, $timeout);
// 組裝請求
$isPost = is_array($postData) || $postData !== '';
$method = $isPost ? 'POST' : 'GET';
$request = "$method $path HTTP/1.1\r\n";
$request .= "Host: $host\r\n";
$request .= "Connection: Close\r\n"; // 簡化:每次請求關閉連線
$request .= "User-Agent: PHP-Socket-Client/1.0\r\n";
if ($isPost) {
$body = is_array($postData) ? http_build_query($postData) : $postData;
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
$request .= "Content-Length: " . strlen($body) . "\r\n";
}
// 自定 Header
foreach ($headers as $name => $value) {
$request .= "$name: $value\r\n";
}
$request .= "\r\n"; // Header 結束
if ($isPost) {
$request .= $body;
}
// 發送
fwrite($fp, $request);
// 接收回應
$response = '';
while (!feof($fp)) {
$response .= fread($fp, 8192);
}
fclose($fp);
return $response;
}
// 解析回應:分離 Header 與 Body
function parseHttpResponse(string $response): array
{
$headerEnd = strpos($response, "\r\n\r\n");
if ($headerEnd === false) {
return ['headers' => '', 'body' => $response];
}
$headers = substr($response, 0, $headerEnd);
$body = substr($response, $headerEnd + 4);
return ['headers' => $headers, 'body' => $body];
}
// === 使用範例 ===
// GET 請求
$resp = socketHttpRequest('https://httpbin.org/get?foo=bar');
$data = parseHttpResponse($resp);
echo "GET Body:\n" . $data['body'] . "\n";
// POST 請求
$resp = socketHttpRequest('https://httpbin.org/post', ['name' => 'John', 'age' => 30]);
$data = parseHttpResponse($resp);
echo "POST Body:\n" . $data['body'] . "\n";
?>
關鍵點解析
| 步驟 | 說明 |
| parse_url | 解析 URL 取得 host、port、path、query。 |
| fsockopen | 建立 TCP 連線,HTTPS 用 ssl://host:443(需 openssl)。 |
| HTTP/1.1 請求格式 | 請求行 + Headers + 空行 + Body。\r\n 為換行標準。 |
| Content-Length | POST 必須指定,否則伺服器不知何時結束讀取 Body。 |
| Connection: Close | 避免 Keep-Alive 處理複雜性,教學用途足夠。 |
| stream_set_timeout | 防止讀取卡死。 |
| fread 迴圈 | 完整讀取直到 EOF。 |
生產環境建議改用
- cURL:
curl_exec() 功能完整、效能優、支援 HTTP/2、多工、憑證驗證。
- GuzzleHttp:物件導向、PSR-7/18 相容、中介軟體、重試、異步。
- PHP 內建 stream_context:
file_get_contents() + stream_context_create() 簡單 GET/POST。
安全提醒
- HTTPS 必須驗證憑證(
stream_context_set_option($ctx, 'ssl', 'verify_peer', true)),否則遭受 MITM 攻擊。
- 使用者輸入拼接 URL 時需
parse_url 驗證 scheme/host,防 SSRF。
- 回應內容不可直接
eval/unserialize,防反序列化攻擊。