今天我给大家分享一下基于普通NFS文件传输方式的实现。虽然我前面已经介绍了NFS的基本概念,但是可能还是会有新朋友不清楚什么是NFS?
NFS(Network File System) 是一种分布式文件系统协议,通俗理解就是让多台计算机通过网络共享文件和目录的技术。它的核心目标是:像访问本地文件一样访问远程文件。
特点 | 说明 |
---|---|
核心原理 | 将远程服务器的存储空间挂载到本地,像操作本地磁盘一样读写文件 |
协议类型 | 基于客户端-服务器模型(Client-Server) |
典型场景 | 集群服务器共享存储、虚拟机共享磁盘、跨主机文件访问 |
在代码中的作用 | 代替传统FTP/SFTP,实现高性能直接文件读写 |
有了NFS的基本概念,我今天分享UploadFileTest类,它主要功能是上传文件到NFS服务器,并监控上传速度,显示进度、速度、剩余时间等信息。
类结构与代码分析
成员变量:
host
: NFS服务器的IP地址(172.20.22.159
)videoPath
: NFS服务器上存储视频的路径(/data/nginx/yktvideo
)
程序的入口点,创建一个UploadFileTest
实例
调用uploadVideoWithSpeedMonitor
方法,参数为一个本地文件(D:\\20250714231254.mp4
)和文件名(20250714231254.mp4
)
uploadVideoWithSpeedMonitor方法
这是核心方法,负责上传文件并监控上传速度。方法参数为本地文件localFile
和文件名fileName
,返回一个包含新生成的文件名和完整路径的Map。
步骤分解:
-
获取文件扩展名:
- 如果
fileName
不为空且包含点号,则提取扩展名(如.mp4
)。
- 如果
-
生成唯一文件名:
- 使用UUID生成一个唯一字符串(去掉横线),并加上扩展名,得到新文件名
nfileName
。
- 使用UUID生成一个唯一字符串(去掉横线),并加上扩展名,得到新文件名
-
初始化变量:
- 输入输出流:
inputStream
(从本地文件读取)和outputStream
(写入到NFS)。 - 网速监控相关变量:
fileSize
: 本地文件大小(字节)totalRead
: 已读取(上传)的字节数startTime
: 方法开始执行的时间(用于整体耗时统计,但注意后面有实际传输开始时间)lastLogTime
: 上次记录日志的时间lastBytes
: 上次记录日志时已上传的字节数
- 输入输出流:
-
NFS连接初始化:
- 创建
Nfs3
对象:指定NFS服务器地址、路径和凭证(这里使用Unix凭证,uid和gid均为99,无密码)。 - 创建
Nfs3File
对象,表示要写入的NFS文件(路径为videoPath
下的新文件名)。
- 创建
-
准备流:
inputStream
: 使用BufferedInputStream
包装FileInputStream
,缓冲区32KB。outputStream
: 使用BufferedOutputStream
包装NfsFileOutputStream
。
-
文件传输循环:
- 使用32KB的缓冲区读取本地文件,并写入到NFS输出流。
- 在循环中,累计已读取的字节数
totalRead
。 - 速度监控与日志输出:
- 每100毫秒或每上传1MB数据时,计算并输出一次传输信息。
- 计算实际传输时间(从开始传输算起)、瞬时速度(两次记录之间的速度)和平均速度(总数据量除以总时间)。
- 计算进度百分比。
- 格式化输出:进度、已传输数据量、平均速度、瞬时速度、预计剩余时间(ETA)。
- 限制日志刷新频率:当进度变化超过2%或时间间隔超过1秒时才输出,避免过于频繁。
-
传输完成后的处理:
- 计算总耗时和平均速度。
- 输出完成信息,包括文件名、平均速度和总耗时。
-
异常处理与资源关闭:
- 捕获异常并打印堆栈。
- 在finally块中关闭输入输出流(使用
closeQuietly
方法,忽略关闭异常)。
-
返回结果:
- 构造一个Map,包含两个键值对:
videoUploadName
: 新生成的文件名(nfileName
)videoPath
: 文件在NFS服务器上的完整路径(videoPath + "/" + nfileName
)
- 打印新文件名并返回Map。
- 构造一个Map,包含两个键值对:
4. 辅助方法
formatSpeed(double bytesPerSec)
: 将速度(字节/秒)格式化为更易读的单位(B/s, KB/s, MB/s, GB/s)。calculateETA(long currentSize, long totalSize, double speed)
: 计算剩余时间(以秒计),并格式化为MM:SS
。如果速度过低则返回--:--
。formatSize(long bytes)
: 将文件大小格式化为易读的单位(B, KB, MB)。closeQuietly(Closeable closeable)
: 安全关闭流,忽略关闭时的异常
核心源码
import com.emc.ecs.nfsclient.nfs.io.Nfs3File;
import com.emc.ecs.nfsclient.nfs.io.NfsFileOutputStream;
import com.emc.ecs.nfsclient.nfs.nfs3.Nfs3;
import com.emc.ecs.nfsclient.rpc.CredentialUnix;
import com.yinhai.ta404.core.utils.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class UploadFileTest {
String host = "172.20.22.159";
String videoPath = "/data/nginx/yktvideo";
public static void main(String[] args) {
UploadFileTest uploadFileTest = new UploadFileTest();
Map<String, String> map = uploadFileTest.uploadVideoWithSpeedMonitor(new File("D:\\20250714231254.mp4"), "20250714231254.mp4");
}
public Map<String, String> uploadVideoWithSpeedMonitor(File localFile, String fileName) {
// 获取文件后缀名
String ext = "";
if (StringUtils.isNotEmpty(fileName) && fileName.contains(".")) {
ext = fileName.substring(fileName.lastIndexOf("."));
}
// 生成UUID文件名
String uuid = UUID.randomUUID().toString().replace("-", "");
String nfileName = uuid + ext;
InputStream inputStream = null;
OutputStream outputStream = null;
// 网速监控相关变量
long fileSize = localFile.length();
long totalRead = 0;
long startTime = System.currentTimeMillis();
long lastLogTime = startTime;
long lastBytes = 0;
try {
// 初始化NFS连接
Nfs3 nfs3 = new Nfs3(host, videoPath, new CredentialUnix(99, 99, null), 3);
Nfs3File nfsFile = new Nfs3File(nfs3, "/" + nfileName);
inputStream = new BufferedInputStream(new FileInputStream(localFile));
outputStream = new BufferedOutputStream(new NfsFileOutputStream(nfsFile));
byte[] buffer = new byte[1024 * 32]; // 增大缓冲区到32KB
int bytesRead;
// 记录初始化完成后的实际开始传输时间
long actualStartTime = System.currentTimeMillis();
double lastProgress = 0.0;
System.out.printf("开始上传文件: %s (%.2f MB)%n", fileName, fileSize / (1024.0 * 1024));
while ((bytesRead = inputStream.read(buffer)) != -1) {
// 写入数据到NFS
outputStream.write(buffer, 0, bytesRead);
totalRead += bytesRead;
// 每100ms或每1MB数据时计算一次速度
long currentTime = System.currentTimeMillis();
if (currentTime - lastLogTime >= 100 || (totalRead - lastBytes) >= 1024 * 1024) {
// 计算传输耗时
double timeElapsedSec = (currentTime - actualStartTime) / 1000.0;
// 计算瞬时速度和平均速度
double instantSpeed = (totalRead - lastBytes) * 1000.0 / (currentTime - lastLogTime);
double avgSpeed = totalRead / Math.max(0.1, timeElapsedSec);
// 计算进度百分比
double progress = (totalRead * 100.0) / fileSize;
// 格式化输出
String progressStr = String.format("%.1f%%", progress);
String avgSpeedStr = formatSpeed(avgSpeed);
String instantSpeedStr = formatSpeed(instantSpeed);
String transferred = formatSize(totalRead);
// 限制频繁刷新进度,每2%变化或1秒间隔才输出
if (progress - lastProgress >= 2.0 || currentTime - lastLogTime >= 1000) {
System.out.printf("[%s] %s | ↑速度: %s (瞬时: %s) | ETA: %s%n",
progressStr, transferred, avgSpeedStr, instantSpeedStr,
calculateETA(totalRead, fileSize, avgSpeed));
lastProgress = progress;
}
lastLogTime = currentTime;
lastBytes = totalRead;
}
}
// 计算最终结果
long totalTime = System.currentTimeMillis() - actualStartTime;
double finalSpeed = totalRead / (totalTime / 1000.0);
System.out.printf("\n✅ 上传完成! 文件名: %s | 平均速度: %s | 总耗时: %.2f秒%n",
nfileName, formatSpeed(finalSpeed), totalTime / 1000.0);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
closeQuietly(outputStream);
closeQuietly(inputStream);
}
Map<String, String> map = new HashMap<>();
map.put("videoUploadName", nfileName);
map.put("videoPath", videoPath + "/" + nfileName);
System.out.println("nfileName == "+nfileName);
return map;
}
// 工具方法:格式化速度显示
private String formatSpeed(double bytesPerSec) {
if (bytesPerSec < 1024) {
return String.format("%.1f B/s", bytesPerSec);
} else if (bytesPerSec < 1024 * 1024) {
return String.format("%.1f KB/s", bytesPerSec / 1024);
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return String.format("%.1f MB/s", bytesPerSec / (1024 * 1024));
} else {
return String.format("%.1f GB/s", bytesPerSec / (1024 * 1024 * 1024));
}
}
// 工具方法:计算剩余时间
private String calculateETA(long currentSize, long totalSize, double speed) {
if (speed < 100) return "--:--"; // 低速时不计算
long remainingBytes = totalSize - currentSize;
long secondsRemaining = (long) (remainingBytes / Math.max(1, speed));
if (secondsRemaining <= 0) return "00:00";
long mins = secondsRemaining / 60;
long secs = secondsRemaining % 60;
return String.format("%02d:%02d", mins, secs);
}
// 工具方法:格式化文件大小
private String formatSize(long bytes) {
if (bytes < 1024) {
return bytes + "B";
} else if (bytes < 1024 * 1024) {
return String.format("%.1fKB", bytes / 1024.0);
} else {
return String.format("%.1fMB", bytes / (1024.0 * 1024));
}
}
// 安全关闭流
private void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// 静默关闭
}
}
}
}