一:字节流和字符流详解
1.流的概念
流是一种抽象的概念,就好比“水流”,从一段流向另一端。在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
2.流的分类
按照传输单位可以分为:字节流和字符流
按照流向可以分为:输入流(如:键盘,鼠标),输出流(如:显示器,音箱)
输入流 :把数据从其他设备上读取到内存中的流。
输出流 :把数据从内存中写出到其他设备上的流。
3.字节流,字符流区别与使用场景
1)区别
字节流(InputStream和OutputStream): 它处理单元为1个字节(byte),操作字节和字节数组,存储的是二进制文件,如果是音频文件、图片、歌曲,就用字节流好点(1byte = 8位);
字符流(Reader和Writer): 它处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,如果是关系到中文(文本)的,用字符流好点(1Unicode = 2字节 = 16位);
2)使用场景
字节流:字节流可用于任何类型的对象,包括二进制对象。字节流提供了处理任何类型的IO操作的功能。例如音频文件、图片、歌曲等。但它不能直接处理Unicode字符,而字符流就可以。
字符流:将文件在记事本里面打开,如果打开后能看的懂的就是字符流,如果看不懂那就是字节流。
3)顶级父类
4)对比——总结对比字节流和字符流
- 字节流操作的基本单元是字节;字符流操作的基本单元为Unicode码元。
- 字节流在操作的时候本身不会用到缓冲区的,是与文件本身直接操作的;而字符流在操作的时候使用到缓冲区的。
- 所有文件的存储都是字节(byte)的存储,在磁盘上保留的是字节。
- 在使用字节流操作中,即使没有关闭资源(close方法),也能输出;而字符流不使用close方法的话,不会输出任何内容
二:断点续传
1.概念
从文件上次中断的地方开始重新下载或上传,当下载或上传文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会去重头下载,这样很浪费时间。所以断点续传的功能就应运而生了。要实现断点续传的功能,需要客户端记录下当前的下载或上传进度,并在需要续传的时候通知服务端本次需要下载或上传的内容片段。
2.详细讲解
从 HTTP1.1 协议开始就已经支出获取文件的部分内容,断点续传技术就是利用 HTTP1.1 协议的这个特点在Header 里添加两个参数来实现的。这两个参数分别是客户端请求时发送的 Range 和服务器返回信息时返回的 Content-Range – Range,Range 用于指定第一个字节和最后一个字节的位置,格式如下:Range:(unit=first byte pos)-[last byte pos]
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)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 符合以上两个条件即可判定位支持断点续传。
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.断点续传至服务器
/**
* 断点续传
* 会将文件进行分片,每次只调用后台接口上传一个分片
* @param request
* @param response
*/
@RequestMapping("/uploadBreakContinue")
@ResponseBody
public void uploadBreakContinue( HttpServletRequest request , HttpServletResponse response ) throws IOException {
//定义响应的编码为utf8
response.setCharacterEncoding(utf8);
//当前分片数
Integer chunk = null;
//总分片数
Integer chunks = null;
//当前文件名称
String name = null;
//临时文件的存储位置
String uploadPath = "F:\\fileItem";
//缓冲流,
BufferedOutputStream os = null;
try {
/**
* DiskFileItemFactory
* 作用:可以设置缓存大小以及临时文件保存位置.
* DiskFileItemFactory(int sizeThreshold, File repository)
* sizeThreshold :缓存大小,默认缓存大小是 10240(10k).
* repository:临时文件存储位置,临时文件默认存储在系统的临时文件目录下.(可以在环境变量中查看)
*/
DiskFileItemFactory factory = new DiskFileItemFactory();
//设置缓冲区
factory.setSizeThreshold(1024);
File file = new File(uploadPath);
//设置临时目录
factory.setRepository(file);
/**
* 创建一个上传工具,指定使用缓存区与临时文件存储位置,解析request
*/
ServletFileUpload fileUpload = new ServletFileUpload(factory);
//单个文件的大小
fileUpload.setFileSizeMax(51L*1024*1024*1024);
//设置文件的的总大小
fileUpload.setSizeMax(10L*1024*1024*1024);
//获取当前文件信息
List<FileItem> fileItems = fileUpload.parseRequest(request);
for (FileItem item : fileItems) {
//如果是表单域,非文件域
if(!item.isFormField()){
//当前分片
if( "chunk".equals(item.getFieldName())){
//保证中文不会乱码
chunk = Integer.parseInt(item.getString(utf8));
}
//总分片
if( "chunks".equals(item.getFieldName())){
//保证中文不会乱码
chunks = Integer.parseInt(item.getString(utf8));
}
//名称
if( "name".equals(item.getFieldName())){
//保证中文不会乱码
name = item.getString(utf8);
}
}
}
//取文件
for (FileItem item : fileItems) {
if(!item.isFormField()){
String temFileName = name;
//判断文件名称有没有获取到
if( name != null ){
//判当前分片有没有获取到
if( chunk != null){
temFileName = chunk + "_" + name;
}
File temFile = new File(uploadPath, temFileName);
//如果temFile存在就表明当前分片已经有内容,就不需要写了
if(!temFile.exists()){
//断点续传
item.write(temFile);
}
}
}
}
//文件合并,判断分片不能为空
if( chunk != null && chunk.intValue() == chunks.intValue() - 1){
File tempFile = new File(uploadPath,name);
os = new BufferedOutputStream(new FileOutputStream(tempFile));
for (int i = 0; i < chunks; i++) {
File chunkFile = new File(uploadPath,i+"_"+name);
//判断分片文件是否存在,直到获取到为止
while(!chunkFile.exists()){
Thread.sleep(100);
}
//转化为字节数组
byte[] bytes = FileUtils.readFileToByteArray(chunkFile);
os.write(bytes);
os.flush();
chunkFile.delete();
}
os.flush();
}
response.getWriter().write("长传成功"+name);
} catch (FileUploadException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if( os != null ) {
//关闭缓冲流
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1)核心API介绍——DiskFileItemFactory
作用:可以设置缓存大小以及临时文件保存位置。默认缓存大小是 10240(10k).临时文件默认存储在系统的临时文件目录下.(可以在环境变量中查看)
- new DiskFileItemFactory();缓存大小与临时文件存储位置使用默认的.
- DiskFileItemFactory(int sizeThreshold, File repository) sizeThreshold :缓存大小。repository:临时文件存储位置
- 注意,对于无参数构造,也可以设置缓存大小以及临时文件存储位置. setSizeThreshold(int sizeThreshold)。setRepository(File repository)
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)实现步骤
1、创建DiskFileItemFactory对象,设置缓冲区大小和临时文件目录
2、使用DiskFileItemFactory 对象创建ServletFileUpload对象,并设置上传文件的大小限制。
3、调用ServletFileUpload.parseRequest方法解析request对象,得到一个保存了所有上传内容的List对
象。
4、对list进行迭代,每迭代一个FileItem对象,调用其isFormField方法判断是否是上传文件
True 为普通表单字段,则调用getFieldName、getString方法得到字段名和字段值
False 为上传文件,则调用getInputStream方法得到数据输入流,从而读取上传数据。
编码实现文件上传
三:断点下载
1.大文件下载至服务端
/**
* 大文件下载至服务端
* @param request
* @param response
*/
@RequestMapping("/download/file")
public void downloadFile ( HttpServletRequest request , HttpServletResponse response ) throws Exception {
//设置响应的编码格式为utf8
response.setCharacterEncoding(utf8);
//需要下载文件的路径
String filePath = "E:\\idea\\idea\\ideaIU-2021.1.2";
File file = new File(filePath);
//字节流,将文件写到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( null != request.getHeader("Range")){
//设置响应码为206
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
//range: bytes=100-1000
String numRange = request.getHeader("Range").replaceAll("bytes=", "");
String[] strRange = numRange.split("-");
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 {
end = Long.parseLong(numRange.replaceAll("-","").trim());
}
}
}
//读取文件的多少
long rangeLength = end - start + 1;
//告诉客户端当前读取的是哪一段
String contentRange = new StringBuffer("bytes " ).append(start).append("-").append("end").append("/").append(fileSize).toString();
response.setHeader("Content-Range",contentRange);
response.setHeader("Content-Length",String.valueOf(rangeLength));
os = new BufferedOutputStream(response.getOutputStream());
is = new BufferedInputStream(new FileInputStream(file));
//从start的位置开始读取
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);
}
System.out.println("下载完成");
} finally {
//关闭流
if( is != null){
is.close();
}
if( os != null){
os.close();
}
}
}
2.大文件下载至客户端——通过多线程进行下载
package com.sysg.controller;
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.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.util.concurrent.*;
/**
* @author Administrator
*/
@RestController
public class DownloadClient {
//每片分片的大小
private final static long PER_PAGE = 1021L * 1024L * 50L;
//分片存储的临时目录
private final static String DOWN_PATH = "F:\\FILEiTEM";
/**
* 线程池,分片下载,提高效率
* newFixedThreadPool:固定大小的线程池
*/
private static final ExecutorService pool = Executors.newFixedThreadPool(10);
/**
* 文件大小,文件名称
* 试探性下载,获取变量
* 多线程分片下载
* 最后一个分片下载完,开始合并
*
* @return
*/
@RequestMapping("/downloadFile")
public String downloadFile() throws IOException{
FileInfo fileInfo = download(0, 10, -1, null);
if( fileInfo != null ){
long pages = fileInfo.fileSize/PER_PAGE;
for( int i = 0 ; i <= pages ; i++ ){
pool.submit(new DownLoad(i*PER_PAGE,(i+1)*PER_PAGE-1,i,fileInfo.fileName));
}
}
return "success";
}
/**
* 实现runnable接口,在此任务中实现多线程下载
*/
class DownLoad implements Runnable{
long start,end,page;
String fileName;
/**
* 构造器
* @param start
* @param end
* @param page
* @param fileName
*/
public DownLoad(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 {
download(start,end,page,fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
}
class FileInfo{
long fileSize;
String fileName;
/**
* 构造方法
* @param fileSize
* @param fileName
*/
public FileInfo(long fileSize, String fileName) {
this.fileSize = fileSize;
this.fileName = fileName;
}
}
/**
* 开始位置以及结束位置
* 结束位置-开始位置=分片大小
* @return
*/
private FileInfo download( long start , long end , long page ,String fileName) throws Exception {
//断点下载,如果存在就不需要下载了
File file = new File(DOWN_PATH,page+"-"+fileName);
/**
* 1.如果存在就不继续下载了
* 2.探测当前分片
* 3.判断当前分片是否等于定义的分片大小
*/
if(file.exists() && page != -1 && file.length() == PER_PAGE){
return null;
}
HttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/download");
httpGet.setHeader("Range","bytes="+start+"-"+end);
HttpResponse httpResponse = client.execute(httpGet);
String fileSize = httpResponse.getFirstHeader("fileSize").getValue();
fileName = httpResponse.getFirstHeader("fileName").getValue();
HttpEntity entity = httpResponse.getEntity();
InputStream is = entity.getContent();
//文件输出流
FileOutputStream fos = new FileOutputStream(file);
//定义缓冲区
byte[] bytes = new byte[1024];
int ch;
//不等于-1代表文件继续读取
while((ch = is.read(bytes)) != -1){
fos.write(bytes,0,ch);
}
is.close();
fos.close();
fos.flush();
//判断是否为最后一个分片,只有最后一个分片大小可能会大于文件大小
if( end - Long.valueOf(fileSize) > 0 ){
/**
* 合并分片
* fileName:文件名称
* page:当前分片数
*/
mergerFile(fileName,page);
}
return new FileInfo(Long.valueOf(fileSize),fileName);
}
/**
* 分片合并下载
* @param fileName
* @param page
* @throws IOException
*/
private void mergerFile(String fileName, Long page) throws Exception{
//最终文件
File file = new File(DOWN_PATH,fileName);
//文件输出流
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(file));
//寻找分片文件
for( int i = 0; i <= page; i++){
File temFile = new File(DOWN_PATH,i+"-"+fileName);
//判断文件是否下载完成,多线程下载是随机的
while( !file.exists() || (i != page && file.length() < PER_PAGE)){
Thread.sleep(100);
}
//将文件合并为byte数组
byte[] bytes = FileUtils.readFileToByteArray(temFile);
bos.write(bytes);
//删除临时文件
temFile.delete();
//删除探测文件
File exploreFile = new File(DOWN_PATH,-1 +"-null");
exploreFile.delete();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
bos.flush();
bos.close();
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/84092.html