PHP ftp_nb_put() 函数

PHP ftp_nb_put() 函数用于以非阻塞方式将本地文件上传到FTP服务器。

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

语法

ftp_nb_put(
    resource $ftp,
    string $remote_file,
    string $local_file,
    int $mode = FTP_BINARY,
    int $startpos = 0
): int

参数说明

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

返回值

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

与ftp_put()的区别

特性 ftp_put() ftp_nb_put()
阻塞性 阻塞模式 非阻塞模式
执行方式 脚本等待传输完成 传输过程中可执行其他代码
适用场景 小文件或简单传输 大文件传输或需要并发的场景
返回值 布尔值(成功/失败) 状态码(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 = "upload.txt";
if (!file_exists($local_file)) {
    die("本地文件不存在: $local_file");
}

// 获取文件大小
$file_size = filesize($local_file);
echo "准备上传文件: $local_file (大小: $file_size 字节)\n";

// 开始非阻塞上传
$remote_file = "uploads/upload.txt";
$result = ftp_nb_put($ftp_conn, $remote_file, $local_file, FTP_ASCII);

// 循环处理上传
$start_time = time();
$last_update = time();
$last_position = 0;

while ($result == FTP_MOREDATA) {
    // 模拟进度跟踪(通过检查文件读取位置)
    // 注意:实际应用中可能需要使用其他方式跟踪进度
    $current_time = time();

    // 每秒更新一次进度显示
    if ($current_time - $last_update >= 1) {
        // 检查文件读取位置(模拟)
        $uploaded = min($last_position + 10240, $file_size); // 模拟每次增加10KB
        $last_position = $uploaded;

        $progress = ($file_size > 0) ? round(($uploaded / $file_size) * 100, 2) : 0;
        $elapsed = $current_time - $start_time;
        $speed = $elapsed > 0 ? $uploaded / $elapsed : 0;

        echo "\r上传进度: $progress% | 已上传: " . formatBytes($uploaded) .
             "/" . formatBytes($file_size) . " | 速度: " . formatBytes($speed) . "/s";

        $last_update = $current_time;
    }

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

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

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

    // 验证上传文件大小
    $remote_size = ftp_size($ftp_conn, $remote_file);
    if ($remote_size == $file_size) {
        echo "文件大小验证成功: $remote_size 字节\n";
    } else {
        echo "警告: 本地文件大小 ($file_size) 与远程文件大小 ($remote_size) 不一致\n";
    }
} elseif ($result == FTP_FAILED) {
    echo "\n文件上传失败!\n";
}

// 关闭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 ResumeFTPUpload {
    private $ftp_conn;
    private $breakpoint_dir = 'upload_breakpoints';

    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);

        // 创建断点目录
        if (!is_dir($this->breakpoint_dir)) {
            mkdir($this->breakpoint_dir, 0755, true);
        }
    }

    public function uploadWithResume($local_file, $remote_file) {
        if (!file_exists($local_file)) {
            throw new Exception("本地文件不存在: $local_file");
        }

        $file_size = filesize($local_file);
        $startpos = 0;

        // 检查是否有断点记录
        $breakpoint_file = $this->breakpoint_dir . '/' . md5($local_file . $remote_file) . '.bp';

        if (file_exists($breakpoint_file)) {
            $breakpoint_data = json_decode(file_get_contents($breakpoint_file), true);
            $startpos = $breakpoint_data['position'] ?? 0;

            // 验证文件是否已更改
            $file_mtime = filemtime($local_file);
            if (isset($breakpoint_data['mtime']) && $breakpoint_data['mtime'] != $file_mtime) {
                echo "文件已修改,重新开始上传\n";
                $startpos = 0;
            } else {
                echo "发现断点,从 {$startpos} 字节处继续上传\n";
            }
        }

        // 开始非阻塞上传(从断点处继续)
        $result = ftp_nb_put($this->ftp_conn, $remote_file, $local_file, FTP_BINARY, $startpos);

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

        while ($result == FTP_MOREDATA) {
            // 模拟进度计算(实际应用中可能需要更精确的方法)
            // 这里使用简单的线性递增模拟
            $current_time = time();

            if ($current_time - $last_update >= 1) {
                // 计算上传速度
                $elapsed = $current_time - $start_time;
                $speed = $elapsed > 0 ? $uploaded / $elapsed : 0;

                // 模拟上传进度(每次更新增加一定量)
                $uploaded = min($uploaded + ($speed * 1), $file_size);
                $progress = $file_size > 0 ? round(($uploaded / $file_size) * 100, 1) : 0;

                echo sprintf("\r进度: %.1f%% | 已上传: %s/%s | 速度: %s/s",
                    $progress,
                    formatBytes($uploaded),
                    formatBytes($file_size),
                    formatBytes($speed)
                );

                // 保存断点
                $breakpoint_data = [
                    'position' => $uploaded,
                    'mtime' => filemtime($local_file),
                    'timestamp' => time(),
                    'local_file' => $local_file,
                    'remote_file' => $remote_file
                ];

                file_put_contents($breakpoint_file, json_encode($breakpoint_data, JSON_PRETTY_PRINT));

                $last_update = $current_time;
            }

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

            usleep(100000); // 0.1秒

            // 检查超时
            if ($current_time - $start_time > 300) { // 5分钟超时
                echo "\n上传超时,已保存断点\n";
                break;
            }
        }

        // 检查上传结果
        if ($result == FTP_FINISHED) {
            echo "\n上传完成!\n";

            // 删除断点文件
            if (file_exists($breakpoint_file)) {
                unlink($breakpoint_file);
            }

            return true;
        } elseif ($result == FTP_FAILED) {
            echo "\n上传失败\n";
            return false;
        }

        return false;
    }

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

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

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

    $success = $uploader->uploadWithResume($local_file, $remote_file);

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

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

示例3:批量文件上传管理器

<?php
class BatchFTPUploader {
    private $ftp_conn;
    private $upload_queue = [];
    private $active_uploads = [];
    private $max_concurrent = 3; // 最大并发上传数
    private $progress_file = 'upload_progress.json';

    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);

        // 加载进度文件(如果存在)
        if (file_exists($this->progress_file)) {
            $this->upload_queue = json_decode(file_get_contents($this->progress_file), true);
        }
    }

    public function addToQueue($local_file, $remote_dir) {
        if (!file_exists($local_file)) {
            throw new Exception("本地文件不存在: $local_file");
        }

        $remote_file = $remote_dir . '/' . basename($local_file);

        $this->upload_queue[] = [
            'local' => $local_file,
            'remote' => $remote_file,
            'status' => 'pending',
            'progress' => 0,
            'size' => filesize($local_file),
            'added_time' => time()
        ];

        $this->saveProgress();
    }

    public function startUploads() {
        echo "开始批量上传...\n";

        $iteration = 0;
        $max_iterations = 10000;

        while (($this->hasPendingUploads() || !empty($this->active_uploads))
               && $iteration < $max_iterations) {

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

            // 处理正在进行的上传
            $this->processActiveUploads();

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

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

        echo "\n批量上传完成\n";
        $this->showFinalReport();

        // 清理进度文件
        if (file_exists($this->progress_file)) {
            unlink($this->progress_file);
        }
    }

    private function startNewUploads() {
        $available_slots = $this->max_concurrent - count($this->active_uploads);

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

        foreach ($this->upload_queue as &$item) {
            if ($item['status'] == 'pending' && $available_slots > 0) {
                // 检查本地文件是否存在
                if (!file_exists($item['local'])) {
                    $item['status'] = 'failed';
                    $item['error'] = '本地文件不存在';
                    echo "文件不存在: {$item['local']}\n";
                    continue;
                }

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

                if ($result == FTP_FAILED) {
                    $item['status'] = 'failed';
                    $item['error'] = '开始上传失败';
                    echo "开始上传失败: {$item['local']}\n";
                } else {
                    $item['status'] = 'uploading';
                    $item['ftp_result'] = $result;
                    $item['start_time'] = time();
                    $item['uploaded'] = 0;

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

                    echo "开始上传: {$item['local']} -> {$item['remote']}\n";
                }
            }
        }

        $this->saveProgress();
    }

    private function processActiveUploads() {
        foreach ($this->active_uploads as $key => &$upload) {
            if ($upload['status'] == 'uploading' && $upload['ftp_result'] == FTP_MOREDATA) {
                // 继续上传
                $upload['ftp_result'] = ftp_nb_continue($this->ftp_conn);

                // 更新进度(模拟)
                $elapsed = time() - $upload['start_time'];
                $upload['uploaded'] = min($upload['uploaded'] + 10240, $upload['size']); // 模拟每次增加10KB
                $upload['progress'] = $upload['size'] > 0 ?
                    round(($upload['uploaded'] / $upload['size']) * 100, 1) : 0;

                // 计算速度
                if ($elapsed > 0) {
                    $upload['speed'] = $upload['uploaded'] / $elapsed;
                }

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

                    echo "上传完成: {$upload['local']}\n";
                    unset($this->active_uploads[$key]);

                } elseif ($upload['ftp_result'] == FTP_FAILED) {
                    $upload['status'] = 'failed';
                    $upload['error'] = '上传过程中失败';
                    echo "上传失败: {$upload['local']}\n";
                    unset($this->active_uploads[$key]);
                }
            }
        }

        $this->saveProgress();
    }

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

    private function showStatus() {
        $completed = 0;
        $uploading = 0;
        $failed = 0;
        $pending = 0;
        $total_size = 0;
        $uploaded_size = 0;

        foreach ($this->upload_queue as $item) {
            switch ($item['status']) {
                case 'completed':
                    $completed++;
                    $uploaded_size += $item['size'];
                    break;
                case 'uploading':
                    $uploading++;
                    $uploaded_size += $item['uploaded'];
                    break;
                case 'failed':
                    $failed++;
                    break;
                case 'pending':
                    $pending++;
                    break;
            }
            $total_size += $item['size'];
        }

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

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

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

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

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

            if ($item['status'] == 'completed') {
                $report['completed']++;
                $report['total_size'] += $item['size'];
                $report['uploaded_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['uploaded_size']) . "\n";

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

    private function saveProgress() {
        file_put_contents($this->progress_file, json_encode($this->upload_queue, JSON_PRETTY_PRINT));
    }

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

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

    // 添加上传任务
    $files = [
        '/path/to/file1.pdf' => '/uploads',
        '/path/to/file2.jpg' => '/uploads',
        '/path/to/file3.zip' => '/uploads',
        '/path/to/file4.txt' => '/uploads',
        '/path/to/file5.mp3' => '/uploads'
    ];

    foreach ($files as $local => $remote_dir) {
        if (file_exists($local)) {
            $uploader->addToQueue($local, $remote_dir);
        } else {
            echo "文件不存在: $local\n";
        }
    }

    // 开始批量上传
    $uploader->startUploads();

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

注意事项

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

最佳实践建议

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

常见问题解答

ftp_nb_put() 本身不提供精确的进度跟踪。可以通过以下方式实现:
  1. 使用ftp_nb_fput()配合文件指针,用ftell()获取当前位置
  2. 实现自己的进度估算算法
  3. 使用回调函数定期报告进度
  4. 结合文件大小和传输时间估算进度

1. 实现断点续传功能
2. 使用startpos参数从上次中断的位置继续
3. 保存断点信息到文件或数据库
4. 添加重试机制,设置最大重试次数
5. 记录详细的错误日志以便排查问题

  • 调整并发上传数量(根据服务器性能和网络带宽)
  • 使用二进制模式(FTP_BINARY
  • 启用被动模式(ftp_pasv($ftp_conn, true)
  • 压缩大文件后再上传(如果适用)
  • 优化usleep()延迟值
  • 使用连接池复用FTP连接

相关函数

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