引言
ftp协议是一种专门用来文件传输的协议,但是随着网络的发展,用户体验度需要更加友好,ftp协议显得那么的无力。网上还有一些网络会问到ftp协议已经过时了么?至于是否真的过时笔者不敢轻易下定论。
暂且先不管ftp如何,http协议同样支持文件传输,但是总有人说http不适合大文件传输,但是多大的文件算大文件,又没有人回答的清楚,网上找了很多资料也没有找到更加有说服力的文献。可能这种想法是对于2G时代的http来说的吧,笔者也不敢轻易下结论。说来惭愧,java开发多年,并没有对文件上传下载做过一些专门的研究。如果有一天突然来一个网盘这样的功能,可能还无处下手。那么趁周末了解了下更加友好的上传方式。断点续传。
通过散列函数进行文件的标识,来判断文件是否有上传过。散列函数(Hash),一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。附录中可查看散列函数的特性。
文件切割,这个就是客户端的工作了,对于一个专职后端的开发来说,做文件切割,虽然是有一点点的困难,但是简单的做一做还是必须要可以的。上代码!
<form enctype="multipart/form-data">
<input type="file" id="file" value="选择文件"/>
form>
简单来一个文件上传的入口。通过ajax进行上传。
// 引入jquery
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
$("#file").change(function () {
var file = $("#file").get(0).files[0];
// 查询是否有上传过
// 这里应该是有一个查询的逻辑的,不在做了哈
// 就直接冲第0个分片开始传了。
var shardIndex = 0;
uploadFile(file,shardIndex);
})
通过file.slice
方法进行文件的切割,用来处理开始字节数和结束字节的位置。
function uploadFile(file,shardIndex) {
// 声明 文件信息和分片信息。
// 注意:shardTotal是总分片数,即使后面有一点点字节,也要进行加1操作。避免丢了字节。
var name = file.name,
size = file.size,
shardSize = 1024*1024*10,
shardTotal = Math.ceil( size /shardSize);
if(shardIndex >=shardTotal){
return ;
}
var start = shardIndex*shardSize;
var end = Math.min(start+shardSize,size);
// 文件切割
var packet = file.slice(start,end);
var formData = new FormData();
formData.append("file",packet);
// formData.append("hashCode",md5(file));
formData.append("hashCode","1111"); // 这里应该进行hash运算。用111代替一下
formData.append("fileName",name);
formData.append("size",size);
formData.append("shardIndex",shardIndex);
formData.append("shardSize",shardSize);
formData.append("shardTotal",shardTotal);
// 通过ajax进行上传
$.ajax({
url:"http://localhost:8080/file/upload",
type:"post",
cache: false,
data: formData,
processData: false,
contentType: false,
success :function (data) {
console.log(data);
//这里进行循环调用,把所有的分片循环上传
if(shardIndex<shardTotal){
shardIndex++;
uploadFile(file,shardIndex);
}
}
});
}
前后端分离太久了, 就不善于看前端代码,慢慢的更熟悉http请求信息了。请求的信息长这个样子,后来进行接收就好了。认准有二进制信息的提交。如果没有,可能是前端代码有问题了。
接收文件信息
public class FileInfo {
private String hasdCode;
private String fileName;
private Integer size;
private Integer shardIndex;
private Integer shardSize;
private Float shardTotal;
}
@RestController
public class FileController {
private static HashMap<String,FileInfo> fileMap = new HashMap();
// 模拟保存的位置
private String savePath="E:\\data";
@PostMapping(value = "/file/upload")
public void fileUpload(FileInfo fileInfo, HttpServletRequest request) throws IOException {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile multipartFile = multipartRequest.getFile("file"); // 通过参数名获取指定文件
//创建分片
File fileblob = new File(savePath+"\\"+fileInfo.getFileName()+"."+"blob");
System.out.println("保存分片"+fileblob.getName());
//保存分片 后面为true 表示在已有的文件进行追加。
FileCopyUtils.copy(multipartFile.getInputStream(),new FileOutputStream(fileblob,true));
//判断是否全部上传完成。
if((fileInfo.getShardIndex()+1) == Math.ceil(fileInfo.getShardTotal())){
System.out.println("上传完成,重命名");
File saveFile = new File(savePath+"\\"+fileInfo.getFileName());
//重命名操作
fileblob.renameTo(saveFile);
}
// 类似保存到数据库,这里简化下,直接放到map中。
fileMap.put(fileInfo.getHasdCode(),fileInfo);
}
@GetMapping("/getfile/{hashCode}")
public FileInfo getFileInfoByCode(@PathVariable("hashCode") String hashCode){
return fileMap.get(hashCode);
}
}
至此,断点续传的核心逻辑是完成了。但是代码是有很多需要优化。比如数据库如何进行保存数据等等。
简单的文件复制大家都直到,但是如何复制的更快,却是一个比较难的优化,毕竟java在对系统操作的时候有先天的不足。如果有兴趣的可以看下这个文章,讲述了如何更好的进行文件IO的处理。不过对于一般开发的应用程序来说,既然都用到java来操作文件了,应该也不太在乎复制效率了,这个道理貌似和”吃泡面的时候还在乎什么健康“一样。
笔者环境
spring boot 2.3.1.RELEASE JDK1.8
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>file-upload-test</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
散列函数
所有散列函数都有如下一个基本特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。但另一方面,散列函数的输入和输出不是一一对应的,如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等(可能出现哈希碰撞)。输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。
更多散列函数信息