1.原理
1).HTTP 请求头 Range
Range: bytes=start-end
Range: bytes=10- :第10个字节及最后个字节的数据
Range: bytes=40-100 :第40个字节到第100个字节之间的数据.
示例:
#表示从第0个字节开始下载
conn.addRequestProperty(“range”, “bytes=” + 0 + “-“);
注意:若支持range分段下载,服务端需返回206状态码。
nginx默认支持range分段下载。
tomcat默认也支持range分段下载
示例响应头:
{
“1”: [“HTTP/1.1 206 Partial Content”],
“ETag”: [“W/\”174093750-1525092037000\”“],
“Date”: [“Mon, 30 Apr 2018 12:42:06 GMT”],
“Content-Length”: [“174093750”],
“Last-Modified”: [“Mon, 30 Apr 2018 12:40:37 GMT”],
“Set-Cookie”: [“JSESSIONID=8A5AF04A71028DD2F8742CACA8830995; Path=/; HttpOnly”],
“Accept-Ranges”: [“bytes”],
“Server”: [“Apache-Coyote/1.1”],
“Content-Range”: [“bytes 0-174093749/174093750”]
}
2).响应头
Content-Range
Content-Range: bytes 0-10/3103
这个表示,服务器响应了前(0-10)个字节的数据,该资源一共有(3103)个字节大小。
Content-Type
Content-Type: image/png
表示这个资源的类型
Content-Length
Content-Length: 11
表示这次服务器响应了11个字节的数据(0-10)
Last-Modified
Last-Modified: Tue, 30 Jun 2015 03:12:48 GMT
表示资源最近修改的时间(分段下载时要注意这个东西,因为如果修改了,分段下载可能就要重新下载了)
ETag
ETag: W/"3103-1435633968000"
这个响应头表示资源版本的标识符,通常是消息摘要(类似MD5一样)(分段下载时要注意这个东西,或者缓存控制也要注意这个东西)
注意,每种服务器对生成ETag的算法不同,这个要特别注意 如果使用分布式缓存,要特别要保证每台服务器生成的ETag算法是一致的.
缓存的过期,要同时结合(ETag + Last-Modified)这两个响应头来判断.
强ETag
只要实体发生任何改变,都会改变ETag值.如:
ETag: "1234234234"
弱ETag
它在前面会有个 W/ ,如:
ETag: W/"12342423"
注意:根据HTTP规范,HTTP的消息头部的字段名,是不区分大小写的.
2.服务器端代码
/**
* 下载
* @param request
* @param response
* @param model
* @return
* @throws IOException
*/
@RequestMapping(value = "/api/update", method = RequestMethod.GET)
public ResponseEntity<String> update(HttpServletRequest request, HttpServletResponse response, ModelMap model)
throws IOException {
InputStream inputStream = null;
ServletOutputStream out = null;
String odexName = "file.apk";
try {
File file = new File("/data/apk/" + odexName);
int fSize = Integer.parseInt(String.valueOf(file.length()));
response.setCharacterEncoding("utf-8");
response.setContentType("application/x-download");
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Length", String.valueOf(fSize));
response.setHeader("Content-Disposition", "attachment;fileName=" + odexName);
inputStream = new FileInputStream("/data/apk/" + odexName);
long pos = 0;
if (null != request.getHeader("Range")) {
// 断点续传
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try {
pos = Long.parseLong(request.getHeader("Range").replaceAll("bytes=", "").replaceAll("-", ""));
} catch (NumberFormatException e) {
pos = 0;
}
}
out = response.getOutputStream();
response.setHeader("Content-Range", new StringBuffer("bytes ").append(pos + "").append("-")
.append((fSize - 1) + "").append("/").append(fSize + "").toString());
inputStream.skip(pos);
byte[] buffer = new byte[1024 * 10];
int length = 0;
while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, length);
Thread.sleep(100);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != out)
out.flush();
if (null != out)
out.close();
if (null != inputStream)
inputStream.close();
} catch (IOException e) {
}
}
return new ResponseEntity(null, HttpStatus.OK);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
3.客户端代码
/**
* 断点下载
*
* @throws InterruptedException
*/
@Test
public void testDebugDownload() throws IOException, InterruptedException {
// 创建URL对象
URL url = new URL("http://localhost:8080/file.apk");
// 使用url获取HttpURLConnection对象
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 客户端的请求方式
conn.setRequestMethod("GET");
// 已经下载的字节数
long alreadySize = 0;
// 将文件写到download/file.apk中
File file = new File("/download/file.apk");
// 如果存在,说明原来下载过,不过可能没有下载完
if (file.exists()) {
// 如果文件存在,就获取当前文件的大小
alreadySize = file.length();
}
/**
* Range头域可以请求实体的一个或者多个子范围。 例如: 表示头500个字节:bytes=0-499
* 表示第二个500字节:bytes=500-999 表示最后500个字节:bytes=-500
* 表示500字节以后的范围:bytes=500- 第一个和最后一个字节:bytes=0-0,-1
* 同时指定几个范围:bytes=500-600,601-999
* 但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200
* (OK)。
*/
conn.addRequestProperty("range", "bytes=" + alreadySize + "-");
conn.connect();
// 206,一般表示断点续传
// 获取服务器回馈的状态码
int code = conn.getResponseCode();
// 如果响应成功,因为使用了range请求头,那么响应成功的状态码为206,而不是200
if (code == 206) {
// 获取未下载的文件的大小
// 本方法用来获取响应正文的大小,但因为设置了range请求头,那么这个方法返回的就是剩余的大小
long unfinishedSize = conn.getContentLength();
// 文件的大小
long size = alreadySize + unfinishedSize;
// 获取输入流
InputStream in = conn.getInputStream();
// 获取输出对象,参数一:目标文件,参数2表示在原来的文件中追加
OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true));
// 开始下载
byte[] buff = new byte[2048];
int len;
StringBuilder sb = new StringBuilder();
while ((len = in.read(buff)) != -1) {
out.write(buff, 0, len);
// 将下载的累加到alreadSize中
alreadySize += len;
// 下载进度
System.out.printf("%.2f%%\n", alreadySize * 1.0 / size * 100);
// 由于文件大小可以看得到,那么我们这里使用阻塞
//Thread.sleep(2);
//break;
}
out.close();
System.out.println("下载完成!!!");
} else {
System.out.println("下载失败!!!");
}
// 断开连接
conn.disconnect();
}