PHP ftp_nb_fget() 函数用于以非阻塞方式从FTP服务器下载文件,并写入一个已打开的文件指针。
特点:此函数与 ftp_fget() 类似,但以非阻塞模式运行,允许在文件传输过程中执行其他PHP代码。
语法
ftp_nb_fget(
resource $ftp,
resource $handle,
string $remote_file,
int $mode = FTP_BINARY,
int $resumepos = 0
): int
参数说明
| 参数 |
描述 |
ftp |
必需。FTP连接的标识符,由ftp_connect()或ftp_ssl_connect()返回 |
handle |
必需。已打开的文件指针,用于写入下载的数据。文件必须以写入模式打开 |
remote_file |
必需。FTP服务器上要下载的文件路径 |
mode |
可选。传输模式,可以是:
FTP_ASCII - 文本模式(ASCII)
FTP_BINARY - 二进制模式(默认)
|
resumepos |
可选。从远程文件的指定位置开始下载(断点续传) |
返回值
FTP_FAILED (0) - 传输失败
FTP_FINISHED (1) - 传输完成
FTP_MOREDATA (2) - 传输仍在继续
与ftp_fget()的区别
| 特性 |
ftp_fget() |
ftp_nb_fget() |
| 阻塞性 |
阻塞模式 |
非阻塞模式 |
| 执行方式 |
脚本等待传输完成 |
传输过程中可执行其他代码 |
| 适用场景 |
小文件或简单传输 |
大文件传输或需要并发的场景 |
| 返回值 |
布尔值(成功/失败) |
状态码(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 = "download.txt";
$handle = fopen($local_file, 'w');
if (!$handle) {
die("无法打开文件: $local_file");
}
// 开始非阻塞下载
$remote_file = "data/example.txt";
$result = ftp_nb_fget($ftp_conn, $handle, $remote_file, FTP_ASCII);
// 循环处理传输
while ($result == FTP_MOREDATA) {
echo "正在下载文件...\n";
// 可以在这里执行其他任务
// 例如:更新进度、检查用户输入等
// 继续传输
$result = ftp_nb_continue($ftp_conn);
// 添加延迟以避免过度占用CPU
usleep(100000); // 0.1秒
}
// 关闭文件句柄
fclose($handle);
// 检查传输结果
if ($result == FTP_FINISHED) {
echo "文件下载完成!保存为: $local_file\n";
// 显示文件信息
$file_size = filesize($local_file);
echo "文件大小: " . $file_size . " 字节\n";
} elseif ($result == FTP_FAILED) {
echo "文件下载失败!\n";
// 删除可能不完整的文件
if (file_exists($local_file)) {
unlink($local_file);
}
}
// 关闭FTP连接
ftp_close($ftp_conn);
?>
示例2:断点续传功能
<?php
function resumeDownload($ftp_conn, $remote_file, $local_file, $mode = FTP_BINARY) {
// 检查本地文件是否已存在(部分下载)
$resume_pos = 0;
$handle = null;
if (file_exists($local_file)) {
$resume_pos = filesize($local_file);
$handle = fopen($local_file, 'a'); // 追加模式
echo "发现已下载的部分,将从 {$resume_pos} 字节处继续下载\n";
} else {
$handle = fopen($local_file, 'w'); // 新建文件
}
if (!$handle) {
die("无法打开文件: $local_file");
}
// 开始非阻塞下载(从断点处继续)
$result = ftp_nb_fget($ftp_conn, $handle, $remote_file, $mode, $resume_pos);
$start_time = time();
$last_size = $resume_pos;
while ($result == FTP_MOREDATA) {
// 计算下载速度
$current_size = ftell($handle);
$elapsed_time = time() - $start_time;
if ($elapsed_time > 0) {
$speed = ($current_size - $last_size) / $elapsed_time;
$last_size = $current_size;
$start_time = time();
echo "\r已下载: " . formatBytes($current_size) .
" | 速度: " . formatBytes($speed) . "/s";
}
// 继续传输
$result = ftp_nb_continue($ftp_conn);
usleep(50000); // 0.05秒
}
fclose($handle);
if ($result == FTP_FINISHED) {
echo "\n下载完成!文件: $local_file\n";
return true;
} else {
echo "\n下载失败!\n";
return false;
}
}
// 辅助函数:格式化字节大小
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];
}
// 使用示例
$ftp_conn = ftp_connect('localhost');
if ($ftp_conn && ftp_login($ftp_conn, 'user', 'pass')) {
ftp_pasv($ftp_conn, true);
$remote_file = 'largefile.zip';
$local_file = 'largefile_download.zip';
resumeDownload($ftp_conn, $remote_file, $local_file, FTP_BINARY);
ftp_close($ftp_conn);
} else {
echo "无法连接FTP服务器\n";
}
?>
示例3:多个文件并发下载
<?php
class ConcurrentFTPDownloader {
private $ftp_conn;
private $downloads = [];
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 addDownload($remote_file, $local_file) {
$handle = fopen($local_file, 'w');
if (!$handle) {
throw new Exception("无法打开文件: $local_file");
}
// 开始非阻塞下载
$result = ftp_nb_fget($this->ftp_conn, $handle, $remote_file, FTP_BINARY);
$this->downloads[] = [
'remote' => $remote_file,
'local' => $local_file,
'handle' => $handle,
'status' => $result,
'progress' => 0
];
return count($this->downloads) - 1; // 返回下载ID
}
public function processDownloads($callback = null) {
$active_downloads = array_filter($this->downloads, function($dl) {
return $dl['status'] == FTP_MOREDATA;
});
if (empty($active_downloads)) {
return false;
}
foreach ($this->downloads as &$download) {
if ($download['status'] == FTP_MOREDATA) {
$download['status'] = ftp_nb_continue($this->ftp_conn);
// 更新进度
if (is_resource($download['handle'])) {
$download['progress'] = ftell($download['handle']);
}
// 调用回调函数
if ($callback && is_callable($callback)) {
$callback($download);
}
// 如果下载完成,关闭文件句柄
if ($download['status'] == FTP_FINISHED ||
$download['status'] == FTP_FAILED) {
fclose($download['handle']);
}
}
}
return true;
}
public function getStatus() {
$status = [
'total' => count($this->downloads),
'finished' => 0,
'failed' => 0,
'active' => 0
];
foreach ($this->downloads as $download) {
if ($download['status'] == FTP_FINISHED) {
$status['finished']++;
} elseif ($download['status'] == FTP_FAILED) {
$status['failed']++;
} elseif ($download['status'] == FTP_MOREDATA) {
$status['active']++;
}
}
return $status;
}
public function __destruct() {
// 关闭所有打开的文件句柄
foreach ($this->downloads as $download) {
if (is_resource($download['handle'])) {
fclose($download['handle']);
}
}
// 关闭FTP连接
if ($this->ftp_conn) {
ftp_close($this->ftp_conn);
}
}
}
// 使用示例
try {
$downloader = new ConcurrentFTPDownloader('localhost', 'user', 'pass');
// 添加多个下载任务
$downloader->addDownload('files/doc1.pdf', 'downloads/doc1.pdf');
$downloader->addDownload('files/doc2.pdf', 'downloads/doc2.pdf');
$downloader->addDownload('files/doc3.pdf', 'downloads/doc3.pdf');
echo "开始并发下载...\n";
$max_iterations = 500; // 最大迭代次数
$iteration = 0;
// 进度回调函数
$progressCallback = function($download) {
static $last_output = '';
$status = $download['status'] == FTP_MOREDATA ? '下载中' :
($download['status'] == FTP_FINISHED ? '完成' : '失败');
$output = "文件: {$download['remote']} - 状态: {$status}";
if ($download['progress'] > 0) {
$output .= " - 进度: " . $download['progress'] . " 字节";
}
if ($output !== $last_output) {
echo $output . "\n";
$last_output = $output;
}
};
// 处理下载直到所有完成或达到最大迭代次数
while ($downloader->processDownloads($progressCallback) && $iteration < $max_iterations) {
usleep(100000); // 0.1秒
$iteration++;
// 每10次迭代显示一次状态
if ($iteration % 10 == 0) {
$status = $downloader->getStatus();
echo "总任务: {$status['total']}, 完成: {$status['finished']}, ";
echo "失败: {$status['failed']}, 进行中: {$status['active']}\n";
}
}
echo "下载处理完成\n";
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
?>
注意事项
- 必须确保文件指针以正确的模式打开('w'用于写入,'a'用于追加)
- 非阻塞操作需要循环调用
ftp_nb_continue()直到传输完成
- 在传输过程中,文件指针会不断移动,可以使用
ftell()获取当前位置
- 断点续传功能需要服务器支持
- 确保在处理过程中保持FTP连接有效
- 传输完成后必须关闭文件句柄以释放资源
- 二进制模式(
FTP_BINARY)适用于所有文件类型,特别是图像、ZIP等
最佳实践建议
- 使用
ftp_pasv($ftp_conn, true)启用被动模式
- 添加适当的延迟(
usleep())避免过度占用CPU
- 实现进度反馈机制
- 使用try-catch处理异常
- 传输完成后检查文件完整性
- 使用二进制模式传输所有文件类型
- 不要在传输过程中随意移动文件指针
- 避免在传输过程中关闭FTP连接
- 不要忘记关闭打开的文件句柄
- 避免使用过短的延迟导致CPU使用率高
- 不要假设所有服务器都支持断点续传
- 避免在没有错误处理的情况下使用
相关函数
ftp_nb_continue() - 继续非阻塞的FTP操作
ftp_nb_get() - 非阻塞方式下载文件到本地路径
ftp_fget() - 阻塞方式下载文件到已打开的文件指针
ftp_nb_put() - 非阻塞方式上传文件到FTP服务器
ftp_nb_fput() - 非阻塞方式上传已打开的文件
fopen() - 打开文件或URL
fclose() - 关闭一个已打开的文件指针