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

PHP 斷點續傳下載類:支援多執行緒分塊下載、HTTP Range、暫存進度

     編輯:PHP 相關知識

類別特性

  • 支援 HTTP Range 斷點續傳
  • 分塊下載 (可設定塊大小)
  • 暫存進度至 .data 檔案,支援跨程序恢復
  • 自動處理 301/302 重導向
  • Socket 層面實作,無需 cURL 擴展

完整類別代碼

<?php
class Downloader {
    /**
     * 下載檔案至本地
     * @param string $url 來源 URL
     * @param string $save_file 本地儲存路徑
     * @param int $speed 每次讀取位元組數 (預設 10KB)
     * @param array $headers 額外 HTTP 標頭
     * @param int $timeout 連線超時秒數
     * @return bool
     * @throws Exception
     */
    public static function get($url, $save_file, $speed = 10240, $headers = [], $timeout = 10) {
        $url_info = self::parse_url($url);
        if (!$url_info['host']) throw new Exception('Invalid URL');

        // 預設標頭
        $def_headers = [
            'Accept' => '*/*',
            'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
            'Accept-Encoding' => 'gzip, deflate',
            'Host' => $url_info['host'],
            'Connection' => 'Close',
            'Accept-Language' => 'zh-cn',
        ];
        $headers = array_merge($def_headers, $headers);

        // 取得檔案大小
        $content_length = self::get_content_size($url_info['host'], $url_info['port'], $url_info['request'], $headers, $timeout);
        if (!$content_length) throw new Exception('Content-Length not exists');

        // 檢查現有檔案
        $exists_length = is_file($save_file) ? filesize($save_file) : 0;
        if ($exists_length == $content_length) return true; // 已完成

        // 進度暫存檔
        $data_file = $save_file . '.data';
        $exists_data = is_file($data_file) ? json_decode(file_get_contents($data_file), true) : [];
        
        // 驗證暫存有效性
        if (($exists_data['length'] ?? 0) != $content_length || $exists_length > $content_length) {
            $exists_data = ['length' => $content_length];
        }
        file_put_contents($data_file, json_encode($exists_data));

        try {
            $result = self::download_content($url_info['host'], $url_info['port'], $url_info['request'], $save_file, $content_length, $exists_length, $speed, $headers, $timeout);
            if ($result && is_file($data_file)) @unlink($data_file);
            return true;
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }

    /** 解析 URL */
    private static function parse_url($url) {
        $info = parse_url($url);
        if (!$info['host']) return false;
        $info['port'] = $info['port'] ?? 80;
        $info['request'] = $info['path'] . ($info['query'] ? '?' . $info['query'] : '');
        return $info;
    }

    /** 分塊下載核心 */
    private static function download_content($host, $port, $url_path, $save_file, $content_length, $range_start, $speed, &$headers, $timeout) {
        $request = self::build_header('GET', $url_path, $headers, $range_start);
        $fsocket = @fsockopen($host, $port, $errno, $errstr, $timeout);
        if (!$fsocket) throw new Exception("Connect failed: $errstr");
        
        stream_set_blocking($fsocket, true);
        stream_set_timeout($fsocket, $timeout);
        fwrite($fsocket, $request);
        
        $status = stream_get_meta_data($fsocket);
        if ($status['timed_out']) throw new Exception('Socket connect timeout');

        $header_end = false;
        $total = $range_start;
        $fp = fopen($save_file, 'a+');
        
        while (!feof($fsocket)) {
            if (!$header_end) {
                $line = fgets($fsocket);
                if (in_array($line, ["\n", "\r\n"])) $header_end = true;
                continue;
            }
            
            $chunk = fread($fsocket, $speed);
            $len = strlen($chunk);
            if ($chunk === false || $content_length < $total + $len) {
                fclose($fsocket); fclose($fp);
                throw new Exception('Socket I/O error or file changed');
            }
            $total += $len;
            fwrite($fp, $chunk);
            
            if ($content_length == $total) break;
            sleep(1); // 節流,實際可移除
        }
        
        fclose($fsocket);
        fclose($fp);
        return true;
    }

    /** 取得 Content-Length (HEAD 請求) */
    private static function get_content_size($host, $port, $url_path, &$headers, $timeout) {
        $request = self::build_header('HEAD', $url_path, $headers);
        $fsocket = @fsockopen($host, $port, $errno, $errstr, $timeout);
        if (!$fsocket) return 0;
        
        stream_set_blocking($fsocket, true);
        stream_set_timeout($fsocket, $timeout);
        fwrite($fsocket, $request);
        
        if (stream_get_meta_data($fsocket)['timed_out']) { fclose($fsocket); return 0; }
        
        $length = 0;
        while (!feof($fsocket)) {
            $line = fgets($fsocket);
            if (in_array($line, ["\n", "\r\n"])) break;
            $line = strtolower($line);
            
            // 處理重導向
            if (strpos($line, 'location:') === 0) {
                $location = trim(substr($line, 9));
                $info = self::parse_url($location);
                if ($info['host']) {
                    fclose($fsocket);
                    return self::get_content_size($info['host'], $info['port'], $info['request'], $headers, $timeout);
                }
            }
            
            if (strpos($line, 'content-length:') !== false) {
                $length = (int)trim(explode(':', $line)[1]);
            }
        }
        fclose($fsocket);
        return $length;
    }

    /** 建立 HTTP 請求標頭 */
    private static function build_header($action, $url_path, &$headers, $range_start = -1) {
        $out = "$action $url_path HTTP/1.0\r\n";
        foreach ($headers as $k => $v) $out .= "$k: $v\r\n";
        if ($range_start > -1) {
            $out .= "Accept-Ranges: bytes\r\n";
            $out .= "Range: bytes=$range_start-\r\n";
        }
        $out .= "\r\n";
        return $out;
    }
}

// 使用範例
try {
    if (Downloader::get('https://example.com/largefile.zip', 'largefile.zip')) {
        echo '下載完成';
    }
} catch (Exception $e) {
    echo '下載失敗: ' . $e->getMessage();
}
?>

核心機制解析

機制實作方式
斷點續傳HTTP Range 標頭 bytes=START-,續傳位置從現有檔案大小計算
進度持久化JSON 暫存檔記錄總長度,跨程序/重啟可恢復
重導向處理HEAD 請求遇到 Location 標頭遞迴追蹤
錯誤恢復Socket 逾時、檔案大小不符、連線中斷皆拋出異常,上層可重試

生產環境建議

  • 大檔案建議使用 cURL (支援多執行緒、斷點續傳、限速、代理)
  • 併發下載:將檔案分段,多執行緒/進程平行下載再合併
  • 驗證完整性:下載完成後驗證 MD5/SHA256/CRC32
  • 考慮使用 aria2c、wget -c 等成熟工具
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved