java 超大文件分片上传

前言

最近越来越感觉一个东西做到极致才是最厉害的,懂点皮毛的人太多了,正如文件上传谁都会,我还会加进度条呢。但你肯定没有做过 4-5 个 G 的文件上传,需要考虑的东西就多了

  • 比如内存占用,我不可能把文件全读进内存吧,二三个并发你的服务就死翘翘,
  • 比如超时,因为大文件需要占用较长的上传时间,你上传个 30 分钟 session 超时了
  • 比如上传大小限制 ,默认大小限制是 4m
  • 还需要从客户端上传到服务器端每次是有多少数据是保存进内存的,虽然最后是存的临时文件,然后临时文件存储的路径
  • 还有带宽问题,如何给单个客户端限速,不让其把全部带宽都占了等

本文研究的问题

本文主要研究把大文件进行分片,然后分段上传,最后再做文件校验。

本文不研究客户端限速问题,因为公司项目上传在局域网内,千兆网卡,使用人数不多,不存在带宽问题。

其它解决方案:

  • 使用 nginx 的 fileupload 模块
  • 让运营人员单独再登录 ftp 上传

本文还存在的局限

  • 使用的单线程分片上传,只能等上片传完才能传下片
  • 没有做断点续传
  • 文件数据其实不能直接追加上去,很有可能当前这片写到一半,断网了,那么整个文件都坏了,和没分片没有多大区别

所以本文章只是抛砖引玉,有一个大致的思路来做大文件上传,愿 csdn 有牛人写出更好的大文件上传程序,最好可以封装成一个插件,即插即用。

哪位封装好了记得告诉我一声,参考参考 。

代码部分

后面的代码是经过实际测试的,可以使用,并可以在基础上扩展

1. 首先我们根据当前文件生成文件的一些属性信息

比如服务器的存放位置,文件大小,是否有相似文件,做得好一点的话,可以计算文件 md5 值至后台库做文件比对,如果存在文件可以实现秒传。

 /**
* 首先需要获取文件上传元数据信息
 * @return
 */
@GetMapping("/fileMetaData")
public FileMetaData fileMetaData(String fileName,long fileSize) throws URISyntaxException {
    String extension = FilenameUtils.getExtension(fileName);
    String finalFileName = System.currentTimeMillis() + "."+ extension;

    String yyyyMMdd = DateFormatUtils.format(System.currentTimeMillis(),"yyyyMMdd");
    URI relativePath = new URI("/"+appName+"/"+yyyyMMdd+"/"+finalFileName);
    URI absoluteURI = new URI("https://localhost:8080/").resolve(relativePath);

    return new FileMetaData(fileName,finalFileName,fileSize,relativePath.toString(),absoluteURI.toString(),false);
}

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
public class FileMetaData {
    // 原始文件名和最终文件名
    @NonNull private String originFileName;
    @NonNull private String finalFileName;
    //文件大小
    @NonNull private long fileSize;
    // 相对路径和绝对路径
    private String relativePath;
    private String absolutePath;
    // 是否存在相似文件
    private boolean similarFile;
}

2. 然后我们使劲的住后台返回的那个文件地址丢数据

@RequestMapping("/uploadLocal")
public void uploadLocal(HttpServletRequest request, String relativePath) throws IOException {
    // 转型为MultipartHttpRequest:
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    // 获得文件:
    MultipartFile multipartFile = multipartRequest.getFile("file");

    File targetFile = new File(relativePath);
    if(!targetFile.getParentFile().exists()){
        targetFile.getParentFile().mkdirs();
    }
    if(!targetFile.exists()){targetFile.createNewFile();}

    FileOutputStream fileOutputStream = new FileOutputStream(targetFile,true);
    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
    IOUtils.copy(multipartFile.getInputStream(),bufferedOutputStream);
    bufferedOutputStream.flush();

    IOUtils.closeQuietly(bufferedOutputStream);
    IOUtils.closeQuietly(fileOutputStream);
}

3. 最后我们验证客户端文件和服务器文件是否一致

可以先验证文件大小,文件类型,最后通过 md5 来判断文件是否一致

public boolean validateFile(String relativePath,long fileSize,String md5) {
	//根据相对路径获取文件
	File file = new File(relativePath);
	long length = file.length();
	if(length != fileSize){
		return false;
	}
	//计算 md5 值,并与前端计算的 md5 值进行比较
	
}

前端分片逻辑代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>大文件分片上传</title>
</head>
<body>

<input type="file" name="file" id="bigfile"/>
<button id="start">开始上传</button>
<div id="msg">
    <label for="">文件名称:</label><span key="fileName"></span><br>
    <label for="">文件大小:</label><span key="fileSize"></span><br>
    <label for="">相对路径:</label><span key="relativePath"></span><br><br>

    <label for="">分片数量:</label><span key="shardCount"></span><br>
    <label for="">每片大小:</label><span key="shardSize"></span><br><br>

    <label for="">当前进度:</label><span key="process"></span><br>
    <label for="">当前分片:</label><span key="shard"></span><br>
    <label for="">当前上传大小:</label><span key="loaded"></span><br>
</div>
<script src="jquery-1.8.3.min.js"></script>
<script>
    $(function () {
        var fileMeta = 'http://172.24.11.21:8080/sftp/fileMetaData';
        var uploadLocal = 'http://172.24.11.21:8080/sftp/uploadLocal';
        var validateFile = 'http://172.24.11.21:8080/sftp/validateFile';
        var shardSize = 1024 * 1024 * 2;

        $('#msg>span[key=shardSize]').text(shardSize);

        // 文件修改事件绑定
        $('#bigfile').bind('change',changeFile);
        // 上传事件绑定
        $('#start').bind('click',shardUpload);

        var uploadMonitor = {
            shard:0,
            shardCount:0,
            onload:function (event) {
                // console.log('上传完成分片['+uploadMonitor.shard+'],总分片数['+uploadMonitor.shardCount+'],上传大小为['+event.loaded+']');
                $('#msg>span[key=shard]').text(uploadMonitor.shard);
                $('#msg>span[key=loaded]').text(event.loaded);
                $('#msg>span[key=process]').text(((uploadMonitor.shard / uploadMonitor.shardCount).toFixed(2) * 100)+ " %");
            },
            onabort:function (event) {

            },
            onprogress:function (event) {

            }
        }

        function shardUpload() {
            var files =  $('#bigfile')[0].files;
            var singleFile  = files[0];
            var shardCount =Math.ceil(singleFile.size/shardSize);

            for(var i=0;i<shardCount;i++){
                var start = i * shardSize;
                var end = Math.min(singleFile.size,start + shardSize);//在file.size和start+shardSize中取最小值,避免切片越界
                var file = singleFile.slice(start,end);
                var formData = new FormData();
                formData.append("file",file);
                formData.append('relativePath',uploadMonitor.relativePath);

                $.ajax({
                    async:false,
                    url: uploadLocal,
                    cache: false,
                    type: "POST",
                    data: formData,
                    dateType: 'json',
                    processData: false,
                    contentType: false,
                    xhr: function () {
                        var myXhr = $.ajaxSettings.xhr();
                        uploadMonitor.shardCount = shardCount;
                        uploadMonitor.shard = i;

                        myXhr.onload = uploadMonitor.onload
                        return myXhr;
                    },
                });
            }
        }

        function changeFile() {
            var files =  $('#bigfile')[0].files;
            var singleFile  = files[0];

            var originFileName = singleFile.name;
            var fileSize = singleFile.size;

            $.ajax({
                url:fileMeta,
                type:'get',
                contentType:'application/json',
                dataType:'json',
                data:{fileName:originFileName,fileSize:fileSize},
                success:function (fileMeta) {
                    var shardCount =Math.ceil(fileSize/shardSize);

                    $('#msg>span[key=fileName]').text(originFileName);
                    $('#msg>span[key=fileSize]').text(fileSize);
                    $('#msg>span[key=relativePath]').text(fileMeta.relativePath);
                    $('#msg>span[key=shardCount]').text(shardCount);

                    uploadMonitor.relativePath = fileMeta.relativePath
                }
            });
        }
    });
</script>
</body>
</html>

完整代码地址

超大文件上传完整代码

一点小推广

创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。

Excel 通用导入导出,支持 Excel 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi

使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven

发布了91 篇原创文章 · 获赞 75 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览