首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP fsockopen 模擬 HTTP 請求完整指南:GET/POST 實作、錯誤處理與現代替代方案

     編輯:PHP 綜合知識

fsockopen 底層原理

fsockopen() 建立 TCP 連線到目標主機埠,手動構造 HTTP 協議封包(請求行、Header、Body),發送後讀取原始回應。這是 PHP 底層網路通訊基礎,理解它有助於掌握 HTTP 協議細節,但實務開發強烈建議使用 cURL 或 Guzzle。

基礎實作:封裝可復用的 HttpClient 類

<?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,但部分環境需 CAstream_context_set_option 設定 verify_peer=false 或指定 cafile
大回應截斷fread 預設 8192,需迴圈讀取至 feofwhile (!feof($fp)) { $data .= fread($fp, 8192); }
Chunked 編碼解析錯誤HTTP/1.1 預設啟用 Transfer-Encoding: chunked實作 decodeChunked() 或強制 HTTP/1.0
中文亂碼未指定 Content-Type charsetHeader 加入 charset=utf-8,確保檔案編碼為 UTF-8
連線逾時不生效fsockopen timeout 僅適用連線階段stream_set_timeout() 設定讀寫逾時

現代替代方案對比

1. cURL (PHP 原生擴充,效能最佳)

$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);

2. Guzzle 7 (現代 PHP 專案標準,Composer 安裝)

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);

3. Symfony HttpClient (Laravel/Symfony 專案首選)

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();

效能對比 (參考值,100 次請求)

方案平均耗時記憶體特點
fsockopen 手工~450ms低無依賴、教學用、維護成本極高
cURL 原生~120ms低效能最強、功能完整、API 較繁瑣
Guzzle 7~150ms中介面優雅、中介軟體、非同步、測試友善
Symfony HttpClient~140ms中PSR-18 相容、自動重試、快取支援

總結

fsockopen 是理解 HTTP 協議與 TCP 通訊的優秀教學工具,但生產環境請勿直接使用。現代 PHP 專案應採用 Guzzle 或 Symfony HttpClient,享受完善的異常處理、中介軟體管線、非同步併發、測試模擬等企業級特性。若專案極度精簡不可引入依賴,cURL 原生函數 是最佳平衡點。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved