PHP ftp_nb_get() 函数用于以非阻塞方式从FTP服务器下载文件到本地文件系统。
特点:此函数与 ftp_get() 类似,但以非阻塞模式运行,允许在文件传输过程中执行其他PHP代码。
语法
ftp_nb_get(
resource $ftp,
string $local_file,
string $remote_file,
int $mode = FTP_BINARY,
int $resumepos = 0
): int
参数说明
| 参数 |
描述 |
ftp |
必需。FTP连接的标识符,由ftp_connect()或ftp_ssl_connect()返回 |
local_file |
必需。本地文件路径,用于保存下载的数据 |
remote_file |
必需。FTP服务器上要下载的文件路径 |
mode |
可选。传输模式,可以是:
FTP_ASCII - 文本模式(ASCII)
FTP_BINARY - 二进制模式(默认)
|
resumepos |
可选。从远程文件的指定位置开始下载(断点续传) |
返回值
FTP_FAILED (0) - 传输失败
FTP_FINISHED (1) - 传输完成
FTP_MOREDATA (2) - 传输仍在继续
与ftp_get()的区别
| 特性 |
ftp_get() |
ftp_nb_get() |
| 阻塞性 |
阻塞模式 |
非阻塞模式 |
| 执行方式 |
脚本等待传输完成 |
传输过程中可执行其他代码 |
| 适用场景 |
小文件或简单传输 |
大文件传输或需要并发的场景 |
| 返回值 |
布尔值(成功/失败) |
状态码(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);
// 开始非阻塞下载
$remote_file = "data/example.txt";
$local_file = "downloads/example.txt";
// 确保本地目录存在
$dir = dirname($local_file);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$result = ftp_nb_get($ftp_conn, $local_file, $remote_file, FTP_ASCII);
// 循环处理传输
$start_time = time();
$last_size = 0;
while ($result == FTP_MOREDATA) {
// 获取当前文件大小
if (file_exists($local_file)) {
$current_size = filesize($local_file);
// 计算下载速度
$elapsed = time() - $start_time;
if ($elapsed > 0) {
$speed = ($current_size - $last_size) / $elapsed;
echo "\r已下载: " . formatBytes($current_size) .
" | 速度: " . formatBytes($speed) . "/s";
$last_size = $current_size;
$start_time = time();
}
}
// 继续传输
$result = ftp_nb_continue($ftp_conn);
// 添加延迟以避免过度占用CPU
usleep(100000); // 0.1秒
}
// 检查传输结果
if ($result == FTP_FINISHED) {
echo "\n文件下载完成!保存为: $local_file\n";
// 验证文件大小
$remote_size = ftp_size($ftp_conn, $remote_file);
$local_size = filesize($local_file);
if ($remote_size == $local_size) {
echo "文件大小验证成功: $local_size 字节\n";
} else {
echo "警告: 远程文件大小 ($remote_size) 与本地文件大小 ($local_size) 不一致\n";
}
} elseif ($result == FTP_FAILED) {
echo "\n文件下载失败!\n";
// 删除可能不完整的文件
if (file_exists($local_file)) {
unlink($local_file);
}
}
// 关闭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 ResumeFTPDownload {
private $ftp_conn;
private $breakpoint_file = 'breakpoints.json';
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 downloadWithResume($remote_file, $local_file) {
// 检查本地文件是否已存在
$resume_pos = 0;
$mode = 'w'; // 写入模式
if (file_exists($local_file)) {
$resume_pos = filesize($local_file);
$mode = 'a'; // 追加模式
echo "发现已下载的部分 ($resume_pos 字节),将续传下载\n";
// 保存断点信息
$this->saveBreakpoint($remote_file, $local_file, $resume_pos);
}
// 开始非阻塞下载(从断点处继续)
$result = ftp_nb_get($this->ftp_conn, $local_file, $remote_file, FTP_BINARY, $resume_pos);
$total_size = ftp_size($this->ftp_conn, $remote_file);
if ($total_size == -1) {
$total_size = 0;
}
$start_time = time();
$last_update = time();
while ($result == FTP_MOREDATA) {
// 获取当前下载大小
$current_size = file_exists($local_file) ? filesize($local_file) : 0;
// 显示进度
$current_time = time();
if ($current_time - $last_update >= 1) {
$progress = $total_size > 0 ? round(($current_size / $total_size) * 100, 1) : 0;
$elapsed = $current_time - $start_time;
$speed = $elapsed > 0 ? $current_size / $elapsed : 0;
$eta = $speed > 0 ? round(($total_size - $current_size) / $speed) : 0;
echo sprintf("\r进度: %.1f%% | 已下载: %s/%s | 速度: %s/s | ETA: %ds",
$progress,
formatBytes($current_size),
formatBytes($total_size),
formatBytes($speed),
$eta
);
$last_update = $current_time;
// 更新断点信息
$this->saveBreakpoint($remote_file, $local_file, $current_size);
}
// 继续传输
$result = ftp_nb_continue($this->ftp_conn);
usleep(50000); // 0.05秒
// 检查是否超时(30秒无进展)
if ($current_time - $start_time > 30 && $current_size == $resume_pos) {
echo "\n下载超时,已保存断点\n";
break;
}
}
// 清除断点信息
$this->clearBreakpoint($remote_file, $local_file);
if ($result == FTP_FINISHED) {
echo "\n下载完成!\n";
return true;
} elseif ($result == FTP_FAILED) {
echo "\n下载失败\n";
return false;
}
return false;
}
private function saveBreakpoint($remote_file, $local_file, $position) {
$breakpoints = [];
if (file_exists($this->breakpoint_file)) {
$breakpoints = json_decode(file_get_contents($this->breakpoint_file), true);
}
$key = md5($remote_file . $local_file);
$breakpoints[$key] = [
'remote_file' => $remote_file,
'local_file' => $local_file,
'position' => $position,
'timestamp' => time()
];
file_put_contents($this->breakpoint_file, json_encode($breakpoints, JSON_PRETTY_PRINT));
}
private function clearBreakpoint($remote_file, $local_file) {
if (!file_exists($this->breakpoint_file)) {
return;
}
$breakpoints = json_decode(file_get_contents($this->breakpoint_file), true);
$key = md5($remote_file . $local_file);
if (isset($breakpoints[$key])) {
unset($breakpoints[$key]);
file_put_contents($this->breakpoint_file, json_encode($breakpoints, JSON_PRETTY_PRINT));
}
}
public function __destruct() {
if ($this->ftp_conn) {
ftp_close($this->ftp_conn);
}
}
}
// 使用示例
try {
$downloader = new ResumeFTPDownload('localhost', 'user', 'pass');
$remote_file = 'large_file.zip';
$local_file = 'downloads/large_file.zip';
// 确保下载目录存在
$dir = dirname($local_file);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$success = $downloader->downloadWithResume($remote_file, $local_file);
if ($success) {
echo "文件下载成功: $local_file\n";
} else {
echo "文件下载失败,下次将从断点继续\n";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
?>
示例3:批量文件下载管理器
<?php
class BatchFTPDownloader {
private $ftp_conn;
private $download_queue = [];
private $active_downloads = [];
private $max_concurrent = 3; // 最大并发下载数
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 addToQueue($remote_file, $local_file) {
$this->download_queue[] = [
'remote' => $remote_file,
'local' => $local_file,
'status' => 'pending',
'progress' => 0,
'size' => 0
];
}
public function startDownloads() {
echo "开始批量下载...\n";
$iteration = 0;
$max_iterations = 10000;
while (($this->hasPendingDownloads() || !empty($this->active_downloads))
&& $iteration < $max_iterations) {
// 启动新的下载(如果未达到并发上限)
$this->startNewDownloads();
// 处理正在进行的下载
$this->processActiveDownloads();
// 显示状态
if ($iteration % 20 == 0) {
$this->showStatus();
}
usleep(100000); // 0.1秒
$iteration++;
}
echo "\n批量下载完成\n";
$this->showFinalReport();
}
private function startNewDownloads() {
$available_slots = $this->max_concurrent - count($this->active_downloads);
if ($available_slots <= 0) {
return;
}
foreach ($this->download_queue as &$item) {
if ($item['status'] == 'pending' && $available_slots > 0) {
// 确保本地目录存在
$dir = dirname($item['local']);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// 获取文件大小
$item['size'] = ftp_size($this->ftp_conn, $item['remote']);
if ($item['size'] == -1) {
$item['size'] = 0;
}
// 开始非阻塞下载
$result = ftp_nb_get($this->ftp_conn, $item['local'], $item['remote'], FTP_BINARY);
if ($result == FTP_FAILED) {
$item['status'] = 'failed';
echo "开始下载失败: {$item['remote']}\n";
} else {
$item['status'] = 'downloading';
$item['ftp_result'] = $result;
$item['start_time'] = time();
$item['last_size'] = 0;
$this->active_downloads[] = &$item;
$available_slots--;
echo "开始下载: {$item['remote']}\n";
}
}
}
}
private function processActiveDownloads() {
foreach ($this->active_downloads as $key => &$download) {
if ($download['status'] == 'downloading' && $download['ftp_result'] == FTP_MOREDATA) {
// 继续下载
$download['ftp_result'] = ftp_nb_continue($this->ftp_conn);
// 更新进度
if (file_exists($download['local'])) {
$current_size = filesize($download['local']);
$download['progress'] = $download['size'] > 0 ?
round(($current_size / $download['size']) * 100, 1) : 0;
// 计算速度
$elapsed = time() - $download['start_time'];
if ($elapsed > 0) {
$download['speed'] = $current_size / $elapsed;
}
}
// 检查是否完成
if ($download['ftp_result'] == FTP_FINISHED) {
$download['status'] = 'completed';
$download['progress'] = 100;
$download['end_time'] = time();
echo "下载完成: {$download['remote']}\n";
unset($this->active_downloads[$key]);
} elseif ($download['ftp_result'] == FTP_FAILED) {
$download['status'] = 'failed';
echo "下载失败: {$download['remote']}\n";
unset($this->active_downloads[$key]);
}
}
}
}
private function hasPendingDownloads() {
foreach ($this->download_queue as $item) {
if ($item['status'] == 'pending') {
return true;
}
}
return false;
}
private function showStatus() {
$completed = 0;
$downloading = 0;
$failed = 0;
$pending = 0;
$total_size = 0;
$downloaded_size = 0;
foreach ($this->download_queue as $item) {
switch ($item['status']) {
case 'completed':
$completed++;
$downloaded_size += $item['size'];
break;
case 'downloading':
$downloading++;
if (file_exists($item['local'])) {
$downloaded_size += filesize($item['local']);
}
break;
case 'failed':
$failed++;
break;
case 'pending':
$pending++;
break;
}
$total_size += $item['size'];
}
$progress = $total_size > 0 ? round(($downloaded_size / $total_size) * 100, 1) : 0;
echo "\n=== 下载状态 ===\n";
echo "总文件数: " . count($this->download_queue) . "\n";
echo "已完成: $completed | 下载中: $downloading | 失败: $failed | 等待: $pending\n";
echo "总体进度: $progress% (" . formatBytes($downloaded_size) . "/" . formatBytes($total_size) . ")\n";
// 显示当前下载的文件
if (!empty($this->active_downloads)) {
echo "\n当前下载文件:\n";
foreach ($this->active_downloads as $download) {
echo " {$download['remote']}: {$download['progress']}%";
if (isset($download['speed'])) {
echo " 速度: " . formatBytes($download['speed']) . "/s";
}
echo "\n";
}
}
echo "================\n";
}
private function showFinalReport() {
$report = [
'total' => 0,
'completed' => 0,
'failed' => 0,
'total_size' => 0,
'downloaded_size' => 0
];
foreach ($this->download_queue as $item) {
$report['total']++;
if ($item['status'] == 'completed') {
$report['completed']++;
$report['total_size'] += $item['size'];
$report['downloaded_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['downloaded_size']) . "\n";
if ($report['total'] > 0) {
$success_rate = ($report['completed'] / $report['total']) * 100;
echo "成功率: " . round($success_rate, 1) . "%\n";
}
}
public function __destruct() {
if ($this->ftp_conn) {
ftp_close($this->ftp_conn);
}
}
}
// 使用示例
try {
$downloader = new BatchFTPDownloader('localhost', 'user', 'pass');
// 添加下载任务
$files = [
'/remote/path/file1.pdf' => 'downloads/file1.pdf',
'/remote/path/file2.jpg' => 'downloads/file2.jpg',
'/remote/path/file3.zip' => 'downloads/file3.zip',
'/remote/path/file4.txt' => 'downloads/file4.txt',
'/remote/path/file5.mp3' => 'downloads/file5.mp3'
];
foreach ($files as $remote => $local) {
$downloader->addToQueue($remote, $local);
}
// 开始批量下载
$downloader->startDownloads();
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
?>
注意事项
- 非阻塞操作需要循环调用
ftp_nb_continue()直到传输完成
- 确保本地目录有写入权限
- 断点续传功能需要服务器支持
REST命令
- 在传输过程中,可以执行其他PHP代码,但要注意FTP连接保持
- 二进制模式(
FTP_BINARY)适用于所有文件类型
- 注意添加适当的延迟(
usleep())以避免过度占用CPU
- 大文件下载时注意PHP执行时间限制和内存限制
最佳实践建议
- 使用
ftp_pasv($ftp_conn, true)启用被动模式
- 实现下载进度反馈机制
- 使用二进制模式传输所有文件类型
- 实现断点续传功能
- 添加适当的错误处理和重试机制
- 批量下载时限制并发数量
- 不要同时打开过多的并发下载
- 避免在下载过程中修改本地文件
- 不要忘记添加适当的延迟
- 避免在没有进度反馈的情况下下载大文件
- 不要假设所有服务器都支持断点续传
- 避免在没有错误处理的情况下使用
性能优化技巧
- 调整缓冲区大小:PHP内部有传输缓冲区,可通过调整循环延迟来优化
- 并发控制:根据服务器性能和网络带宽调整并发下载数量
- 内存管理:下载大文件时注意内存使用,定期释放不需要的变量
- 连接复用:批量下载时尽量复用同一个FTP连接
- 超时设置:设置合理的超时时间,避免无限等待
- 日志记录:记录下载日志,便于问题排查和性能分析
相关函数
ftp_nb_continue() - 继续非阻塞的FTP操作
ftp_nb_fget() - 非阻塞方式下载文件到已打开的文件指针
ftp_get() - 阻塞方式下载文件
ftp_nb_put() - 非阻塞方式上传文件到FTP服务器
ftp_nb_fput() - 非阻塞方式上传已打开的文件
ftp_size() - 获取远程文件大小
ftp_mdtm() - 获取远程文件的最后修改时间