不依賴 cURL、Guzzle 等外部庫,直接用 fsockopen 建立 TCP 連線,手工組裝 HTTP 請求標頭,即可實現頁面抓取、API 呼叫、壓力測試等功能。本文提供完整可用的封裝範例。
function httpRequestBySocket(string $url, array $options = []): string {
$defaults = [
'method' => 'GET',
'data' => [],
'headers' => [],
'timeout' => 10,
'port' => 80,
];
$opts = array_merge($defaults, $options);
$parsed = parse_url($url);
$host = $parsed['host'];
$path = $parsed['path'] ?? '/';
if (!empty($parsed['query'])) $path .= '?' . $parsed['query'];
$port = $parsed['port'] ?? $opts['port'];
$scheme = $parsed['scheme'] ?? 'http';
// 建立連線
$fp = @fsockopen($host, $port, $errno, $errstr, $opts['timeout']);
if (!$fp) {
throw new RuntimeException("Socket 連線失敗 [$errno]: $errstr");
}
stream_set_timeout($fp, $opts['timeout']);
// 組裝請求
$request = "{$opts['method']} $path HTTP/1.1\r\n";
$request .= "Host: $host\r\n";
$request .= "Connection: Close\r\n";
$request .= "User-Agent: PHP-Socket/1.0\r\n";
$request .= "Accept: */*\r\n";
// 自訂標頭
foreach ($opts['headers'] as $k => $v) {
$request .= "$k: $v\r\n";
}
// POST 資料
if ($opts['method'] === 'POST' && !empty($opts['data'])) {
$body = is_array($opts['data']) ? http_build_query($opts['data']) : $opts['data'];
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
$request .= "Content-Length: " . strlen($body) . "\r\n";
$request .= "\r\n$body";
} else {
$request .= "\r\n";
}
// 發送
fwrite($fp, $request);
// 讀取回應(分離標頭與內容)
$response = '';
$headerDone = false;
while (!feof($fp)) {
$line = fgets($fp, 8192);
if (!$headerDone) {
if ($line === "\r\n" || $line === "\n") {
$headerDone = true;
}
} else {
$response .= $line;
}
}
fclose($fp);
return $response;
}
$html = httpRequestBySocket('https://httpbin.org/get?foo=bar', [
'timeout' => 15,
'headers' => ['Accept-Language' => 'zh-TW']
]);
$result = httpRequestBySocket('https://httpbin.org/post', [
'method' => 'POST',
'data' => ['username' => 'test', 'password' => '123456'],
'headers' => [
'Cookie' => 'session_id=abc123; theme=dark',
'X-Custom-Header' => 'MyValue'
],
'timeout' => 20
]);
將 fsockopen 改為 stream_socket_client 並加入 SSL 上下文:
$context = stream_context_create([
'ssl' => [
'verify_peer' => false, // 生產環境請設 true 並配置 CA
'verify_peer_name' => false,
]
]);
$fp = stream_socket_client("ssl://$host:$port", $errno, $errstr, $opts['timeout'], STREAM_CLIENT_CONNECT, $context);
若回應含 Transfer-Encoding: chunked,需手動解碼:
function decodeChunked(string $data): string {
$output = '';
$pos = 0;
while ($pos < strlen($data)) {
$lenLineEnd = strpos($data, "\r\n", $pos);
if ($lenLineEnd === false) break;
$length = hexdec(trim(substr($data, $pos, $lenLineEnd - $pos)));
if ($length === 0) break;
$chunkStart = $lenLineEnd + 2;
$output .= substr($data, $chunkStart, $length);
$pos = $chunkStart + $length + 2; // 跳過 \r\n
}
return $output;
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| fsockopen | 零依賴、完全控制、教學價值高 | 手動處理 HTTPS/Chunked/Keep-Alive/Redirect、易出錯 | 嵌入式環境、特殊協定、學習 HTTP 協定 |
| cURL | 功能完整、效能佳、支援所有 HTTP 特性 | 需安裝擴充 | 生產環境首選 |
| Guzzle | 物件導向、中介軟體、非同步、測試友善 | Composer 依賴、記憶體較大 | 現代 PHP 專案、微服務呼叫 |
\r\n\r\nHost 標頭在 HTTP/1.1 必填Connection: Keep-Alive 並複用連線stream_set_timeout 避免卡死掌握 Socket 底層運作,能在無法安裝擴充的受限環境中完成 HTTP 整合,也是理解高層庫原理的最佳途徑。