PHP ftp_nb_fget() 函数

PHP ftp_nb_fget() 函数用于以非阻塞方式从FTP服务器下载文件,并写入一个已打开的文件指针。

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

语法

ftp_nb_fget(
    resource $ftp,
    resource $handle,
    string $remote_file,
    int $mode = FTP_BINARY,
    int $resumepos = 0
): int

参数说明

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

返回值

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

与ftp_fget()的区别

特性 ftp_fget() ftp_nb_fget()
阻塞性 阻塞模式 非阻塞模式
执行方式 脚本等待传输完成 传输过程中可执行其他代码
适用场景 小文件或简单传输 大文件传输或需要并发的场景
返回值 布尔值(成功/失败) 状态码(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);

// 打开本地文件用于写入
$local_file = "download.txt";
$handle = fopen($local_file, 'w');
if (!$handle) {
    die("无法打开文件: $local_file");
}

// 开始非阻塞下载
$remote_file = "data/example.txt";
$result = ftp_nb_fget($ftp_conn, $handle, $remote_file, FTP_ASCII);

// 循环处理传输
while ($result == FTP_MOREDATA) {
    echo "正在下载文件...\n";

    // 可以在这里执行其他任务
    // 例如:更新进度、检查用户输入等

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

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

// 关闭文件句柄
fclose($handle);

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

    // 显示文件信息
    $file_size = filesize($local_file);
    echo "文件大小: " . $file_size . " 字节\n";
} elseif ($result == FTP_FAILED) {
    echo "文件下载失败!\n";

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

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

示例2:断点续传功能

<?php
function resumeDownload($ftp_conn, $remote_file, $local_file, $mode = FTP_BINARY) {
    // 检查本地文件是否已存在(部分下载)
    $resume_pos = 0;
    $handle = null;

    if (file_exists($local_file)) {
        $resume_pos = filesize($local_file);
        $handle = fopen($local_file, 'a'); // 追加模式
        echo "发现已下载的部分,将从 {$resume_pos} 字节处继续下载\n";
    } else {
        $handle = fopen($local_file, 'w'); // 新建文件
    }

    if (!$handle) {
        die("无法打开文件: $local_file");
    }

    // 开始非阻塞下载(从断点处继续)
    $result = ftp_nb_fget($ftp_conn, $handle, $remote_file, $mode, $resume_pos);

    $start_time = time();
    $last_size = $resume_pos;

    while ($result == FTP_MOREDATA) {
        // 计算下载速度
        $current_size = ftell($handle);
        $elapsed_time = time() - $start_time;

        if ($elapsed_time > 0) {
            $speed = ($current_size - $last_size) / $elapsed_time;
            $last_size = $current_size;
            $start_time = time();

            echo "\r已下载: " . formatBytes($current_size) .
                 " | 速度: " . formatBytes($speed) . "/s";
        }

        // 继续传输
        $result = ftp_nb_continue($ftp_conn);
        usleep(50000); // 0.05秒
    }

    fclose($handle);

    if ($result == FTP_FINISHED) {
        echo "\n下载完成!文件: $local_file\n";
        return true;
    } else {
        echo "\n下载失败!\n";
        return false;
    }
}

// 辅助函数:格式化字节大小
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];
}

// 使用示例
$ftp_conn = ftp_connect('localhost');
if ($ftp_conn && ftp_login($ftp_conn, 'user', 'pass')) {
    ftp_pasv($ftp_conn, true);

    $remote_file = 'largefile.zip';
    $local_file = 'largefile_download.zip';

    resumeDownload($ftp_conn, $remote_file, $local_file, FTP_BINARY);

    ftp_close($ftp_conn);
} else {
    echo "无法连接FTP服务器\n";
}
?>

示例3:多个文件并发下载

<?php
class ConcurrentFTPDownloader {
    private $ftp_conn;
    private $downloads = [];

    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 addDownload($remote_file, $local_file) {
        $handle = fopen($local_file, 'w');
        if (!$handle) {
            throw new Exception("无法打开文件: $local_file");
        }

        // 开始非阻塞下载
        $result = ftp_nb_fget($this->ftp_conn, $handle, $remote_file, FTP_BINARY);

        $this->downloads[] = [
            'remote' => $remote_file,
            'local' => $local_file,
            'handle' => $handle,
            'status' => $result,
            'progress' => 0
        ];

        return count($this->downloads) - 1; // 返回下载ID
    }

    public function processDownloads($callback = null) {
        $active_downloads = array_filter($this->downloads, function($dl) {
            return $dl['status'] == FTP_MOREDATA;
        });

        if (empty($active_downloads)) {
            return false;
        }

        foreach ($this->downloads as &$download) {
            if ($download['status'] == FTP_MOREDATA) {
                $download['status'] = ftp_nb_continue($this->ftp_conn);

                // 更新进度
                if (is_resource($download['handle'])) {
                    $download['progress'] = ftell($download['handle']);
                }

                // 调用回调函数
                if ($callback && is_callable($callback)) {
                    $callback($download);
                }

                // 如果下载完成,关闭文件句柄
                if ($download['status'] == FTP_FINISHED ||
                    $download['status'] == FTP_FAILED) {
                    fclose($download['handle']);
                }
            }
        }

        return true;
    }

    public function getStatus() {
        $status = [
            'total' => count($this->downloads),
            'finished' => 0,
            'failed' => 0,
            'active' => 0
        ];

        foreach ($this->downloads as $download) {
            if ($download['status'] == FTP_FINISHED) {
                $status['finished']++;
            } elseif ($download['status'] == FTP_FAILED) {
                $status['failed']++;
            } elseif ($download['status'] == FTP_MOREDATA) {
                $status['active']++;
            }
        }

        return $status;
    }

    public function __destruct() {
        // 关闭所有打开的文件句柄
        foreach ($this->downloads as $download) {
            if (is_resource($download['handle'])) {
                fclose($download['handle']);
            }
        }

        // 关闭FTP连接
        if ($this->ftp_conn) {
            ftp_close($this->ftp_conn);
        }
    }
}

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

    // 添加多个下载任务
    $downloader->addDownload('files/doc1.pdf', 'downloads/doc1.pdf');
    $downloader->addDownload('files/doc2.pdf', 'downloads/doc2.pdf');
    $downloader->addDownload('files/doc3.pdf', 'downloads/doc3.pdf');

    echo "开始并发下载...\n";

    $max_iterations = 500; // 最大迭代次数
    $iteration = 0;

    // 进度回调函数
    $progressCallback = function($download) {
        static $last_output = '';

        $status = $download['status'] == FTP_MOREDATA ? '下载中' :
                  ($download['status'] == FTP_FINISHED ? '完成' : '失败');

        $output = "文件: {$download['remote']} - 状态: {$status}";
        if ($download['progress'] > 0) {
            $output .= " - 进度: " . $download['progress'] . " 字节";
        }

        if ($output !== $last_output) {
            echo $output . "\n";
            $last_output = $output;
        }
    };

    // 处理下载直到所有完成或达到最大迭代次数
    while ($downloader->processDownloads($progressCallback) && $iteration < $max_iterations) {
        usleep(100000); // 0.1秒
        $iteration++;

        // 每10次迭代显示一次状态
        if ($iteration % 10 == 0) {
            $status = $downloader->getStatus();
            echo "总任务: {$status['total']}, 完成: {$status['finished']}, ";
            echo "失败: {$status['failed']}, 进行中: {$status['active']}\n";
        }
    }

    echo "下载处理完成\n";

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

注意事项

  • 必须确保文件指针以正确的模式打开('w'用于写入,'a'用于追加)
  • 非阻塞操作需要循环调用ftp_nb_continue()直到传输完成
  • 在传输过程中,文件指针会不断移动,可以使用ftell()获取当前位置
  • 断点续传功能需要服务器支持
  • 确保在处理过程中保持FTP连接有效
  • 传输完成后必须关闭文件句柄以释放资源
  • 二进制模式(FTP_BINARY)适用于所有文件类型,特别是图像、ZIP等

最佳实践建议

推荐做法
  • 使用ftp_pasv($ftp_conn, true)启用被动模式
  • 添加适当的延迟(usleep())避免过度占用CPU
  • 实现进度反馈机制
  • 使用try-catch处理异常
  • 传输完成后检查文件完整性
  • 使用二进制模式传输所有文件类型
避免做法
  • 不要在传输过程中随意移动文件指针
  • 避免在传输过程中关闭FTP连接
  • 不要忘记关闭打开的文件句柄
  • 避免使用过短的延迟导致CPU使用率高
  • 不要假设所有服务器都支持断点续传
  • 避免在没有错误处理的情况下使用

相关函数

  • ftp_nb_continue() - 继续非阻塞的FTP操作
  • ftp_nb_get() - 非阻塞方式下载文件到本地路径
  • ftp_fget() - 阻塞方式下载文件到已打开的文件指针
  • ftp_nb_put() - 非阻塞方式上传文件到FTP服务器
  • ftp_nb_fput() - 非阻塞方式上传已打开的文件
  • fopen() - 打开文件或URL
  • fclose() - 关闭一个已打开的文件指针