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() - 关闭一个已打开的文件指针