断点续传主要利用的就是HTTP1.1协议,主要是利用其中的Http头Range和Content-Range。
断点续传也就是从下载断开的哪里,重新接着下载,直到下载完整/可用。如果要使用这种断点续传,4个HTTP头不可少的,分别是Range头、Content-Range头、Accept-Ranges头、Content-Length头。这里我讲的是服务端,其中要用Range头是因为它是客户端发过来的信息。服务端是响应,而客户端(浏览器)是请求。
Range头必须要了解它,否则没法解析。请求中会带过来的断点信息,一般三种格式。
Range : bytes=50- //意思是从第50个字节开始到最后一个字节
Range : bytes=-70 //意思是最后的70个字节
Range : bytes=50-100 //意思是从第50字节到100字节 ,多线程分段加速下载可以使用
读取客户端发来的Range头解析为:
假设文件总大小为130字节。
第一种Range 50-130
第二种Range ( 130 - 70 )-130
第三种Range 50-100
还有一点要晓得的就是返回的HTTP状态码200、206、416这些意义。200是OK(一切正常),206是Partial Content(服务器已经成功处理了部分内容),416 Requested Range Not Satisfiable(对方(客户端)发来的Range 请求头不合理)。
请求的流程
- 客户端发起下载请求
- 服务端返回200 ,并且返回响应头中包含,("Accept-Ranges", "bytes"),这就是告诉了浏览器,我支持分段下载。
- 客户端开始接受数据 ,网络出现问题,或者用户手动暂停 ,客户端停止接受数据 ,客户端都没说再见就与服务端断开了
- 用户网络恢复或者点击了重新开始下载 ,客户端再次与服务端连接上,这时候因为浏览器知道服务器支持断点下载,所以会将Range请求头给服务端
- 这时服务端返回是206 , 服务端从断开的数据那继续发送,并且会发送响应头:Content-Range给客户端 ,客户端接收数据,直到完成。
在服务端返回206的前面,客户端假如发送了些不合理的Range请求头,服务端就不是返回206而是416。就是结尾字节大于开始字节或者是结尾字节是0什么的,这必定是416的。
单线程通常就是这样,那么我们的客户端是多线程呢,那么我们必定也是多线程。客户端会一次性发来多个请求,来贪婪的快速地下载完成文件。链接别太多就行了。会爆?
GET /123.zip HTTP/1.1 // 客户端发来请求了。
那我们告诉它。
HTTP/1.1 200 OK
Accept-Ranges : bytes //告诉客户端,我们是支持断点传输的,你知道了吗?
Content-Length : 1900 //文件总大小
Content-Type : image/jpeg //文件类型
body-data
好了,就这样发送去了,传输过程中出现了问题,连接断开了。
客户端又发来请求这回有点意思。
GET /123.zip HTTP/1.1
Range:bytes=580-
大家看到没,会多了怎么一行,我们解析为从580字节开始到1900字节,是要部分内容耶,那么返回什么呢。没错206啊。
HTTP/1.1 206 Partial Content
Accept-Ranges : bytes
Content-Type : image/jpeg //文件类型
Content-Length : (1900 - 580) //长度则不是总长度了,而580到1900共有多少字节。
Content-Range :bytes 580-(1900-1 ) / 1900 //,为什么结束字节要减1呢。这是因为发来的Range请求头文件下标是0开始。
重点来了,假设我们用Java的RandomAccessFile类读取,首先肯定是skipBytes(580 byte),然后循环读取,读到结束字节=1900。
代码实现
SpringMVC - Controller
@RequestMapping(value = "/download/{fileName}", method = RequestMethod.GET)
public void download(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
try{
File file = new File(fileName);
String headerInfo = request.getHeader("Range");
if (headerInfo != null) {
response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
}
// 表示下载范围的pojo
ResponseContentRange range = getRange(file.length(), headerInfo);
String fileName = file.getName();
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
if (request.getHeader(HttpHeaders.USER_AGENT).contains("MSIE")) {
fileName = URLEncoder.encode(fileName, "UTF-8");
} else {
fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
}
response.setContentType(MediaType.valueOf(FileUtil.getFileMediaType(file)).toString());
response.setContentLengthLong(file.length());
response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setHeader("Content-Range", "bytes " + range.getStartIndex() + "-" + (range.getStartIndex()
+ range.getContentSize() - 1) + "/" + file.length());
byte[] buffer = new byte[8129];
int n;
int writeCount = 0;
OutputStream outputStream = response.getOutputStream();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
randomAccessFile.skipBytes((int) range.getStartIndex());
InputStream in = getInputStream(randomAccessFile);
while ((n = in.read(buffer)) != -1 && writeCount < range.getContentSize()) {
outputStream.write(buffer, 0, n);
writeCount += n;
}
} catch (IOException ignore) {
}
}
/**
* 获取不同类型的输入流,实现限速的目的
*
* @param randomAccessFile
* @return
*/
private InputStream getInputStream(RandomAccessFile randomAccessFile) {
if (enableRateLimit) {
BandwidthLimiter bandwidthLimiter = new BandwidthLimiter(maxSpeed);
return new LimitRandomAccessStream(randomAccessFile, bandwidthLimiter);
} else {
return new RandomAccessFileStream(randomAccessFile);
}
}
/**
* 根据给定的rangeInfo,解析出回复的内容的范围
*
* @param maxSize 范围的最大值
* @param rangeInfo rangeInfo
* @return
*/
private ResponseContentRange getRange(long maxSize, String rangeInfo) {
long startIndex = 0L, contentLength = maxSize;
if (rangeInfo != null && rangeInfo.trim().length() > 0) {
String rangBytes = rangeInfo.replaceAll("bytes=", "");
if (rangBytes.endsWith("-")) {
startIndex = Long.parseLong(rangBytes.substring(0, rangBytes.indexOf("-")));
contentLength = maxSize - startIndex;
} else if (rangBytes.startsWith("-")) {
startIndex = Long.parseLong(rangBytes.substring(rangBytes.indexOf("-") + 1));
contentLength = maxSize - startIndex;
} else {
String[] indexs = rangBytes.split("-");
startIndex = Long.parseLong(indexs[0]);
contentLength = Long.parseLong(indexs[1]) - startIndex + 1;
}
}
return new ResponseContentRange(startIndex, contentLength);
}
上面这个方式其实是存在缺陷的,首先第一个,上面这种方式在chrome浏览器下是不能正确的进行断点下载的(断点下载的时候会马上结束下载),因为上面的下载方式少了两个非常重要的header,ETag和Last-Modify。这两个header是用来检查资源是否被修改过,以此来判断之前的下载的部分文件是否有效。当然我们可以进行添加,但是这样代码量又增加了太多了。
经过对springMVC和tomcat的研究之后,发现了一个新的方式去进行文件下载,springMVC和tomcat中都有对静态资源的处理,我们可以直接利用,且也支持断点下载,也利用到了浏览器缓存机制,感觉很不错,减少了重复造轮子。
tomcat中的defaultServelt是支持静态资源的处理的,像我们的html,css等静态资源一般都是tomcat进行的处理,且能进行浏览器缓存控制,我们只需要在server.xml中配置资源的映射规则就可以了,例如像下面这样
<Context docBase="D:\" path="/base/image" reloadable="true"/>
doBase是资源所在路径,path是访问路径
我这里选择的是springMVC的资源映射,感觉更方便一点。
@RequestMapping(value = "/download/{fileName}", method = RequestMethod.GET)
public void download(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response, @SessionAttribute(SessionKeyConstant.LOGGED_ID_NAME) String ownerId) {
try {
// 必须加上Content-Disposition的头,因为springmvc的静态资源处理不会加这个头
// 浏览器就不知道这是个下载的文件
if (request.getHeader(HttpHeaders.USER_AGENT).contains("MSIE")) {
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
} else {
response.addHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
}
// 直接利用request进行重定向,然后利用springmvc的静态资源处理,就ok了
request.getRequestDispatcher("/upload/" + fileName).forward(request, response);
} catch (ServletException | IOException ignore) {
}
}
当然了,前往不要忘记了配置SpringMVC的静态资源处理的注解,这也跟tomcat是差不多的,这个就根据自己的需要进行灵活配置吧,在location中可以指定任何一个位置,不止局限于webapp路径下,当然如果你直接写路径就是相对的webapp路径,如果想要访问到任何一个路径,就可以使用file:/D:/sad/dsa这种方式,也就是URL协议
<mvc:resources mapping="/upload/**" location="${file.fileLocal}"/>