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() 本身不提供精确的进度跟踪。可以通过以下方式实现:
- 使用
ftp_nb_fput()配合文件指针,用ftell()获取当前位置
- 实现自己的进度估算算法
- 使用回调函数定期报告进度
- 结合文件大小和传输时间估算进度
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() - 获取远程文件的最后修改时间