分段下载(也叫断点续传)的流程就三步:
先发送一个请求,知道总文件大小,然后根据总文件大小分成多段
每个请求只下载其中一段数据,可以看情况采用是并行还是串行的方式
全部下载完成后把所有小文件合并成一个文件
请求头和响应头
请求头
Range: bytes=start-end
限制头部 例如(bytes=884736-)
限制尾部 (bytes=-123456)
分段截取 (bytes=123456-884736) 获取第一个字节0-0
响应头
Content-Range:bytes 0-200/3000 表示服务器返回了0-200个字节的数据,总共3000字节的数据。
Accept-Ranges:bytes 当浏览器发现Accept-Ranges头时,可以尝试继续中断了的下载,而不是重新开始。
Content-Type:video/mp4 表示资源类型,这里是mp4格式的视频
Content-Length:200 表示服务器此次返回数据是多少字节,这里是200字节
Last-Modified:Fri , 12 May 2006 18:53:33 GMT 表示资源最近修改的时间,如果被修改了需要重新下载
ETag: 例如"2e681a-6-5d044840",表示资源版本的标示符。通常是消息摘要(类似MD5),缓存的过期需要结合 ETag 和 Last-Modified 共同决定。也可以不传。
代码实现
由于浏览器安全策略的限制,javascript程序不能自由地访问本地资源,一般网页通常也只是用此来分段下载视频在线播放,通常只在客户端会实现分段下载功能,所以这里只展示后端部分的代码
public void previewVideo(HttpServletRequest request, String path, HttpServletResponse response) { BucketAndUrl args = getBucketAndUrl(path); log.info("args:" + JSONUtil.toJsonStr(args)); String range = request.getHeader("Range"); log.info("current request rang:" + range); //获取文件信息 try { StatObjectResponse statObjectResponse = minioClient.statObject( StatObjectArgs.builder().bucket(args.getBucket()).object(args.getUrl()).build()); //开始下载位置 long startByte = 0; //结束下载位置 long endByte = statObjectResponse.size() - 1; log.info("文件开始位置:{},文件结束位置:{},文件总长度:{}", startByte, endByte, statObjectResponse.size()); //有range的话 if (StrUtil.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) { range = range.substring(range.lastIndexOf("=") + 1).trim(); String[] ranges = range.split("-"); try { //判断range的类型 if (ranges.length == 1) { //类型一:bytes=-2343 if (range.startsWith("-")) { endByte = Long.parseLong(ranges[0]); } //类型二:bytes=2343- else if (range.endsWith("-")) { startByte = Long.parseLong(ranges[0]); } } //类型三:bytes=22-2343 else if (ranges.length == 2) { startByte = Long.parseLong(ranges[0]); endByte = Long.parseLong(ranges[1]); } } catch (NumberFormatException e) { startByte = 0; endByte = statObjectResponse.size() - 1; log.error("Range Occur Error, Message:" + e.getLocalizedMessage()); } } //要下载的长度 long contentLength = endByte - startByte + 1; //文件类型 String contentType = request.getServletContext().getMimeType(args.getFileName()); //解决下载文件时文件名乱码问题 byte[] fileNameBytes = args.getFileName().getBytes(StandardCharsets.UTF_8); String filename = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1); //各种响应头设置 //支持断点续传,获取部分字节内容: response.setHeader("Accept-Ranges", "bytes"); //http状态码要为206:表示获取部分内容 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setContentType(contentType); response.setHeader("Last-Modified", statObjectResponse.lastModified().toString()); //inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名 response.setHeader("Content-Disposition", "inline;filename=" + filename); response.setHeader("Content-Length", String.valueOf(contentLength)); //Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小] response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + statObjectResponse.size()); response.setHeader("ETag", "\"".concat(statObjectResponse.etag()).concat("\"")); GetObjectResponse stream = minioClient.getObject( GetObjectArgs.builder() .bucket(statObjectResponse.bucket()) .object(statObjectResponse.object()) .offset(startByte) .length(contentLength) .build()); BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream()); byte[] buffer = new byte[1024]; int len; while ((len = stream.read(buffer)) != -1) { os.write(buffer, 0, len); } os.flush(); os.close(); response.flushBuffer(); log.info("下载完毕"); } catch (Exception e) { log.error("分段预览异常", e); } }