fsockopen() 建立 TCP 連線到目標主機埠,手動構造 HTTP 協議封包(請求行、Header、Body),發送後讀取原始回應。這是 PHP 底層網路通訊基礎,理解它有助於掌握 HTTP 協議細節,但實務開發強烈建議使用 cURL 或 Guzzle。
<?php
class SocketHttpClient
{
private $host;
private $port;
private $timeout = 30;
private $ssl = false;
private $fp;
private $errno = 0;
private $errstr = '';
public function __construct(string $url)
{
$parsed = parse_url($url);
$this->host = $parsed['host'];
$this->port = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80);
$this->ssl = ($parsed['scheme'] === 'https');
$this->path = $parsed['path'] ?? '/';
$this->query = $parsed['query'] ?? '';
}
public function setTimeout(int $seconds): self
{
$this->timeout = $seconds;
return $this;
}
private function connect(): bool
{
$target = ($this->ssl ? 'ssl://' : 'tcp://') . $this->host;
$this->fp = @fsockopen($target, $this->port, $this->errno, $this->errstr, $this->timeout);
if (!$this->fp) {
throw new \RuntimeException("連線失敗 [$this->errno]: $this->errstr");
}
// 設定非阻塞與逾時
stream_set_timeout($this->fp, $this->timeout);
stream_set_blocking($this->fp, true);
return true;
}
private function buildRequest(string $method, array $headers, string $body): string
{
$url = $this->path . ($this->query ? '?' . $this->query : '');
$request = "$method $url HTTP/1.1\r\n";
$request .= "Host: {$this->host}\r\n";
$request .= "Connection: Close\r\n";
$request .= "User-Agent: PHP-SocketClient/1.0\r\n";
foreach ($headers as $k => $v) {
$request .= "$k: $v\r\n";
}
if ($body) {
$request .= "Content-Length: " . strlen($body) . "\r\n";
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
}
$request .= "\r\n";
$request .= $body;
return $request;
}
private function send(string $request): void
{
$written = fwrite($this->fp, $request);
if ($written === false || $written !== strlen($request)) {
throw new \RuntimeException('請求發送不完整');
}
}
private function readResponse(): array
{
$response = '';
while (!feof($this->fp)) {
$chunk = fread($this->fp, 8192);
if ($chunk === false) break;
$response .= $chunk;
}
fclose($this->fp);
// 解析回應
list($headerBlock, $body) = explode("\r\n\r\n", $response, 2);
$headerLines = explode("\r\n", $headerBlock);
$statusLine = array_shift($headerLines);
// 解析狀態碼
preg_match('/HTTP\/\d\.\d\s+(\d{3})/', $statusLine, $matches);
$statusCode = (int)($matches[1] ?? 0);
// 解析 Header
$headers = [];
foreach ($headerLines as $line) {
if (strpos($line, ':') !== false) {
list($k, $v) = explode(':', $line, 2);
$headers[trim($k)] = trim($v);
}
}
// 處理 chunked 編碼
if (isset($headers['Transfer-Encoding']) && stripos($headers['Transfer-Encoding'], 'chunked') !== false) {
$body = $this->decodeChunked($body);
}
return [
'status' => $statusCode,
'headers' => $headers,
'body' => $body,
];
}
private function decodeChunked(string $data): string
{
$result = '';
$pos = 0;
$len = strlen($data);
while ($pos < $len) {
$lineEnd = strpos($data, "\r\n", $pos);
if ($lineEnd === false) break;
$chunkLen = hexdec(trim(substr($data, $pos, $lineEnd - $pos)));
if ($chunkLen === 0) break;
$pos = $lineEnd + 2;
$result .= substr($data, $pos, $chunkLen);
$pos += $chunkLen + 2; // 跳過 \r\n
}
return $result;
}
public function get(array $params = [], array $headers = []): array
{
$query = http_build_query($params);
$fullQuery = $this->query ? $this->query . '&' . $query : $query;
$this->connect();
$request = $this->buildRequest('GET', $headers, '');
$this->send($request);
return $this->readResponse();
}
public function post(array $data = [], array $headers = []): array
{
$body = http_build_query($data);
$this->connect();
$request = $this->buildRequest('POST', $headers, $body);
$this->send($request);
return $this->readResponse();
}
public function __destruct()
{
if ($this->fp && is_resource($this->fp)) {
fclose($this->fp);
}
}
}
// GET 請求
$client = new SocketHttpClient('https://api.example.com/users');
$response = $client->setTimeout(10)->get(['page' => 1, 'limit' => 20]);
echo "Status: {$response['status']}\n";
echo "Body: {$response['body']}\n";
// POST 請求
$client = new SocketHttpClient('https://api.example.com/login');
$response = $client->post([
'username' => 'admin',
'password' => 'secret'
], [
'X-API-Key' => 'your-key',
'Accept' => 'application/json'
]);
| 問題 | 原因 | 解決 |
|---|---|---|
| SSL 憑證驗證失敗 | 預設不驗證 peer,但部分環境需 CA | stream_context_set_option 設定 verify_peer=false 或指定 cafile |
| 大回應截斷 | fread 預設 8192,需迴圈讀取至 feof | while (!feof($fp)) { $data .= fread($fp, 8192); } |
| Chunked 編碼解析錯誤 | HTTP/1.1 預設啟用 Transfer-Encoding: chunked | 實作 decodeChunked() 或強制 HTTP/1.0 |
| 中文亂碼 | 未指定 Content-Type charset | Header 加入 charset=utf-8,確保檔案編碼為 UTF-8 |
| 連線逾時不生效 | fsockopen timeout 僅適用連線階段 | stream_set_timeout() 設定讀寫逾時 |
$ch = curl_init('https://api.example.com/users');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['name' => 'test']),
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false, // 開發環境
]);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://api.example.com/',
'timeout' => 10.0,
'verify' => false, // 開發環境
]);
// 同步請求
$response = $client->post('users', [
'form_params' => ['name' => 'test'],
'headers' => ['Accept' => 'application/json']
]);
$data = json_decode($response->getBody()->getContents(), true);
// 非同步並發請求
$promises = [
'users' => $client->getAsync('users'),
'posts' => $client->getAsync('posts'),
'comments' => $client->getAsync('comments'),
];
$results = \GuzzleHttp\Promise\Utils::unwrap($promises);
use Symfony\Contracts\HttpClient\HttpClientInterface;
$client = new \Symfony\Component\HttpClient\HttpClient();
$response = $client->request('POST', 'https://api.example.com/users', [
'json' => ['name' => 'test'],
'headers' => ['Accept' => 'application/json'],
]);
$content = $response->toArray();
| 方案 | 平均耗時 | 記憶體 | 特點 |
|---|---|---|---|
| fsockopen 手工 | ~450ms | 低 | 無依賴、教學用、維護成本極高 |
| cURL 原生 | ~120ms | 低 | 效能最強、功能完整、API 較繁瑣 |
| Guzzle 7 | ~150ms | 中 | 介面優雅、中介軟體、非同步、測試友善 |
| Symfony HttpClient | ~140ms | 中 | PSR-18 相容、自動重試、快取支援 |
fsockopen 是理解 HTTP 協議與 TCP 通訊的優秀教學工具,但生產環境請勿直接使用。現代 PHP 專案應採用 Guzzle 或 Symfony HttpClient,享受完善的異常處理、中介軟體管線、非同步併發、測試模擬等企業級特性。若專案極度精簡不可引入依賴,cURL 原生函數 是最佳平衡點。