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

PHP 使用 fsockopen 模擬 HTTP 請求獲取頁面內容完整指南

     編輯:PHP 綜合知識

適用場景與現代替代

fsockopen 是 PHP 底層 Socket 操作函數,適用於:

  • 環境禁用 cURL/stream 處理 HTTP
  • 教學理解 HTTP 協議細節
  • 特殊協議調試(如原始 SMTP、自定義 TCP)

生產建議: 優先使用 GuzzleHttp、Symfony HttpClient 或原生 file_get_contents() (配合 stream_context),代碼更健壯、支持 HTTPS/HTTP2/自動重定向/超時控制。

核心實現:手動構造 HTTP/1.1 請求

<?php
/**
 * 使用 fsockopen 發送 HTTP 請求並獲取響應體
 *
 * @param string $url 目標 URL (支持 http/https)
 * @param array  $options 選項: method, headers, body, timeout, ssl_verify
 * @return string 響應體
 * @throws RuntimeException 連接/協議錯誤
 */
function httpRequestBySocket(string $url, array $options = []): string
{
    $opts = array_merge([
        'method'      => 'GET',
        'headers'     => [],
        'body'        => '',
        'timeout'     => 10.0,
        'ssl_verify'  => true, // HTTPS 是否驗證證書
    ], $options);

    $parsed = parse_url($url);
    if (!$parsed || !isset($parsed['host'])) {
        throw new InvalidArgumentException('Invalid URL: ' . $url);
    }

    $host = $parsed['host'];
    $port = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80);
    $path = ($parsed['path'] ?? '/') . (isset($parsed['query']) ? '?' . $parsed['query'] : '');

    // 構造 Host 頭 (HTTP/1.1 必須)
    $headers = $opts['headers'];
    $headers['Host'] = $host;
    if ($opts['body'] !== '' && !isset($headers['Content-Length'])) {
        $headers['Content-Length'] = strlen($opts['body']);
    }
    if ($opts['body'] !== '' && !isset($headers['Content-Type'])) {
        $headers['Content-Type'] = 'application/x-www-form-urlencoded';
    }

    // 建立連接 (支持 SSL/TLS)
    $schemePrefix = ($parsed['scheme'] === 'https') ? 'ssl://' : '';
    $target = $schemePrefix . $host . ':' . $port;

    $flags = STREAM_CLIENT_CONNECT;
    $context = stream_context_create([
        'ssl' => [
            'verify_peer'      => $opts['ssl_verify'],
            'verify_peer_name' => $opts['ssl_verify'],
            'allow_self_signed' => !$opts['ssl_verify'],
        ]
    ]);

    $fp = stream_socket_client($target, $errno, $errstr, $opts['timeout'], $flags, $context);
    if (!$fp) {
        throw new RuntimeException("Connect failed [$errno]: $errstr");
    }

    stream_set_timeout($fp, $opts['timeout']);

    // 發送請求行
    $request = "{$opts['method']} $path HTTP/1.1\r\n";
    // 發送頭部
    foreach ($headers as $k => $v) {
        $request .= "$k: $v\r\n";
    }
    $request .= "\r\n"; // 空行結束頭部
    // 發送體
    $request .= $opts['body'];

    if (fwrite($fp, $request) === false) {
        fclose($fp);
        throw new RuntimeException('Write request failed');
    }

    // 讀取響應
    $response = '';
    $headerPart = '';
    $inHeader = true;
    $contentLength = null;
    $chunked = false;

    while (!feof($fp)) {
        $line = fgets($fp, 8192);
        if ($line === false) break;

        if ($inHeader) {
            $headerPart .= $line;
            if (strpos($line, 'Content-Length:') !== false) {
                $contentLength = (int)trim(explode(':', $line, 2)[1]);
            }
            if (stripos($line, 'Transfer-Encoding: chunked') !== false) {
                $chunked = true;
            }
            // 頭部結束標記
            if ($line === "\r\n" || $line === "\n") {
                $inHeader = false;
            }
        } else {
            $response .= $line;
        }
    }
    fclose($fp);

    // 處理 Chunked 編碼 (簡易版,生產建議用成熟解析器)
    if ($chunked) {
        $response = decodeChunked($response);
    } elseif ($contentLength !== null) {
        $response = substr($response, 0, $contentLength);
    }

    return $response;
}

/** 簡易 Chunked 解碼 */
function decodeChunked(string $data): string
{
    $out = '';
    $pos = 0;
    $len = strlen($data);
    while ($pos < $len) {
        $lineEnd = strpos($data, "\r\n", $pos);
        if ($lineEnd === false) break;
        $chunkSize = hexdec(trim(substr($data, $pos, $lineEnd - $pos)));
        $pos = $lineEnd + 2;
        if ($chunkSize === 0) break; // 最後一塊
        $out .= substr($data, $pos, $chunkSize);
        $pos += $chunkSize + 2; // 跳過數據和 \r\n
    }
    return $out;
}

// ========== 使用示例 ==========
try {
    // GET 請求
    $html = httpRequestBySocket('https://httpbin.org/get');
    echo "GET 響應長度: " . strlen($html) . "\n";

    // POST XML
    $xml = '<order><id>123</id></order>';
    $result = httpRequestBySocket('https://httpbin.org/post', [
        'method'  => 'POST',
        'headers' => ['Content-Type' => 'application/xml'],
        'body'    => $xml,
    ]);
    echo "POST 響應: $result\n";

    // 攜帶 Cookie
    $withCookie = httpRequestBySocket('https://httpbin.org/cookies', [
        'headers' => ['Cookie' => 'session=abc123; user=john'],
    ]);
    echo "Cookie 響應: $withCookie\n";

} catch (Throwable $e) {
    error_log('Socket HTTP Error: ' . $e->getMessage());
}
?>

關鍵技術點深度解析

1. HTTP/1.1 強制要求 Host 頭

虛擬主機環境下,服務器依靠 Host 頭路由請求,缺失將返回 400 Bad Request。

2. HTTPS 處理:stream_socket_client + SSL Context

fsockopen 不直接支持 SSL,需用 stream_socket_client('ssl://host:443') 並配置 stream_context_create(['ssl' => [...]) 實現證書驗證、SNI 等。

3. 響應解析難點:Content-Length vs Transfer-Encoding: chunked

  • Content-Length:固定長度,讀滿即止。
  • Chunked:分塊傳輸,需解析十六進制長度前綴,直到 0 塊結束。
  • 無長度且非 Chunked:靠 Connection: close 斷開連接判斷結束(HTTP/1.0 行為)。

4. 超時控制:stream_set_timeout

分別控制**連接超時** (stream_socket_client 第 4 參數) 和**讀寫超時** (stream_set_timeout),避免死掛。

5. 保持連接

發送 Connection: keep-alive 並複用 $fp 可提升性能,但需嚴格按 Content-Length/Chunked 讀取完整響應,否則管道會亂序。

常見坑與調試技巧

現象原因解決
返回亂碼/壓縮內容服務端返回 gzip/deflate,未解壓請求頭加 Accept-Encoding: identity 禁用壓縮,或用 gzinflate()/gzdecode() 解壓
HTTPS 證書驗證失敗自簽證書/CA 不信任開發環境 'verify_peer'=>false,生產**必須**配置正確 CA 憑證路徑
POST 數據服務端收不到缺少 Content-Length 或 Content-Type手動計算 strlen($body) 設置 Content-Length,設置正確 Content-Type
中文參數亂碼URL 編碼/Body 編碼不一致GET 參數用 urlencode(),POST Body 按 Content-Type 編碼 (JSON 用 utf-8)
大文件下載記憶體爆炸將整個響應存入變量邊讀邊寫文件/輸出,不要累積 $response 字符串

對比:為什麼現代項目不直接用 fsockopen?

<?php
// 現代寫法 (Guzzle 7+)
$client = new \GuzzleHttp\Client(['timeout' => 10]);
$response = $client->post('https://api.example.com', [
    'headers' => ['Content-Type' => 'application/xml'],
    'body'    => $xml,
    'verify'  => true, // 自動處理證書、重定向、Chunked、gzip、keep-alive、異步、重試...
]);
$data = $response->getBody()->getContents();
?>

**核心差距**:成熟客戶端處理了**重定向、Cookie 罐、連接池、異步並發、中介軟體擴展、PSR-18 標準化**,手寫 Socket 純屬造輪子。

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