普通NFS文件传输的实现

今天我给大家分享一下基于普通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。

步骤分解:
  1. 获取文件扩展名:

    • 如果fileName不为空且包含点号,则提取扩展名(如.mp4)。
  2. 生成唯一文件名:

    • 使用UUID生成一个唯一字符串(去掉横线),并加上扩展名,得到新文件名nfileName
  3. 初始化变量:

    • 输入输出流:inputStream(从本地文件读取)和outputStream(写入到NFS)。
    • 网速监控相关变量:
      • fileSize: 本地文件大小(字节)
      • totalRead: 已读取(上传)的字节数
      • startTime: 方法开始执行的时间(用于整体耗时统计,但注意后面有实际传输开始时间)
      • lastLogTime: 上次记录日志的时间
      • lastBytes: 上次记录日志时已上传的字节数
  4. NFS连接初始化:

    • 创建Nfs3对象:指定NFS服务器地址、路径和凭证(这里使用Unix凭证,uid和gid均为99,无密码)。
    • 创建Nfs3File对象,表示要写入的NFS文件(路径为videoPath下的新文件名)。
  5. 准备流:

    • inputStream: 使用BufferedInputStream包装FileInputStream,缓冲区32KB。
    • outputStream: 使用BufferedOutputStream包装NfsFileOutputStream
  6. 文件传输循环:

    • 使用32KB的缓冲区读取本地文件,并写入到NFS输出流。
    • 在循环中,累计已读取的字节数totalRead
    • 速度监控与日志输出:
      • 每100毫秒或每上传1MB数据时,计算并输出一次传输信息。
      • 计算实际传输时间(从开始传输算起)、瞬时速度(两次记录之间的速度)和平均速度(总数据量除以总时间)。
      • 计算进度百分比。
      • 格式化输出:进度、已传输数据量、平均速度、瞬时速度、预计剩余时间(ETA)。
      • 限制日志刷新频率:当进度变化超过2%或时间间隔超过1秒时才输出,避免过于频繁。
  7. 传输完成后的处理:

    • 计算总耗时和平均速度。
    • 输出完成信息,包括文件名、平均速度和总耗时。
  8. 异常处理与资源关闭:

    • 捕获异常并打印堆栈。
    • 在finally块中关闭输入输出流(使用closeQuietly方法,忽略关闭异常)。
  9. 返回结果:

    • 构造一个Map,包含两个键值对:
      • videoUploadName: 新生成的文件名(nfileName
      • videoPath: 文件在NFS服务器上的完整路径(videoPath + "/" + nfileName
    • 打印新文件名并返回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) {
                // 静默关闭
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值