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

PHP HTTP 下載類完整實現:支援 HTTP/HTTPS、Header 解析、斷點續傳、大文件流式處理

     編輯:PHP 相關知識

類設計目標

  • 封裝 Socket 底層通訊,不依賴 cURL 擴展
  • 支援 HTTP/1.1、HTTPS (SSL)、自定義 Header、Cookie、User-Agent
  • 流式下載大文件(不佔用 PHP 記憶體)
  • 解析回應 Header,提取狀態碼、Content-Type、Content-Length、Set-Cookie 等
  • 支援斷點續傳

核心類代碼

<?php
declare(strict_types=1);

class HttpDownloader
{
    private string $url = '';
    private string $scheme = 'http';
    private string $host = '';
    private int $port = 80;
    private string $path = '/';
    private string $query = '';
    private string $user = '';
    private string $pass = '';
    
    private $fp = null;           // Socket 資源
    private array $responseHeaders = [];
    private string $responseBody = '';
    private string $error = '';
    private int $statusCode = 0;
    
    // 下載進度回調
    public ?callable $progressCallback = null;
    
    public function __construct(private int $connectTimeout = 10, private int $readTimeout = 30) {}

    /** 設定目標 URL 並解析 */
    public function setUrl(string $url): self
    {
        $this->reset();
        $this->url = $url;
        $parts = parse_url($url);
        if (!$parts) throw new InvalidArgumentException("無效 URL: $url");
        
        $this->scheme = $parts['scheme'] ?? 'http';
        $this->host   = $parts['host'] ?? throw new InvalidArgumentException("缺少 Host");
        $this->port   = $parts['port'] ?? ($this->scheme === 'https' ? 443 : 80);
        $this->path   = $parts['path'] ?? '/';
        $this->query  = $parts['query'] ?? '';
        $this->user   = $parts['user'] ?? '';
        $this->pass   = $parts['pass'] ?? '';
        return $this;
    }

    /** 執行 GET 請求並下載到文件(流式) */
    public function downloadToFile(string $savePath, int $offset = 0): bool
    {
        $this->openConnection();
        $this->sendRequest('GET', $offset);
        $this->readResponseHeaders();
        
        if ($this->statusCode >= 400) {
            $this->error = "HTTP {$this->statusCode}: " . $this->getHeader('status');
            return false;
        }
        
        $fpOut = fopen($savePath, $offset ? 'ab' : 'wb');
        if (!$fpOut) {
            $this->error = "無法創建本地文件: $savePath";
            return false;
        }
        
        $totalSize = (int)$this->getHeader('content-length');
        $downloaded = $offset;
        $bufferSize = 8192;
        
        while (!feof($this->fp)) {
            $chunk = fread($this->fp, $bufferSize);
            if ($chunk === '') break;
            fwrite($fpOut, $chunk);
            $downloaded += strlen($chunk);
            
            if ($this->progressCallback) {
                ($this->progressCallback)($downloaded, $totalSize, $this->statusCode);
            }
        }
        
        fclose($fpOut);
        $this->close();
        return true;
    }

    /** 獲取完整回應 (頭+體),適用於小文件/API */
    public function get(string $url): array
    {
        $this->setUrl($url);
        $this->openConnection();
        $this->sendRequest('GET');
        $this->readResponseHeaders();
        $this->readResponseBody();
        $this->close();
        return [
            'status'  => $this->statusCode,
            'headers' => $this->responseHeaders,
            'body'    => $this->responseBody,
        ];
    }

    /** POST 請求 */
    public function post(string $url, array $data = [], array $headers = []): array
    {
        $this->setUrl($url);
        $this->openConnection();
        $body = http_build_query($data);
        $this->sendRequest('POST', 0, $headers, $body);
        $this->readResponseHeaders();
        $this->readResponseBody();
        $this->close();
        return [
            'status'  => $this->statusCode,
            'headers' => $this->responseHeaders,
            'body'    => $this->responseBody,
        ];
    }

    // ---------- 內部方法 ----------
    
    private function reset(): void
    {
        $this->close();
        $this->responseHeaders = [];
        $this->responseBody = '';
        $this->error = '';
        $this->statusCode = 0;
    }

    private function openConnection(): void
    {
        $target = ($this->scheme === 'https' ? 'ssl://' : 'tcp://') . $this->host . ':' . $this->port;
        $this->fp = @stream_socket_client(
            $target,
            $errno,
            $errstr,
            $this->connectTimeout,
            STREAM_CLIENT_CONNECT,
            stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]])
        );
        if (!$this->fp) {
            throw new RuntimeException("連接失敗: {$errstr} ({$errno})");
        }
        stream_set_timeout($this->fp, $this->readTimeout);
        
        // HTTP/1.1 需要 Host header
        if ($this->scheme === 'https') {
            stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
        }
    }

    private function sendRequest(string $method, int $rangeStart = 0, array $extraHeaders = [], string $body = ''): void
    {
        $requestPath = $this->path . ($this->query ? '?' . $this->query : '');
        
        $headers = [
            "{$method} {$requestPath} HTTP/1.1",
            "Host: {$this->host}",
            "User-Agent: PHP HttpDownloader/1.0",
            "Connection: Close",
            "Accept: */*",
        ];
        
        if ($this->user || $this->pass) {
            $auth = base64_encode($this->user . ':' . $this->pass);
            $headers[] = "Authorization: Basic {$auth}";
        }
        
        if ($rangeStart > 0) {
            $headers[] = "Range: bytes={$rangeStart}-";
        }
        
        if ($body) {
            $headers[] = "Content-Type: application/x-www-form-urlencoded";
            $headers[] = "Content-Length: " . strlen($body);
        }
        
        $headers = array_merge($headers, $extraHeaders);
        $request = implode("\r\n", $headers) . "\r\n\r\n" . $body;
        
        fwrite($this->fp, $request);
    }

    private function readResponseHeaders(): void
    {
        $this->responseHeaders = [];
        while (($line = fgets($this->fp, 4096)) !== false) {
            $line = rtrim($line, "\r\n");
            if ($line === '') break; // Headers 結束
            $this->responseHeaders[] = $line;
            
            // 解析狀態行
            if (str_starts_with($line, 'HTTP/')) {
                if (preg_match('/HTTP\/\d\.\d\s+(\d{3})/', $line, $m)) {
                    $this->statusCode = (int)$m[1];
                }
            }
        }
    }

    private function readResponseBody(): void
    {
        $this->responseBody = '';
        while (!feof($this->fp)) {
            $this->responseBody .= fread($this->fp, 8192);
        }
    }

    private function close(): void
    {
        if (is_resource($this->fp)) {
            fclose($this->fp);
            $this->fp = null;
        }
    }

    public function getHeader(string $name): ?string
    {
        $name = strtolower($name);
        foreach ($this->responseHeaders as $header) {
            if (str_starts_with(strtolower($header), $name . ':')) {
                return trim(substr($header, strlen($name) + 1));
            }
        }
        return null;
    }

    public function getAllHeaders(): array
    {
        return $this->responseHeaders;
    }

    public function getError(): string
    {
        return $this->error;
    }

    public function getStatusCode(): int
    {
        return $this->statusCode;
    }

    public function __destruct()
    {
        $this->close();
    }
}

使用範例

$downloader = new HttpDownloader(connectTimeout: 10, readTimeout: 60);

// 1. 簡單 GET 獲取 API 數據
$api = $downloader->get('https://api.example.com/v1/user/123');
if ($api['status'] === 200) {
    $data = json_decode($api['body'], true);
    var_dump($data);
}

// 2. 大文件下載 + 進度條
$url = 'https://releases.ubuntu.com/22.04.3/ubuntu-22.04.3-desktop-amd64.iso';
$save = '/tmp/ubuntu.iso';

$downloader->progressCallback = function(int $downloaded, int $total, int $status) {
    static $lastPct = -1;
    if ($total > 0) {
        $pct = (int)($downloaded / $total * 100);
        if ($pct !== $lastPct) {
            echo "\r進度: {$pct}% ({$downloaded}/{$total} bytes)";
            $lastPct = $pct;
        }
    }
};

if ($downloader->downloadToFile($save)) {
    echo "\n下載完成: {$save}\n";
} else {
    echo "\n下載失敗: " . $downloader->getError() . "\n";
}

// 3. 斷點續傳
$offset = filesize($save) ?? 0;
if ($offset > 0) {
    echo "續傳從: {$offset} bytes\n";
    $downloader->downloadToFile($save, $offset);
}

與 cURL/Guzzle 對比

特性HttpDownloader (Socket)cURLGuzzle
依賴無 (核心流)php_curl.dll/soComposer 包
HTTP/2不支援支援支援
並發/異步需自行實現 stream_selectcurl_multiPromise/異步
SSL 驗證簡易 (可關閉)完整 CA 鏈完整
Cookie 罐手動管理內建內建
重定向跟隨手動處理 3xxCURLOPT_FOLLOWLOCATION自動
大文件記憶體流式,常數記憶體流式寫入文件流式/PSR-7

適用場景建議

  • HttpDownloader:極簡環境(無 cURL)、教學演示、嵌入式 PHP、需完全控制 Socket 行為
  • cURL:生產環境標準選擇,功能完整、性能優異、支援所有協議特性
  • Guzzle / Symfony HTTP Client:現代 PHP 專案,PSR-7/18 標準、中間件、測試雙、異步支援

總結

本類展示了 PHP 底層 Socket 實現 HTTP 客戶端的核心邏輯:解析 URL → 建立 TCP/SSL 連接 → 發送請求行/頭 → 讀取回應頭/體 → 流式寫入文件。理解這一流程對排查網絡異常、實現自定義協議、優化大文件傳輸極有幫助。實際項目中,請優先選擇 Guzzle 或 cURL,享受成熟生態與安全保障。

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