PHP ftp_nb_fput() 函数

PHP ftp_nb_fput() 函数用于以非阻塞方式将已打开的文件指针上传到FTP服务器。

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

语法

ftp_nb_fput(
    resource $ftp,
    string $remote_file,
    resource $handle,
    int $mode = FTP_BINARY,
    int $startpos = 0
): int

参数说明

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

返回值

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

与ftp_fput()的区别

特性 ftp_fput() ftp_nb_fput()
阻塞性 阻塞模式 非阻塞模式
执行方式 脚本等待上传完成 上传过程中可执行其他代码
适用场景 小文件或简单上传 大文件上传或需要并发的场景
返回值 布尔值(成功/失败) 状态码(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";
$handle = fopen($local_file, 'r');
if (!$handle) {
    die("无法打开文件: $local_file");
}

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

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

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

while ($result == FTP_MOREDATA) {
    // 获取当前上传位置
    $current_pos = ftell($handle);

    // 计算上传速度
    $current_time = time();
    if ($current_time - $last_update >= 1) { // 每秒更新一次
        $speed = $current_pos - $uploaded_bytes;
        $progress = ($file_size > 0) ? round(($current_pos / $file_size) * 100, 2) : 0;

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

        $uploaded_bytes = $current_pos;
        $last_update = $current_time;
    }

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

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

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

// 检查上传结果
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
function resumeUpload($ftp_conn, $remote_file, $local_file, $mode = FTP_BINARY) {
    // 检查远程文件是否已存在(部分上传)
    $startpos = 0;

    try {
        $remote_size = ftp_size($ftp_conn, $remote_file);
        if ($remote_size != -1) {
            $startpos = $remote_size;
            echo "发现已上传的部分 ($remote_size 字节),将从该位置继续上传\n";
        }
    } catch (Exception $e) {
        // 文件不存在,从头开始上传
        $startpos = 0;
    }

    // 打开本地文件
    $handle = fopen($local_file, 'r');
    if (!$handle) {
        throw new Exception("无法打开文件: $local_file");
    }

    // 移动文件指针到续传位置
    if ($startpos > 0) {
        fseek($handle, $startpos);
    }

    $total_size = filesize($local_file);
    $remaining = $total_size - $startpos;

    echo "开始续传,剩余: " . formatBytes($remaining) . "\n";

    // 开始非阻塞上传
    $result = ftp_nb_fput($ftp_conn, $remote_file, $handle, $mode, $startpos);

    $last_pos = $startpos;
    $start_time = time();

    while ($result == FTP_MOREDATA) {
        $current_pos = ftell($handle);
        $uploaded = $current_pos - $startpos;

        // 显示进度
        $elapsed = time() - $start_time;
        $speed = ($elapsed > 0) ? $uploaded / $elapsed : 0;
        $progress = ($remaining > 0) ? round(($uploaded / $remaining) * 100, 1) : 0;
        $eta = ($speed > 0) ? round(($remaining - $uploaded) / $speed) : 0;

        echo sprintf("\r进度: %s%% | 已上传: %s/%s | 速度: %s/s | ETA: %ds",
            $progress,
            formatBytes($uploaded),
            formatBytes($remaining),
            formatBytes($speed),
            $eta
        );

        // 检查是否到达文件末尾
        if (feof($handle)) {
            break;
        }

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

        // 如果长时间没有进展,检查连接
        if ($current_pos == $last_pos && $elapsed > 30) {
            echo "\n警告: 上传停滞超过30秒\n";
            break;
        }

        $last_pos = $current_pos;
        usleep(100000); // 0.1秒
    }

    fclose($handle);

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

        // 记录断点位置
        $breakpoint = ftell($handle);
        file_put_contents("{$local_file}.breakpoint", $breakpoint);
        echo "断点已保存: $breakpoint\n";

        return false;
    }

    return false;
}

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

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

    if (file_exists($local_file)) {
        resumeUpload($ftp_conn, $remote_file, $local_file, FTP_BINARY);
    } else {
        echo "本地文件不存在: $local_file\n";
    }

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

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

<?php
class BatchFTPUploader {
    private $ftp_conn;
    private $uploads = [];
    private $completed = [];
    private $failed = [];

    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 addUpload($local_file, $remote_path) {
        if (!file_exists($local_file)) {
            throw new Exception("本地文件不存在: $local_file");
        }

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

        // 打开文件
        $handle = fopen($local_file, 'r');
        if (!$handle) {
            throw new Exception("无法打开文件: $local_file");
        }

        // 开始非阻塞上传
        $result = ftp_nb_fput($this->ftp_conn, $remote_file, $handle, FTP_BINARY);

        $upload_id = uniqid('upload_', true);

        $this->uploads[$upload_id] = [
            'id' => $upload_id,
            'local_file' => $local_file,
            'remote_file' => $remote_file,
            'handle' => $handle,
            'status' => $result,
            'file_size' => filesize($local_file),
            'start_time' => time(),
            'last_activity' => time(),
            'bytes_transferred' => 0
        ];

        return $upload_id;
    }

    public function processUploads() {
        $active_count = 0;

        foreach ($this->uploads as $upload_id => &$upload) {
            if ($upload['status'] == FTP_MOREDATA) {
                $active_count++;

                // 保存当前位置
                $previous_pos = ftell($upload['handle']);

                // 继续上传
                $upload['status'] = ftp_nb_continue($this->ftp_conn);

                // 更新统计信息
                $current_pos = ftell($upload['handle']);
                $upload['bytes_transferred'] = $current_pos;
                $upload['last_activity'] = time();

                // 计算速度
                $elapsed = time() - $upload['start_time'];
                $upload['speed'] = ($elapsed > 0) ? $current_pos / $elapsed : 0;

                // 检查是否完成或失败
                if ($upload['status'] == FTP_FINISHED) {
                    $this->markAsCompleted($upload_id);
                } elseif ($upload['status'] == FTP_FAILED) {
                    $this->markAsFailed($upload_id);
                }
            }
        }

        return $active_count;
    }

    private function markAsCompleted($upload_id) {
        if (isset($this->uploads[$upload_id])) {
            $upload = $this->uploads[$upload_id];

            // 关闭文件句柄
            fclose($upload['handle']);

            // 添加到完成列表
            $this->completed[$upload_id] = $upload;

            // 从未完成列表中移除
            unset($this->uploads[$upload_id]);

            echo "[完成] {$upload['local_file']} → {$upload['remote_file']}\n";
        }
    }

    private function markAsFailed($upload_id) {
        if (isset($this->uploads[$upload_id])) {
            $upload = $this->uploads[$upload_id];

            // 关闭文件句柄
            fclose($upload['handle']);

            // 添加到失败列表
            $this->failed[$upload_id] = $upload;

            // 从未完成列表中移除
            unset($this->uploads[$upload_id]);

            echo "[失败] {$upload['local_file']} → {$upload['remote_file']}\n";
        }
    }

    public function getStatus() {
        $status = [
            'total' => count($this->uploads) + count($this->completed) + count($this->failed),
            'active' => count($this->uploads),
            'completed' => count($this->completed),
            'failed' => count($this->failed),
            'throughput' => 0
        ];

        // 计算总吞吐量
        foreach ($this->uploads as $upload) {
            $status['throughput'] += $upload['speed'];
        }

        return $status;
    }

    public function getProgress() {
        $progress = [];

        foreach ($this->uploads as $upload) {
            $percentage = ($upload['file_size'] > 0) ?
                round(($upload['bytes_transferred'] / $upload['file_size']) * 100, 1) : 0;

            $progress[$upload['id']] = [
                'file' => basename($upload['local_file']),
                'progress' => $percentage,
                'transferred' => formatBytes($upload['bytes_transferred']),
                'total' => formatBytes($upload['file_size']),
                'speed' => formatBytes($upload['speed']) . '/s'
            ];
        }

        return $progress;
    }

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

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

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

    // 添加要上传的文件
    $files = [
        '/path/to/file1.pdf',
        '/path/to/file2.jpg',
        '/path/to/file3.zip',
        '/path/to/file4.mp4'
    ];

    foreach ($files as $file) {
        if (file_exists($file)) {
            try {
                $upload_id = $uploader->addUpload($file, '/uploads');
                echo "已添加: " . basename($file) . "\n";
            } catch (Exception $e) {
                echo "添加失败: " . $e->getMessage() . "\n";
            }
        }
    }

    echo "开始批量上传...\n\n";

    $max_iterations = 1000;
    $iteration = 0;

    while ($iteration < $max_iterations) {
        $active = $uploader->processUploads();

        if ($active == 0) {
            echo "所有上传任务处理完成\n";
            break;
        }

        // 显示进度
        if ($iteration % 10 == 0) {
            $status = $uploader->getStatus();
            $progress = $uploader->getProgress();

            echo "\n=== 上传状态 ===\n";
            echo "总任务: {$status['total']} | 进行中: {$status['active']} | ";
            echo "已完成: {$status['completed']} | 失败: {$status['failed']}\n";
            echo "总吞吐量: " . formatBytes($status['throughput']) . "/s\n\n";

            if (!empty($progress)) {
                echo "当前上传进度:\n";
                foreach ($progress as $item) {
                    echo "  {$item['file']}: {$item['progress']}% ";
                    echo "({$item['transferred']}/{$item['total']}) ";
                    echo "速度: {$item['speed']}\n";
                }
            }
            echo "================\n";
        }

        usleep(200000); // 0.2秒
        $iteration++;
    }

    if ($iteration >= $max_iterations) {
        echo "达到最大迭代次数,强制结束\n";
    }

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

注意事项

  • 必须确保文件指针以读取模式打开('r')
  • 非阻塞操作需要循环调用ftp_nb_continue()直到传输完成
  • 在传输过程中,文件指针会不断移动,可以使用ftell()获取当前位置
  • 断点续传功能需要服务器支持REST命令
  • 确保在上传过程中保持FTP连接有效
  • 上传完成后必须关闭文件句柄以释放资源
  • 二进制模式(FTP_BINARY)适用于所有文件类型
  • 注意服务器存储空间限制

最佳实践建议

推荐做法
  • 使用ftp_pasv($ftp_conn, true)启用被动模式
  • 实现上传进度反馈机制
  • 使用二进制模式传输所有文件类型
  • 实现断点续传功能
  • 添加上传速度限制(如有需要)
  • 使用try-catch处理异常
避免做法
  • 不要在传输过程中修改源文件
  • 避免同时打开过多文件句柄
  • 不要忘记关闭打开的文件句柄
  • 避免使用过短的延迟导致CPU使用率高
  • 不要假设所有服务器都支持断点续传
  • 避免在没有错误处理的情况下使用

常见问题解答

当上传大文件或需要在上传过程中执行其他任务时使用ftp_nb_fput()。对于小文件或简单的上传任务,使用ftp_put()更简单。

1. 使用二进制模式
2. 减少usleep()延迟
3. 确保网络连接稳定
4. 使用压缩(如果支持)
5. 批量处理多个文件时适当调整并发数

使用ftell()函数获取文件指针当前位置,然后与文件总大小计算百分比。可以在每次调用ftp_nb_continue()后更新进度显示。

相关函数

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