关键词:springboot,download,Range,Content-Range,Content-Length,http code 206 Partial Content
下载文件的一部分,我们在request header:Range 中指定要获取的文件的字节范围。要注意http response header:Content-Length 一定要与Range中所表示的获取bytes的范围长度相等。
如果Content-Length大于要Range所表示的bytes长度,那么客户端在发起请求后,下载完相应的Range范围的bytes后,连接不会自动关闭,直到连接超时,抛出异常。
如果Content-Length小于要Range所表示的bytes长度,那么发起请求后,只会获得Content-Length所指定的较小范围的bytes,导致获取的数据不完整。
controller:
@SneakyThrows
@GetMapping("/download")
public void download(HttpServletResponse response,
@RequestParam String versionNum,
@RequestHeader(value = HttpHeaders.RANGE, required = false) String range) {
if (StringUtils.isEmpty(versionNum)) {
throw new ServiceException("版本号不能为空");
}
// jar包的上一级目录
String canonicalPath = FileUtil.getCanonicalPath(new File(".."));
String filePath = canonicalPath + "/device-upgrade-pack-repo/" + versionNum + "/" + versionNum + ".tar";
long fileSize = FileUtil.file(filePath).length();
// 获得文件流
BufferedInputStream inputStream = FileUtil.getInputStream(filePath);
// offset表示从哪个字节开始读取,length表示读取多少个字节
long offset = 0;
long length;
// 如果range为空,那么就是读取整个文件
if (StringUtils.isEmpty(range)) {
length = fileSize;
} else {
// 根据range计算offset和length
Pair<Long, Long> pair = getOffset(range);
offset = pair.getLeft();
length = pair.getRight();
// length == -1 表示 end range 为最后一个字节
if (length == -1) {
length = fileSize - offset + 1;
}
// 跳过文件的offset个字节
inputStream.skip(offset);
}
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition",
"attachment;filename=" + versionNum + ".tar");
response.setHeader("Content-Length", String.valueOf(length));
response.setHeader("Content-Range", "bytes" + " " +
offset + "-" + (offset + length - 1) + "/" + fileSize);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
ServletOutputStream os = response.getOutputStream();
byte[] buf = new byte[1024];
int bytesRead;
// sum 是当前读取的字节数
int sum = 0;
while (sum < length && (bytesRead = inputStream.read(buf, 0, (length - sum) <= buf.length ? (int) (length - sum) : buf.length)) != -1) {
os.write(buf, 0, bytesRead);
sum += bytesRead;
}
inputStream.close();
os.close();
}
解析range:
/**
* left is offset, right is length
*
* @param range
* @return
*/
private Pair<Long, Long> getOffset(String range) {
if (!range.contains("bytes=") || !range.contains("-")) {
throw new ServiceException("range 格式错误");
}
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] split = StringUtils.split(range, "-");
if (range.startsWith("-")) {
return Pair.of(0L, Long.parseLong(split[0]) + 1);
} else if (range.endsWith("-")) {
return Pair.of(Long.parseLong(split[0]), -1L);
} else {
if (split.length != 2) {
throw new ServiceException("range 格式错误");
}
return Pair.of(Long.parseLong(split[0]), Long.parseLong(split[1]) - Long.parseLong(split[0]) + 1);
}
}
测试下载接口:
@Test
void downloadObject() {
OkHttpClient httpClient = HttpUtil.getHttpClient();
Request request = new Request.Builder()
.get()
.url("http://localhost:8081/device/version/download?versionNum=1.1.1.2023")
.header("Range", "bytes=0-10")
.build();
try (Response response = httpClient.newCall(request).execute()) {
InputStream inputStream = response.body().byteStream();
byte[] buf = new byte[5];
int readBytes;
while ((readBytes = inputStream.read(buf)) != -1) {
if (readBytes < buf.length) {
buf = Arrays.copyOf(buf, readBytes);
}
System.out.println(Arrays.toString(buf) + " " + readBytes);
}
inputStream.close();
response.close();
System.out.println("end");
} catch (IOException e) {
throw new ServiceException("请求异常");
}
}
输出结果:
[49, 46, 49, 46, 49] 5
[46, 50, 48, 50, 51] 5
[46] 1
end