@[toc]
一:字节流和字符流详解
1.1 流的概念
流是一种抽象的概念,就好比"水流",从一段流向另一端。在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
1.2 流的分类
按照传输单位可以分为 :字节流和字符流 按照流向可以分为 :输入流(如:键盘,鼠标),输出流(如:显示器,音箱) 输入流 :把数据从其他设备上读取到内存中的流。 输出流 :把数据从内存中写出到其他设备上的流。
1.3 字节流,字符流区别与使用场景
1.3.1 区别
字节流(InputStream和OutputStream): 它处理单元为1个字节(byte),操作字节和字节数组,存储的是二进制文件,如果是音频文件、图片、歌曲,就用字节流好点(1byte = 8位); 字符流(Reader和Writer): 它处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,如果是关系到中文(文本)的,用字符流好点(1Unicode = 2字节 = 16位);
1.3.2 使用场景
字节流:字节流可用于任何类型的对象,包括二进制对象。字节流提供了处理任何类型的IO操作的功能。例如音频文件、图片、歌曲等。但它不能直接处理Unicode字符,而字符流就可以。 字符流:将文件在记事本里面打开,如果打开后能看的懂的就是字符流,如果看不懂那就是字节流。
1.3.3 顶级父类

1.3.4 对比------总结对比字节流和字符流
- 字节流操作的基本单元是字节;字符流操作的基本单元为Unicode码元。
- 字节流在操作的时候本身不会用到缓冲区的,是与文件本身直接操作的;而字符流在操作的时候使用到缓冲区的。
- 所有文件的存储都是字节(byte)的存储,在磁盘上保留的是字节。
- 在使用字节流操作中,即使没有关闭资源(close方法),也能输出;而字符流不使用close方法的话,不会输出任何内容
二:断点续传的简述
2.1 概念
从文件上次中断的地方开始重新下载或上传,当下载或上传文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会去重头下载,这样很浪费时间。所以断点续传的功能就应运而生了。要实现断点续传的功能,需要客户端记录下当前的下载或上传进度,并在需要续传的时候通知服务端本次需要下载或上传的内容片段。
2.2 详细讲解
从 HTTP1.1 协议开始就已经支出获取文件的部分内容,断点续传技术就是利用 HTTP1.1 协议的这个特点在Header 里添加两个参数来实现的 。这两个参数分别是客户端请求时发送的 Range 和服务器返回信息时返回的 Content-Range - Range,Range 用于指定第一个字节和最后一个字节的位置,格式如下:Range:(unit=first byte pos)-[last byte pos]
2.2.1 Range 常用的格式有如下几种情况:
Range:bytes=0-1024
,表示传输的是从开头到第1024字节的内容; Range:bytes=1025-2048
,表示传输的是从第1025到2048字节范围的内容; Range:bytes=-2000
,表示传输的是最后2000字节的内容; Range:bytes=1024-
,表示传输的是从第1024字节开始到文件结束部分的内容; Range:bytes=0-0
,-1 表示传输的是第一个和最后一个字节 ; Range:bytes=1024-2048,2049-3096,3097-4096
,表示传输的是多个字节范围。
2.2.2 Content-Range
Content-Range 用于响应带有 Range 的请求。服务器会将 Content-Range 添加在响应的头部,格式如下:Content-Range:bytes(unit first byte pos)-[last byte pos]/[entity length]
常见的格式内容如下: Content-Range:bytes 2048-4096/10240
这里边 2048-4096 表示当前发送的数据范围, 10240 表示文件总大小。 这里我顺便说一下,如果在客户端请求报文头中,对 Range 填入了错误的范围值,服务器会返回 416 状态码。416 状态码表示服务器无法处理所请求的数据区间,常见的情况是请求的数据区间不在文件范围之内,也就是说,Range 值,从语法上来说是没问题的,但从语义上来说却没有意义。
注意:当使用断点续传的方式上传下载软件时 HTTP 响应头将会变为:HTTP/1.1 206 Partial Content 当然光有 Range 和 Content-Range 还是不够的,我们还要知道服务端是否支持断点续传,只需要从如下两方面判断即可: 判断服务端是否只 HTTP/1.1 及以上版本,如果是则支持断点续传,如果不是则不支持 服务端返回响应的头部是否包含 Access-Ranges ,且参数内容是 bytes 符合以上两个条件即可判定位支持断点续传。
2.2.3 校验
这里的校验主要针对断点续传下载来说的。当服务器端的文件发生改变时,客户端再次向服务端发送断点续传请求时,数据肯定就会发生错误。这时我们可以利用Last-Modified 来标识最后的修改时间,这样就可以判断服务器上的文件是否发生改变。和 Last-Modified具有同样功能的还有 if-Modified-Since,它俩的不同点是 Last-Modified 由服务器发送给客户端,而if-Modified-Since 是由客户端发出, if-Modified-Since 将先前服务器发送给客户端的Last-Modified发送给服务器,服务器进行最后修改时间验证后,来告知客户端是否需要重新从服务器端获取新内容。客户端判断是否需要更新,只需要判断服务器返回的状态码即可,206代表不需要重新获取接着下载就行,200代表需要重新获取。 但是 Last-Modified 和 if-Modified-Since存在一些问题:
- 某些文件只是修改了修改时间而内容却没变,这时我们并不希望客户端重新缓存这些文件;
- 某些文件修改频繁,有时一秒要修改十几次,但是 if-Modified-Since 是秒级的,无法判断比秒更小的级别; 部分服务器无法获得精确的修改时间。 要解决上述问题我们就需要用到 Etag ,只需将相关标记(例如文件版本号等)放在引号内即可。
- 当使用校验的时候我们不需要手动实现验证,只需要利用 if-Range 结合 Last-Modified 或者 Etage 来判断是否发生改变,如果没有发生改变服务器将向客户端发送剩余的部分,否则发送全部。
注意:If-Range 必须与 Range 配套使用。缺少其中任意一个另一个都会被忽略。
三:断点续传至服务器指定路径
3.1 前端文件------upload.html
java
@RequestMapping("/getUploadHtml")
public String getBreakPointHtml(){
return "breakPoint/upload";
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webuploader</title>
</head>
<!--引入CSS-->
<link rel="stylesheet" type="text/css" href="/css/webuploader.css">
<script src="/js/jquery.min.js"></script>
<script src="/js/webuploader.min.js"></script>
<style>
#upload-container, #upload-list{width: 500px; margin: 0 auto; }
#upload-container{cursor: pointer; border-radius: 15px; background: #EEEFFF; height: 200px;}
#upload-list{height: 800px; border: 1px solid #EEE; border-radius: 5px; margin-top: 10px; padding: 10px 20px;}
#upload-container>span{widows: 100%; text-align: center; color: gray; display: block; padding-top: 15%;}
.upload-item{margin-top: 5px; padding-bottom: 5px; border-bottom: 1px dashed gray;}
.percentage{height: 5px; background: green;}
.btn-delete, .btn-retry{cursor: pointer; color: gray;}
.btn-delete:hover{color: orange;}
.btn-retry:hover{color: green;}
</style>
<!--引入JS-->
<body>
<div id="upload-container">
<span>点击或将文件拖拽至此上传</span>
</div>
<div id="upload-list">
</div>
<button id="picker" style="display: none;">点击上传文件</button>
</body>
<script>
$('#upload-container').click(function(event) {
$("#picker").find('input').click();
});
var uploader = WebUploader.create({
auto: true,// 选完文件后,是否自动上传。
swf: 'Uploader.swf',// swf文件路径
server: 'http://localhost:8989/breakPoint/upload',// 文件接收服务端。
dnd: '#upload-container',
pick: '#picker',// 内部根据当前运行是创建,可能是input元素,也可能是flash. 这里是div的id
multiple: true, // 选择多个
chunked: true,// 开启分片上传。
threads: 20, // 上传并发数。允许同时最大上传进程数。
method: 'POST', // 文件上传方式,POST或者GET。
fileSizeLimit: 1024*1024*1024*20, //验证文件总大小是否超出限制, 超出则不允许加入队列。
fileSingleSizeLimit: 1024*1024*1024*5, //验证单个文件大小是否超出限制, 超出则不允许加入队列。
fileVal:'upload' // [默认值:'file'] 设置文件上传域的name。
});
uploader.on("beforeFileQueued", function(file) {
console.log(file); // 获取文件的后缀
});
uploader.on('fileQueued', function(file) {
// 选中文件时要做的事情,比如在页面中显示选中的文件并添加到文件列表,获取文件的大小,文件类型等
console.log(file.ext); // 获取文件的后缀
console.log(file.size);// 获取文件的大小
console.log(file.name);
var html = '' +
'<div class="upload-item">' +
'<span>文件名:'+file.name+'</span>' +
'<span data-file_id="'+file.id+'" class="btn-delete">删除</span>' +
'<span data-file_id="'+file.id+'" class="btn-retry">重试</span>' +
'<div class="percentage '+file.id+'" style="width: 0%;"></div>' +
'</div>';
$('#upload-list').append(html);
uploader.md5File( file )//大文件秒传
// 及时显示进度
.progress(function(percentage) {
console.log('Percentage:', percentage);
})
// 完成
.then(function(val) {
console.log('md5 result:', val);
});
});
uploader.on('uploadProgress', function(file, percentage) {
console.log(percentage * 100 + '%');
var width = $('.upload-item').width();
$('.'+file.id).width(width*percentage);
});
uploader.on('uploadSuccess', function(file, response) {
console.log(file.id+"传输成功");
});
uploader.on('uploadError', function(file) {
console.log(file);
console.log(file.id+'upload error')
});
$('#upload-list').on('click', '.upload-item .btn-delete', function() {
// 从文件队列中删除某个文件id
file_id = $(this).data('file_id');
// uploader.removeFile(file_id); // 标记文件状态为已取消
uploader.removeFile(file_id, true); // 从queue中删除
console.log(uploader.getFiles());
});
$('#upload-list').on('click', '.btn-retry', function() {
uploader.retry($(this).data('file_id'));
});
uploader.on('uploadComplete', function(file) {
console.log(uploader.getFiles());
});
</script>
</html>
3.2 代码实现
java
/**
* 文件上传的路径
*/
public static final String UPLOAD_PATH = "D:\\fileItem";
/**
* 编码格式
*/
public static final String UTF8 = "UTF-8";
@Autowired
private FileUploadDownService fileUploadDownService;
/**
* 断点续传
* 会将文件进行分片,每次只调用后台接口上传一个分片
* @param request
* @param response
* @throws IOException
*/
@RequestMapping("/upload")
public void upload( HttpServletRequest request , HttpServletResponse response ) {
//分片
response.setCharacterEncoding(UTF8);
Integer chunk = null;
Integer chunks = null;
String name = null;
BufferedOutputStream os = null;
try{
/**
* DiskFileItemFactory
* 作用:可以设置缓存大小以及临时文件保存位置.
* DiskFileItemFactory(int sizeThreshold, File repository)
* sizeThreshold :缓存大小,默认缓存大小是 10240(10k).
* repository:临时文件存储位置,临时文件默认存储在系统的临时文件目录下.(可以在环境变量中查看)
*/
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(1024);
factory.setRepository(new File(UPLOAD_PATH));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(5L * 1024L * 1024L * 1024L);
upload.setSizeMax(20L * 1024L * 1024L * 1024L);
List<FileItem> items = upload.parseRequest(request);
for(FileItem item : items){
/**
* boolean isFormField()。
* 如果是表单域,非文件域
* isFormField方法用来判断FileItem对象里面封装的数据是一个普通文本表单字段,还是一个文件表单字段。
* 如果是普通文本表单字段,返回一个true否则返回一个false。因此可以用该方法判断是否是普通表单域还是文件上传表单域。
*/
if(item.isFormField()){
if("chunk".equals(item.getFieldName())){
String value = item.getString(UTF8);
chunk = Integer.parseInt(value);
}
if("chunks".equals(item.getFieldName())){
String value = item.getString(UTF8);
chunks = Integer.parseInt(value);
}
if("name".equals(item.getFieldName())){
name = item.getString(UTF8);
}
} else {
String temFileName = name;
if(name != null){
if(chunk != null){
temFileName = chunk + "_" + name;
}
// 如果文件夹没有,则创建
File file = new File(UPLOAD_PATH);
if (!file.exists()) {
file.mkdirs();
}
// 保存文件
File chunkFile = new File(UPLOAD_PATH,temFileName);
if(chunkFile.exists()){
chunkFile.delete();
chunkFile = new File(UPLOAD_PATH,temFileName);
}
item.write(chunkFile);
}
}
}
/**
* 是否全部上传完成
* 所有分片都存在才说明整个文件上传完成
*/
boolean uploadDone = true;
if(chunks != null){
for (int i = 0; i < chunks; i++) {
File partFile = new File(UPLOAD_PATH,i + "_" + name);
if (!partFile.exists()) {
uploadDone = false;
}
}
}
/**
* 合并文件
*/
if(uploadDone && chunk != null && chunks != null && name != null && chunk == chunks - 1){
File tempFile = new File(UPLOAD_PATH,name);
while(!tempFile.exists()){
//为了防止分片文件没下载完成,此时需要让线程休眠100毫秒
Thread.sleep(100);
}
os = new BufferedOutputStream(new FileOutputStream(tempFile));
for(int i = 0 ;i < chunks; i++){
File file = new File(UPLOAD_PATH,i+"_"+name);
byte[] bytes = FileUtils.readFileToByteArray(file);
os.write(bytes);
os.flush();
file.delete();
}
os.flush();
}
response.getWriter().write("上传成功"+name);
} catch (Exception e) {
e.printStackTrace();
} finally {
try{
if(os != null){
os.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
3.2.1 核心API介绍------DiskFileItemFactory
作用:可以设置缓存大小以及临时文件保存位置。默认缓存大小是 10240(10k).临时文件默认存储在系统的临时文件目录下.(可以在环境变量中查看)
- new DiskFileItemFactory();缓存大小与临时文件存储位置使用默认的.
- DiskFileItemFactory(int sizeThreshold, File repository) sizeThreshold :缓存大小。repository:临时文件存储位置
- 注意,对于无参数构造,也可以设置缓存大小以及临时文件存储位置. setSizeThreshold(int sizeThreshold)。setRepository(File repository)
3.2.2 核心API介绍------ServletFileUpload
- ServletFileUpload upload=new ServletFileUpload(factory); 创建一个上传工具,指定使用缓存区与临时文件存储位置.
- List items=upload.parseRequest(request); 它是用于解析request对象,得到所有上传项.每一个FileItem就相当于一个上传项.
- boolean flag=upload.isMultipartContent(request);用于判断是否是上传.可以简单理解,就是判断encType="multipart/form-data";
- 设置上传文件大小void setFileSizeMax(long fileSizeMax) 设置单个文件上传大小 。void setSizeMax(long sizeMax) 设置总文件上传大小
- 解决上传文件中文名称乱码。setHeaderEncoding("utf-8");注意:如果使用reqeust.setCharacterEncoding("utf-8")也可以,但不建议使用。
3.2.3 实现步骤
- 创建DiskFileItemFactory对象,设置缓冲区大小和临时文件目录
- 使用DiskFileItemFactory 对象创建ServletFileUpload对象,并设置上传文件的大小限制。
- 调用ServletFileUpload.parseRequest方法解析request对象,得到一个保存了所有上传内容的List对象。
- 对list进行迭代,每迭代一个FileItem对象,调用其isFormField方法判断是否是上传文件。True 为普通表单字段,则调用getFieldName、getString方法得到字段名和字段值False 为上传文件,则调用getInputStream方法得到数据输入流,从而读取上传数据。编码实现文件上传
四:大文件下载至服务端
4.1 分片下载接口编写
java
/**
* 静态内部类
*/
static class FileInfo{
public long fileSize;
public String fileName;
public FileInfo(long fileSize, String fileName) {
this.fileSize = fileSize;
this.fileName = fileName;
}
}
java
@RequestMapping("/downloadFile")
public void downloadFile() throws Exception {
//获取文件大小和文件名称
FileInfo fileInfo = downloadFile( 0, 10, -1, null);
//总分片数量,取模以后加一,表示下载最后一个分片
long countChunks = fileInfo.fileSize / PER_PAGE;
//多线程分片下载
for (long i = 0; i <= countChunks; i++) {
//减一是防止和下次下载的第一个字节重复
DownloadThread downloadThread =
new DownloadThread(i * PER_PAGE, (i + 1) * PER_PAGE - 1, i, fileInfo.fileName);
executorService.submit(downloadThread);
}
}
4.2 编写多线程类
java
/**
* 下载
*/
class DownloadThread implements Runnable{
long start;
long end;
long page;
String fileName;
public DownloadThread(long start, long end, long page, String fileName) {
this.start = start;
this.end = end;
this.page = page;
this.fileName = fileName;
}
@Override
public void run() {
try {
downloadFile(start, end, page, fileName);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.3 编写download的核心方法
java
/**
* 大文件下载至客户端
* @param request
* @param response
*/
@RequestMapping("/download")
public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {
//设置响应的编码格式为utf8
response.setCharacterEncoding(UTF8);
File file = new File(DOWNLOAD_PATH);
//字节流,将文件写到Java程序中来,输入流
InputStream is = null;
//字节流,将Java程序写到文件中,输出流
OutputStream os = null;
try {
//获取文件长度,进行分片下载
long fileSize = file.length();
//定义文件名称,将文件名进行编码校验,防止文件名称乱码
String fileName = URLEncoder.encode(file.getName(), UTF8);
//设置头信息
//告诉前端需要下载文件
response.setContentType("application/x-download");
//弹出另存为的对话框
response.addHeader("Content-Disposition","attachment;filename=" + fileName);
//告诉前端是否支持分片下载
response.setHeader("Accept-Range","bytes");
//将文件的大小返回给前端
response.setHeader("fileSize",String.valueOf(fileSize));
//响应文件名称
response.setHeader("fileName",fileName);
/**
* 记录文件读取的位置
* start:读取的起始位置
* end:读取的结束位置
* sum:已经读取文件的大小
*/
long start = 0 , end = fileSize - 1 , sum = 0;
if(request.getHeader("Range") != null){
//设置响应码为206,表示分片下载
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
/**
* Range:bytes=0-1024 ,表示传输的是从开头到第1024字节的内容;
* Range:bytes=1025-2048 ,表示传输的是从第1025到2048字节范围的内容;
* Range:bytes=-2000 ,表示传输的是最后2000字节的内容;
* Range:bytes=1024- ,表示传输的是从第1024字节开始到文件结束部分的内容;
* Range:bytes=0-0,-1 表示传输的是第一个和最后一个字节 ;
* Range:bytes=1024-2048,2049-3096,3097-4096 ,表示传输的是多个字节范围。
*/
String numRange = request.getHeader("Range").replaceAll("bytes=", "");
String[] strRange = numRange.split("-");
//判断strRange的长度
if(strRange.length == 2){
//获取文件的起始位置和结束位置
//trim,去掉字符串开头和结尾的空格
start = Long.parseLong(strRange[0].trim());
end = Long.parseLong(strRange[1].trim());
if( end > fileSize - 1){
end = fileSize - 1;
}
} else {
//若只给一个长度 开始位置一直到结束
start = Long.parseLong(numRange.replaceAll("-","").trim());
}
}
//需要读取文件的长度, 2-5 ,读取的长度为2,3,4,5为4
long rangeLength = end - start + 1;
//告诉客户端当前读取的是哪一段
String contentRange = "bytes " + start + "-" + end + "/" + fileSize;
//Content-Range为bytes 2048-4096/10240
response.setHeader("Content-Range",contentRange);
//当前分片读取得长度
response.setHeader("Content-Length",String.valueOf(rangeLength));
//将当前分片的内容保存到输出流中,返回给前端页面
os = new BufferedOutputStream(response.getOutputStream());
//将路径的文件保存到is中
is = new BufferedInputStream(new FileInputStream(file));
//skip,跳过并丢弃输入流的n个字节
is.skip(start);
byte[] bytes = new byte[1024];
int length = 0;
//当读取总量小于需要读取的大小时,则需要一直读取
while( sum < rangeLength ){
length = is.read(bytes,0,((rangeLength-sum) <= bytes.length ? ((int)(rangeLength-sum)) : bytes.length));
sum = sum + length;
os.write(bytes,0,length);
}
log.info("下载完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
if(is != null){
is.close();
}
if(os != null){
os.close();
}
}
}
4.4 分片下载方法
java
/**
* 下载
* @param start 开始下载的位置
* @param end 结束下载的位置
* @param page 第几个分片
* @param fileName 文件名称
* @return FileInfo
* @throws Exception
*/
public FileInfo downloadFile(long start, long end , long page , String fileName) throws Exception {
File file = new File(DOWN_PATH,page + "_" + fileName);
/**
* 1.判断文件存不存在
* 2.当page != -1时判断是否为探测下载
* 3.对分片下载的文件大小进行校验,防止文件只下载一半,程序断了
*/
if(file.exists() && page != -1 && file.length() == PER_PAGE){
return null;
}
String url = "http://127.0.0.1:8989/breakPoint/download";
HttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Range","bytes=" + start + "-" + end);
HttpResponse response = client.execute(httpGet);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
//当前分片的分片大小
String fileSize = response.getFirstHeader("fileSize").getValue();
fileName = URLDecoder.decode(response.getFirstHeader("fileName").getValue(),UTF8);
FileOutputStream fis = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int ch = 0;
while((ch = is.read(buffer)) != -1){
fis.write(buffer,0,ch);
}
is.close();
fis.flush();
fis.close();
//判断是否为最后一个分片,最后一个分片
if(end - Long.parseLong(fileSize) >= 0){
mergeFile(fileName,page);
}
return new FileInfo(Long.parseLong(fileSize), fileName);
}
4.5 合并文件方法
java
/**
* 合并文件
* @param fileName 文件名称
* @param page 当前是第几个分片
* @throws Exception
*/
private void mergeFile(String fileName, long page) throws Exception {
File tempFile = new File(DOWN_PATH , fileName);
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(tempFile));
for(int i = 0; i <= page; i++){
File file = new File(DOWN_PATH,i + "_" + fileName);
while(!file.exists() || (i != page && file.length() < PER_PAGE)){
//为了防止分片文件没下载完成,此时需要让线程休眠100毫秒
Thread.sleep(100);
}
byte[] bytes = FileUtils.readFileToByteArray(file);
os.write(bytes);
os.flush();
file.delete();
}
//删除探测文件
File file = new File( DOWN_PATH ,-1 + "_null");
file.delete();
os.flush();
os.close();
log.info("合并完成");
//文件子节计算导致文件不完整
//流未关闭
}
4.6 完整代码奉上
java
package com.sysg.file.controller;
import com.sysg.file.service.FileUploadDownService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author ikun
*/
@Slf4j
@RestController
@RequestMapping("/breakPoint")
public class FileUploadDownController {
/**
* 文件上传的路径
*/
public static final String UPLOAD_PATH = "D:\\fileItem";
/**
* 需要下载文件的路径
*/
public static final String DOWNLOAD_PATH = "D:\\video.mp4";
/**
* 分片存储的目录
*/
private final static String DOWN_PATH = "D:\\test";
/**
* 每个分片的大小-50M
*/
public static final long PER_PAGE = 1024L * 1024L * 50L;
/**
* 线程池
*/
public static final ExecutorService executorService = Executors.newFixedThreadPool(10);
/**
* 编码格式
*/
public static final String UTF8 = "UTF-8";
@Autowired
private FileUploadDownService fileUploadDownService;
@RequestMapping("/getUploadHtml")
public String getBreakPointHtml(){
return "breakPoint/upload";
}
/**
* 断点续传
* 会将文件进行分片,每次只调用后台接口上传一个分片
* @param request
* @param response
* @throws IOException
*/
@RequestMapping("/upload")
public void upload( HttpServletRequest request , HttpServletResponse response ) {
//分片
response.setCharacterEncoding(UTF8);
Integer chunk = null;
Integer chunks = null;
String name = null;
BufferedOutputStream os = null;
try{
/**
* DiskFileItemFactory
* 作用:可以设置缓存大小以及临时文件保存位置.
* DiskFileItemFactory(int sizeThreshold, File repository)
* sizeThreshold :缓存大小,默认缓存大小是 10240(10k).
* repository:临时文件存储位置,临时文件默认存储在系统的临时文件目录下.(可以在环境变量中查看)
*/
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(1024);
factory.setRepository(new File(UPLOAD_PATH));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(5L * 1024L * 1024L * 1024L);
upload.setSizeMax(20L * 1024L * 1024L * 1024L);
List<FileItem> items = upload.parseRequest(request);
for(FileItem item : items){
/**
* boolean isFormField()。
* 如果是表单域,非文件域
* isFormField方法用来判断FileItem对象里面封装的数据是一个普通文本表单字段,还是一个文件表单字段。
* 如果是普通文本表单字段,返回一个true否则返回一个false。因此可以用该方法判断是否是普通表单域还是文件上传表单域。
*/
if(item.isFormField()){
if("chunk".equals(item.getFieldName())){
String value = item.getString(UTF8);
chunk = Integer.parseInt(value);
}
if("chunks".equals(item.getFieldName())){
String value = item.getString(UTF8);
chunks = Integer.parseInt(value);
}
if("name".equals(item.getFieldName())){
name = item.getString(UTF8);
}
} else {
String temFileName = name;
if(name != null){
if(chunk != null){
temFileName = chunk + "_" + name;
}
// 如果文件夹没有,则创建
File file = new File(UPLOAD_PATH);
if (!file.exists()) {
file.mkdirs();
}
// 保存文件
File chunkFile = new File(UPLOAD_PATH,temFileName);
if(chunkFile.exists()){
chunkFile.delete();
chunkFile = new File(UPLOAD_PATH,temFileName);
}
item.write(chunkFile);
}
}
}
/**
* 是否全部上传完成
* 所有分片都存在才说明整个文件上传完成
*/
boolean uploadDone = true;
if(chunks != null){
for (int i = 0; i < chunks; i++) {
File partFile = new File(UPLOAD_PATH,i + "_" + name);
if (!partFile.exists()) {
uploadDone = false;
}
}
}
/**
* 合并文件
*/
if(uploadDone && chunk != null && chunks != null && name != null && chunk == chunks - 1){
File tempFile = new File(UPLOAD_PATH,name);
while(!tempFile.exists()){
//为了防止分片文件没下载完成,此时需要让线程休眠100毫秒
Thread.sleep(100);
}
os = new BufferedOutputStream(new FileOutputStream(tempFile));
for(int i = 0 ;i < chunks; i++){
File file = new File(UPLOAD_PATH,i+"_"+name);
byte[] bytes = FileUtils.readFileToByteArray(file);
os.write(bytes);
os.flush();
file.delete();
}
os.flush();
}
response.getWriter().write("上传成功"+name);
} catch (Exception e) {
e.printStackTrace();
} finally {
try{
if(os != null){
os.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
/**
* 大文件下载至客户端
* @param request
* @param response
*/
@RequestMapping("/download")
public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {
//设置响应的编码格式为utf8
response.setCharacterEncoding(UTF8);
File file = new File(DOWNLOAD_PATH);
//字节流,将文件写到Java程序中来,输入流
InputStream is = null;
//字节流,将Java程序写到文件中,输出流
OutputStream os = null;
try {
//获取文件长度,进行分片下载
long fileSize = file.length();
//定义文件名称,将文件名进行编码校验,防止文件名称乱码
String fileName = URLEncoder.encode(file.getName(), UTF8);
//设置头信息
//告诉前端需要下载文件
response.setContentType("application/x-download");
//弹出另存为的对话框
response.addHeader("Content-Disposition","attachment;filename=" + fileName);
//告诉前端是否支持分片下载
response.setHeader("Accept-Range","bytes");
//将文件的大小返回给前端
response.setHeader("fileSize",String.valueOf(fileSize));
//响应文件名称
response.setHeader("fileName",fileName);
/**
* 记录文件读取的位置
* start:读取的起始位置
* end:读取的结束位置
* sum:已经读取文件的大小
*/
long start = 0 , end = fileSize - 1 , sum = 0;
if(request.getHeader("Range") != null){
//设置响应码为206,表示分片下载
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
/**
* Range:bytes=0-1024 ,表示传输的是从开头到第1024字节的内容;
* Range:bytes=1025-2048 ,表示传输的是从第1025到2048字节范围的内容;
* Range:bytes=-2000 ,表示传输的是最后2000字节的内容;
* Range:bytes=1024- ,表示传输的是从第1024字节开始到文件结束部分的内容;
* Range:bytes=0-0,-1 表示传输的是第一个和最后一个字节 ;
* Range:bytes=1024-2048,2049-3096,3097-4096 ,表示传输的是多个字节范围。
*/
String numRange = request.getHeader("Range").replaceAll("bytes=", "");
String[] strRange = numRange.split("-");
//判断strRange的长度
if(strRange.length == 2){
//获取文件的起始位置和结束位置
//trim,去掉字符串开头和结尾的空格
start = Long.parseLong(strRange[0].trim());
end = Long.parseLong(strRange[1].trim());
if( end > fileSize - 1){
end = fileSize - 1;
}
} else {
//若只给一个长度 开始位置一直到结束
start = Long.parseLong(numRange.replaceAll("-","").trim());
}
}
//需要读取文件的长度, 2-5 ,读取的长度为2,3,4,5为4
long rangeLength = end - start + 1;
//告诉客户端当前读取的是哪一段
String contentRange = "bytes " + start + "-" + end + "/" + fileSize;
//Content-Range为bytes 2048-4096/10240
response.setHeader("Content-Range",contentRange);
//当前分片读取得长度
response.setHeader("Content-Length",String.valueOf(rangeLength));
//将当前分片的内容保存到输出流中,返回给前端页面
os = new BufferedOutputStream(response.getOutputStream());
//将路径的文件保存到is中
is = new BufferedInputStream(new FileInputStream(file));
//skip,跳过并丢弃输入流的n个字节
is.skip(start);
byte[] bytes = new byte[1024];
int length = 0;
//当读取总量小于需要读取的大小时,则需要一直读取
while( sum < rangeLength ){
length = is.read(bytes,0,((rangeLength-sum) <= bytes.length ? ((int)(rangeLength-sum)) : bytes.length));
sum = sum + length;
os.write(bytes,0,length);
}
log.info("下载完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
if(is != null){
is.close();
}
if(os != null){
os.close();
}
}
}
@RequestMapping("/downloadFile")
public void downloadFile() throws Exception {
//获取文件大小和文件名称
FileInfo fileInfo = downloadFile( 0, 10, -1, null);
//总分片数量,取模以后加一,表示下载最后一个分片
long countChunks = fileInfo.fileSize / PER_PAGE;
//多线程分片下载
for (long i = 0; i <= countChunks; i++) {
//减一是防止和下次下载的第一个字节重复
DownloadThread downloadThread =
new DownloadThread(i * PER_PAGE, (i + 1) * PER_PAGE - 1, i, fileInfo.fileName);
executorService.submit(downloadThread);
}
}
/**
* 下载
* @param start 开始下载的位置
* @param end 结束下载的位置
* @param page 第几个分片
* @param fileName 文件名称
* @return FileInfo
* @throws Exception
*/
public FileInfo downloadFile(long start, long end , long page , String fileName) throws Exception {
File file = new File(DOWN_PATH,page + "_" + fileName);
/**
* 1.判断文件存不存在
* 2.当page != -1时判断是否为探测下载
* 3.对分片下载的文件大小进行校验,防止文件只下载一半,程序断了
*/
if(file.exists() && page != -1 && file.length() == PER_PAGE){
return null;
}
String url = "http://127.0.0.1:8989/breakPoint/download";
HttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Range","bytes=" + start + "-" + end);
HttpResponse response = client.execute(httpGet);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
//当前分片的分片大小
String fileSize = response.getFirstHeader("fileSize").getValue();
fileName = URLDecoder.decode(response.getFirstHeader("fileName").getValue(),UTF8);
FileOutputStream fis = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int ch = 0;
while((ch = is.read(buffer)) != -1){
fis.write(buffer,0,ch);
}
is.close();
fis.flush();
fis.close();
//判断是否为最后一个分片,最后一个分片
if(end - Long.parseLong(fileSize) >= 0){
mergeFile(fileName,page);
}
return new FileInfo(Long.parseLong(fileSize), fileName);
}
/**
* 合并文件
* @param fileName 文件名称
* @param page 当前是第几个分片
* @throws Exception
*/
private void mergeFile(String fileName, long page) throws Exception {
File tempFile = new File(DOWN_PATH , fileName);
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(tempFile));
for(int i = 0; i <= page; i++){
File file = new File(DOWN_PATH,i + "_" + fileName);
while(!file.exists() || (i != page && file.length() < PER_PAGE)){
//为了防止分片文件没下载完成,此时需要让线程休眠100毫秒
Thread.sleep(100);
}
byte[] bytes = FileUtils.readFileToByteArray(file);
os.write(bytes);
os.flush();
file.delete();
}
//删除探测文件
File file = new File( DOWN_PATH ,-1 + "_null");
file.delete();
os.flush();
os.close();
log.info("合并完成");
//文件子节计算导致文件不完整
//流未关闭
}
/**
* 下载
*/
class DownloadThread implements Runnable{
long start;
long end;
long page;
String fileName;
public DownloadThread(long start, long end, long page, String fileName) {
this.start = start;
this.end = end;
this.page = page;
this.fileName = fileName;
}
@Override
public void run() {
try {
downloadFile(start, end, page, fileName);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 静态内部类
*/
static class FileInfo{
public long fileSize;
public String fileName;
public FileInfo(long fileSize, String fileName) {
this.fileSize = fileSize;
this.fileName = fileName;
}
}
}