透過 `fsockopen()` 建立 TCP 連線,手工組裝 HTTP 請求報文,發送並解析回應,是理解 HTTP 協議、排查網路問題、無擴展環境下發送請求的基礎技能。同時介紹 cURL、Guzzle 等生產級替代方案。
<?php
class HttpSocket {
private $host;
private $port;
private $timeout = 10;
private $fp;
public function __construct(string $host, int $port = 80) {
$this->host = $host;
$this->port = $port;
}
public function request(string $method, string $path, array $headers = [], string $body = ''): string {
// 1. 建立連線
$this->fp = fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout);
if (!$this->fp) {
throw new RuntimeException("Connect failed: $errstr ($errno)");
}
stream_set_timeout($this->fp, $this->timeout);
// 2. 組裝請求行
$request = "$method $path HTTP/1.1\r\n";
// 3. 組裝標頭
$headers['Host'] = $headers['Host'] ?? $this->host;
$headers['Connection'] = $headers['Connection'] ?? 'close';
$headers['User-Agent'] = $headers['User-Agent'] ?? 'PHP-Socket/1.0';
if ($body) {
$headers['Content-Length'] = $headers['Content-Length'] ?? strlen($body);
$headers['Content-Type'] = $headers['Content-Type'] ?? 'application/x-www-form-urlencoded';
}
foreach ($headers as $k => $v) {
$request .= "$k: $v\r\n";
}
$request .= "\r\n"; // 空行分隔標頭與主體
$request .= $body;
// 4. 發送
fwrite($this->fp, $request);
// 5. 讀取回應
$response = '';
while (!feof($this->fp)) {
$response .= fread($this->fp, 8192);
}
fclose($this->fp);
return $response;
}
public function get(string $path, array $headers = []): string {
return $this->request('GET', $path, $headers);
}
public function post(string $path, array $data, array $headers = []): string {
$body = http_build_query($data);
return $this->request('POST', $path, $headers, $body);
}
}
// 使用範例
$client = new HttpSocket('httpbin.org', 80);
// GET
echo $client->get('/get');
// POST
echo $client->post('/post', ['username' => 'test', 'action' => 'login']);
?>
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| cURL (PHP 擴展) | 功能完整 (HTTPS、代理、憑證、Cookie、多執行緒、HTTP/2)、效能高、C 實現 | 需安裝擴展、API 較繁瑣 | 絕大多數生產環境 HTTP 客戶端 |
| Guzzle (Composer 套件) | PSR-7/18 相容、中介軟體、異步、重試、測試雙工、文件豐富 | 依賴 Composer、增加依賴體積 | 現代 PHP 專案、微服務呼叫、API 客戶端 |
| Symfony HttpClient | 原生支援非同步、Scoping Client、契約測試、整合 Symfony 生態 | 學習曲線稍高 | Symfony/Laravel 專案、長期維護專案 |
| 原生 Socket | 零依賴、教學價值高、極度精簡環境可用 | 需自行處理 HTTPS、Chunked、Keep-Alive、重導向、錯誤重試、超時細節 | 教學、嵌入式/受限環境、協議除錯 |
<?php
function http_request(string $url, string $method = 'GET', array $data = [], array $headers = [], int $timeout = 10): array {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true, // 生產必須驗證
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => array_map(fn($k,$v)=>"$k: $v", array_keys($headers), $headers),
CURLOPT_HEADER => true, // 回應含標頭
]);
if ($data) {
if ($method === 'GET') {
$url .= (strpos($url, '?') ? '&' : '?') . http_build_query($data);
curl_setopt($ch, CURLOPT_URL, $url);
} else {
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
}
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
return ['success' => false, 'error' => $error, 'errno' => $errno];
}
$headerSize = $info['header_size'];
$respHeaders = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
return [
'success' => true,
'status' => $info['http_code'],
'headers' => $respHeaders,
'body' => $body,
'info' => $info,
];
}
?>
理解 Socket 底層原理有助於深度除錯,但**生產環境請務必使用 cURL 或 Guzzle 等成熟庫**,專注業務邏輯而非協議細節。