springMVC实现断点续传(你能找到的最好的方式)

断点续传主要利用的就是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 请求头不合理)。

请求的流程

  1. 客户端发起下载请求
  2. 服务端返回200  ,并且返回响应头中包含,("Accept-Ranges", "bytes"),这就是告诉了浏览器,我支持分段下载。
  3. 客户端开始接受数据  ,网络出现问题,或者用户手动暂停 ,客户端停止接受数据  ,客户端都没说再见就与服务端断开了
  4. 用户网络恢复或者点击了重新开始下载 ,客户端再次与服务端连接上,这时候因为浏览器知道服务器支持断点下载,所以会将Range请求头给服务端 
  5. 这时服务端返回是206 , 服务端从断开的数据那继续发送,并且会发送响应头:Content-Range给客户端 ,客户端接收数据,直到完成。

在服务端返回206的前面,客户端假如发送了些不合理的Range请求头,服务端就不是返回206而是416。就是结尾字节大于开始字节或者是结尾字节是0什么的,这必定是416的。

单线程通常就是这样,那么我们的客户端是多线程呢,那么我们必定也是多线程。客户端会一次性发来多个请求,来贪婪的快速地下载完成文件。链接别太多就行了。会爆?Http/1.1协议 <wbr>Content-Range头 <wbr>用于http断点续传

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}"/>

 

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值