PHP ftp_nb_get() 函数

PHP ftp_nb_get() 函数用于以非阻塞方式从FTP服务器下载文件到本地文件系统。

特点:此函数与 ftp_get() 类似,但以非阻塞模式运行,允许在文件传输过程中执行其他PHP代码。

语法

ftp_nb_get(
    resource $ftp,
    string $local_file,
    string $remote_file,
    int $mode = FTP_BINARY,
    int $resumepos = 0
): int

参数说明

参数 描述
ftp 必需。FTP连接的标识符,由ftp_connect()ftp_ssl_connect()返回
local_file 必需。本地文件路径,用于保存下载的数据
remote_file 必需。FTP服务器上要下载的文件路径
mode 可选。传输模式,可以是:
  • FTP_ASCII - 文本模式(ASCII)
  • FTP_BINARY - 二进制模式(默认)
resumepos 可选。从远程文件的指定位置开始下载(断点续传)

返回值

  • FTP_FAILED (0) - 传输失败
  • FTP_FINISHED (1) - 传输完成
  • FTP_MOREDATA (2) - 传输仍在继续

与ftp_get()的区别

特性 ftp_get() ftp_nb_get()
阻塞性 阻塞模式 非阻塞模式
执行方式 脚本等待传输完成 传输过程中可执行其他代码
适用场景 小文件或简单传输 大文件传输或需要并发的场景
返回值 布尔值(成功/失败) 状态码(FTP_FAILED/FTP_FINISHED/FTP_MOREDATA)
后续处理 直接完成 需要循环调用ftp_nb_continue()

示例

示例1:基本用法 - 非阻塞方式下载文件

<?php
// 连接FTP服务器
$ftp_server = "ftp.example.com";
$ftp_user = "username";
$ftp_pass = "password";

$ftp_conn = ftp_connect($ftp_server);
if (!$ftp_conn) {
    die("无法连接到 $ftp_server");
}

// 登录
if (!ftp_login($ftp_conn, $ftp_user, $ftp_pass)) {
    die("登录失败");
}

// 启用被动模式
ftp_pasv($ftp_conn, true);

// 开始非阻塞下载
$remote_file = "data/example.txt";
$local_file = "downloads/example.txt";

// 确保本地目录存在
$dir = dirname($local_file);
if (!is_dir($dir)) {
    mkdir($dir, 0755, true);
}

$result = ftp_nb_get($ftp_conn, $local_file, $remote_file, FTP_ASCII);

// 循环处理传输
$start_time = time();
$last_size = 0;

while ($result == FTP_MOREDATA) {
    // 获取当前文件大小
    if (file_exists($local_file)) {
        $current_size = filesize($local_file);

        // 计算下载速度
        $elapsed = time() - $start_time;
        if ($elapsed > 0) {
            $speed = ($current_size - $last_size) / $elapsed;
            echo "\r已下载: " . formatBytes($current_size) .
                 " | 速度: " . formatBytes($speed) . "/s";
            $last_size = $current_size;
            $start_time = time();
        }
    }

    // 继续传输
    $result = ftp_nb_continue($ftp_conn);

    // 添加延迟以避免过度占用CPU
    usleep(100000); // 0.1秒
}

// 检查传输结果
if ($result == FTP_FINISHED) {
    echo "\n文件下载完成!保存为: $local_file\n";

    // 验证文件大小
    $remote_size = ftp_size($ftp_conn, $remote_file);
    $local_size = filesize($local_file);

    if ($remote_size == $local_size) {
        echo "文件大小验证成功: $local_size 字节\n";
    } else {
        echo "警告: 远程文件大小 ($remote_size) 与本地文件大小 ($local_size) 不一致\n";
    }
} elseif ($result == FTP_FAILED) {
    echo "\n文件下载失败!\n";

    // 删除可能不完整的文件
    if (file_exists($local_file)) {
        unlink($local_file);
    }
}

// 关闭FTP连接
ftp_close($ftp_conn);

// 辅助函数:格式化字节大小
function formatBytes($bytes, $precision = 2) {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];

    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);

    $bytes /= pow(1024, $pow);

    return round($bytes, $precision) . ' ' . $units[$pow];
}
?>

示例2:带断点续传功能的下载

<?php
class ResumeFTPDownload {
    private $ftp_conn;
    private $breakpoint_file = 'breakpoints.json';

    public function __construct($server, $username, $password) {
        $this->ftp_conn = ftp_connect($server);
        if (!$this->ftp_conn) {
            throw new Exception("无法连接到FTP服务器: $server");
        }

        if (!ftp_login($this->ftp_conn, $username, $password)) {
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->ftp_conn, true);
    }

    public function downloadWithResume($remote_file, $local_file) {
        // 检查本地文件是否已存在
        $resume_pos = 0;
        $mode = 'w'; // 写入模式

        if (file_exists($local_file)) {
            $resume_pos = filesize($local_file);
            $mode = 'a'; // 追加模式
            echo "发现已下载的部分 ($resume_pos 字节),将续传下载\n";

            // 保存断点信息
            $this->saveBreakpoint($remote_file, $local_file, $resume_pos);
        }

        // 开始非阻塞下载(从断点处继续)
        $result = ftp_nb_get($this->ftp_conn, $local_file, $remote_file, FTP_BINARY, $resume_pos);

        $total_size = ftp_size($this->ftp_conn, $remote_file);
        if ($total_size == -1) {
            $total_size = 0;
        }

        $start_time = time();
        $last_update = time();

        while ($result == FTP_MOREDATA) {
            // 获取当前下载大小
            $current_size = file_exists($local_file) ? filesize($local_file) : 0;

            // 显示进度
            $current_time = time();
            if ($current_time - $last_update >= 1) {
                $progress = $total_size > 0 ? round(($current_size / $total_size) * 100, 1) : 0;
                $elapsed = $current_time - $start_time;
                $speed = $elapsed > 0 ? $current_size / $elapsed : 0;
                $eta = $speed > 0 ? round(($total_size - $current_size) / $speed) : 0;

                echo sprintf("\r进度: %.1f%% | 已下载: %s/%s | 速度: %s/s | ETA: %ds",
                    $progress,
                    formatBytes($current_size),
                    formatBytes($total_size),
                    formatBytes($speed),
                    $eta
                );

                $last_update = $current_time;

                // 更新断点信息
                $this->saveBreakpoint($remote_file, $local_file, $current_size);
            }

            // 继续传输
            $result = ftp_nb_continue($this->ftp_conn);

            usleep(50000); // 0.05秒

            // 检查是否超时(30秒无进展)
            if ($current_time - $start_time > 30 && $current_size == $resume_pos) {
                echo "\n下载超时,已保存断点\n";
                break;
            }
        }

        // 清除断点信息
        $this->clearBreakpoint($remote_file, $local_file);

        if ($result == FTP_FINISHED) {
            echo "\n下载完成!\n";
            return true;
        } elseif ($result == FTP_FAILED) {
            echo "\n下载失败\n";
            return false;
        }

        return false;
    }

    private function saveBreakpoint($remote_file, $local_file, $position) {
        $breakpoints = [];
        if (file_exists($this->breakpoint_file)) {
            $breakpoints = json_decode(file_get_contents($this->breakpoint_file), true);
        }

        $key = md5($remote_file . $local_file);
        $breakpoints[$key] = [
            'remote_file' => $remote_file,
            'local_file' => $local_file,
            'position' => $position,
            'timestamp' => time()
        ];

        file_put_contents($this->breakpoint_file, json_encode($breakpoints, JSON_PRETTY_PRINT));
    }

    private function clearBreakpoint($remote_file, $local_file) {
        if (!file_exists($this->breakpoint_file)) {
            return;
        }

        $breakpoints = json_decode(file_get_contents($this->breakpoint_file), true);
        $key = md5($remote_file . $local_file);

        if (isset($breakpoints[$key])) {
            unset($breakpoints[$key]);
            file_put_contents($this->breakpoint_file, json_encode($breakpoints, JSON_PRETTY_PRINT));
        }
    }

    public function __destruct() {
        if ($this->ftp_conn) {
            ftp_close($this->ftp_conn);
        }
    }
}

// 使用示例
try {
    $downloader = new ResumeFTPDownload('localhost', 'user', 'pass');

    $remote_file = 'large_file.zip';
    $local_file = 'downloads/large_file.zip';

    // 确保下载目录存在
    $dir = dirname($local_file);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $success = $downloader->downloadWithResume($remote_file, $local_file);

    if ($success) {
        echo "文件下载成功: $local_file\n";
    } else {
        echo "文件下载失败,下次将从断点继续\n";
    }

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

示例3:批量文件下载管理器

<?php
class BatchFTPDownloader {
    private $ftp_conn;
    private $download_queue = [];
    private $active_downloads = [];
    private $max_concurrent = 3; // 最大并发下载数

    public function __construct($server, $username, $password) {
        $this->ftp_conn = ftp_connect($server);
        if (!$this->ftp_conn) {
            throw new Exception("无法连接到FTP服务器");
        }

        if (!ftp_login($this->ftp_conn, $username, $password)) {
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->ftp_conn, true);
    }

    public function addToQueue($remote_file, $local_file) {
        $this->download_queue[] = [
            'remote' => $remote_file,
            'local' => $local_file,
            'status' => 'pending',
            'progress' => 0,
            'size' => 0
        ];
    }

    public function startDownloads() {
        echo "开始批量下载...\n";

        $iteration = 0;
        $max_iterations = 10000;

        while (($this->hasPendingDownloads() || !empty($this->active_downloads))
               && $iteration < $max_iterations) {

            // 启动新的下载(如果未达到并发上限)
            $this->startNewDownloads();

            // 处理正在进行的下载
            $this->processActiveDownloads();

            // 显示状态
            if ($iteration % 20 == 0) {
                $this->showStatus();
            }

            usleep(100000); // 0.1秒
            $iteration++;
        }

        echo "\n批量下载完成\n";
        $this->showFinalReport();
    }

    private function startNewDownloads() {
        $available_slots = $this->max_concurrent - count($this->active_downloads);

        if ($available_slots <= 0) {
            return;
        }

        foreach ($this->download_queue as &$item) {
            if ($item['status'] == 'pending' && $available_slots > 0) {
                // 确保本地目录存在
                $dir = dirname($item['local']);
                if (!is_dir($dir)) {
                    mkdir($dir, 0755, true);
                }

                // 获取文件大小
                $item['size'] = ftp_size($this->ftp_conn, $item['remote']);
                if ($item['size'] == -1) {
                    $item['size'] = 0;
                }

                // 开始非阻塞下载
                $result = ftp_nb_get($this->ftp_conn, $item['local'], $item['remote'], FTP_BINARY);

                if ($result == FTP_FAILED) {
                    $item['status'] = 'failed';
                    echo "开始下载失败: {$item['remote']}\n";
                } else {
                    $item['status'] = 'downloading';
                    $item['ftp_result'] = $result;
                    $item['start_time'] = time();
                    $item['last_size'] = 0;

                    $this->active_downloads[] = &$item;
                    $available_slots--;

                    echo "开始下载: {$item['remote']}\n";
                }
            }
        }
    }

    private function processActiveDownloads() {
        foreach ($this->active_downloads as $key => &$download) {
            if ($download['status'] == 'downloading' && $download['ftp_result'] == FTP_MOREDATA) {
                // 继续下载
                $download['ftp_result'] = ftp_nb_continue($this->ftp_conn);

                // 更新进度
                if (file_exists($download['local'])) {
                    $current_size = filesize($download['local']);
                    $download['progress'] = $download['size'] > 0 ?
                        round(($current_size / $download['size']) * 100, 1) : 0;

                    // 计算速度
                    $elapsed = time() - $download['start_time'];
                    if ($elapsed > 0) {
                        $download['speed'] = $current_size / $elapsed;
                    }
                }

                // 检查是否完成
                if ($download['ftp_result'] == FTP_FINISHED) {
                    $download['status'] = 'completed';
                    $download['progress'] = 100;
                    $download['end_time'] = time();

                    echo "下载完成: {$download['remote']}\n";
                    unset($this->active_downloads[$key]);

                } elseif ($download['ftp_result'] == FTP_FAILED) {
                    $download['status'] = 'failed';
                    echo "下载失败: {$download['remote']}\n";
                    unset($this->active_downloads[$key]);
                }
            }
        }
    }

    private function hasPendingDownloads() {
        foreach ($this->download_queue as $item) {
            if ($item['status'] == 'pending') {
                return true;
            }
        }
        return false;
    }

    private function showStatus() {
        $completed = 0;
        $downloading = 0;
        $failed = 0;
        $pending = 0;
        $total_size = 0;
        $downloaded_size = 0;

        foreach ($this->download_queue as $item) {
            switch ($item['status']) {
                case 'completed':
                    $completed++;
                    $downloaded_size += $item['size'];
                    break;
                case 'downloading':
                    $downloading++;
                    if (file_exists($item['local'])) {
                        $downloaded_size += filesize($item['local']);
                    }
                    break;
                case 'failed':
                    $failed++;
                    break;
                case 'pending':
                    $pending++;
                    break;
            }
            $total_size += $item['size'];
        }

        $progress = $total_size > 0 ? round(($downloaded_size / $total_size) * 100, 1) : 0;

        echo "\n=== 下载状态 ===\n";
        echo "总文件数: " . count($this->download_queue) . "\n";
        echo "已完成: $completed | 下载中: $downloading | 失败: $failed | 等待: $pending\n";
        echo "总体进度: $progress% (" . formatBytes($downloaded_size) . "/" . formatBytes($total_size) . ")\n";

        // 显示当前下载的文件
        if (!empty($this->active_downloads)) {
            echo "\n当前下载文件:\n";
            foreach ($this->active_downloads as $download) {
                echo "  {$download['remote']}: {$download['progress']}%";
                if (isset($download['speed'])) {
                    echo " 速度: " . formatBytes($download['speed']) . "/s";
                }
                echo "\n";
            }
        }
        echo "================\n";
    }

    private function showFinalReport() {
        $report = [
            'total' => 0,
            'completed' => 0,
            'failed' => 0,
            'total_size' => 0,
            'downloaded_size' => 0
        ];

        foreach ($this->download_queue as $item) {
            $report['total']++;

            if ($item['status'] == 'completed') {
                $report['completed']++;
                $report['total_size'] += $item['size'];
                $report['downloaded_size'] += $item['size'];
            } elseif ($item['status'] == 'failed') {
                $report['failed']++;
            }
        }

        echo "\n=== 下载报告 ===\n";
        echo "总文件数: {$report['total']}\n";
        echo "成功: {$report['completed']}\n";
        echo "失败: {$report['failed']}\n";
        echo "总大小: " . formatBytes($report['total_size']) . "\n";
        echo "下载量: " . formatBytes($report['downloaded_size']) . "\n";

        if ($report['total'] > 0) {
            $success_rate = ($report['completed'] / $report['total']) * 100;
            echo "成功率: " . round($success_rate, 1) . "%\n";
        }
    }

    public function __destruct() {
        if ($this->ftp_conn) {
            ftp_close($this->ftp_conn);
        }
    }
}

// 使用示例
try {
    $downloader = new BatchFTPDownloader('localhost', 'user', 'pass');

    // 添加下载任务
    $files = [
        '/remote/path/file1.pdf' => 'downloads/file1.pdf',
        '/remote/path/file2.jpg' => 'downloads/file2.jpg',
        '/remote/path/file3.zip' => 'downloads/file3.zip',
        '/remote/path/file4.txt' => 'downloads/file4.txt',
        '/remote/path/file5.mp3' => 'downloads/file5.mp3'
    ];

    foreach ($files as $remote => $local) {
        $downloader->addToQueue($remote, $local);
    }

    // 开始批量下载
    $downloader->startDownloads();

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

注意事项

  • 非阻塞操作需要循环调用ftp_nb_continue()直到传输完成
  • 确保本地目录有写入权限
  • 断点续传功能需要服务器支持REST命令
  • 在传输过程中,可以执行其他PHP代码,但要注意FTP连接保持
  • 二进制模式(FTP_BINARY)适用于所有文件类型
  • 注意添加适当的延迟(usleep())以避免过度占用CPU
  • 大文件下载时注意PHP执行时间限制和内存限制

最佳实践建议

推荐做法
  • 使用ftp_pasv($ftp_conn, true)启用被动模式
  • 实现下载进度反馈机制
  • 使用二进制模式传输所有文件类型
  • 实现断点续传功能
  • 添加适当的错误处理和重试机制
  • 批量下载时限制并发数量
避免做法
  • 不要同时打开过多的并发下载
  • 避免在下载过程中修改本地文件
  • 不要忘记添加适当的延迟
  • 避免在没有进度反馈的情况下下载大文件
  • 不要假设所有服务器都支持断点续传
  • 避免在没有错误处理的情况下使用

性能优化技巧

  1. 调整缓冲区大小:PHP内部有传输缓冲区,可通过调整循环延迟来优化
  2. 并发控制:根据服务器性能和网络带宽调整并发下载数量
  3. 内存管理:下载大文件时注意内存使用,定期释放不需要的变量
  4. 连接复用:批量下载时尽量复用同一个FTP连接
  5. 超时设置:设置合理的超时时间,避免无限等待
  6. 日志记录:记录下载日志,便于问题排查和性能分析

相关函数

  • ftp_nb_continue() - 继续非阻塞的FTP操作
  • ftp_nb_fget() - 非阻塞方式下载文件到已打开的文件指针
  • ftp_get() - 阻塞方式下载文件
  • ftp_nb_put() - 非阻塞方式上传文件到FTP服务器
  • ftp_nb_fput() - 非阻塞方式上传已打开的文件
  • ftp_size() - 获取远程文件大小
  • ftp_mdtm() - 获取远程文件的最后修改时间