SpringBoot SpringMVC文件下载,大文件断点续传,可以实时播放视频,拖动进度条

SpringBoot SpringMVC 文件下载,大文件断点续传,可以实时播放视频,拖动进度条

背景

最近业务包含一个视频播放,发现一个问题:我们的视频文件在自己的文件服务器,文件服务器开放的文件获取接口是下载文件,当有视频播放的时候,浏览器客户端会在视频文件下载完毕后开始播放。这当然是不可行的,如果文件比较大,客户端要下载完毕才能播放,用户体验极差,如何才能做到边下载边播放呢?

http 状态码 206 分析

做个实验,在tomcat路径下放个物理文件 xx.mp4,然后浏览器访问这个文件,发现可以实现边下载边播放(亲测可以,如果这样可以满足各位需求,看到这用tomcat去做就可以实现了)。
好奇tomcat怎么实现的,于是打开网页控制台,检查http请求,发现
SpringBoot SpringMVC文件下载,大文件断点续传,可以实时播放视频,拖动进度条_第1张图片
于是,满怀好奇心去查查206状态码是干什么的(以下为部分内容介绍)

服务器已经成功处理了部分 GET 请求。类似于 FlashGet 或者迅雷这类的 HTTP下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。
该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。
响应必须包含如下的头部域:
Content-Range 用以指示本次响应中返回的内容的范围;如果是 Content-Type 为 multipart/byteranges 的多段下载,则每一 multipart 段中都应包含 Content-Range 域用以指示本段的内容范围。假如响应中包含 Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。

看到这里就大概明白了,我们只需要做到返回206和处理Content-Range分段数据就行了

实现

为了方便大家使用,这里给封装成了函数,开箱即用

 /**
     * 文件支持分块下载和断点续传
     * @param filePath 文件完整路径
     * @param request 请求
     * @param response 响应
     */
    private void fileChunkDownload(String filePath, HttpServletRequest request, HttpServletResponse response) {
        String range = request.getHeader("Range");
        log.info("current request rang:" + range);
        File file = new File(filePath);
        //开始下载位置
        long startByte = 0;
        //结束下载位置
        long endByte = file.length() - 1;
        log.info("文件开始位置:{},文件结束位置:{},文件总长度:{}", startByte, endByte, file.length());

        //有range的话
        if (range != null && 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 = file.length() - 1;
                log.error("Range Occur Error,Message:{}",e.getLocalizedMessage());
            }
        }

        //要下载的长度
        long contentLength = endByte - startByte + 1;
        //文件名
        String fileName = file.getName();
        //文件类型
        String contentType = request.getServletContext().getMimeType(fileName);

        解决下载文件时文件名乱码问题
        byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
        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("Content-Type", contentType);
        //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 + "/" + file.length());

        BufferedOutputStream outputStream = null;
        RandomAccessFile randomAccessFile = null;
        //已传送数据大小
        long transmitted = 0;
        try {
            randomAccessFile = new RandomAccessFile(file, "r");
            outputStream = new BufferedOutputStream(response.getOutputStream());
            byte[] buff = new byte[4096];
            int len = 0;
            randomAccessFile.seek(startByte);
            //坑爹地方四:判断是否到了最后不足4096(buff的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //不然会会先读取randomAccessFile,造成后面读取位置出错,找了一天才发现问题所在
            while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
                outputStream.write(buff, 0, len);
                transmitted += len;
            }
            //处理不足buff.length部分
            if (transmitted < contentLength) {
                len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
                outputStream.write(buff, 0, len);
                transmitted += len;
            }

            outputStream.flush();
            response.flushBuffer();
            randomAccessFile.close();
            log.info("下载完毕:" + startByte + "-" + endByte + ":" + transmitted);
        } catch (ClientAbortException e) {
            log.warn("用户停止下载:" + startByte + "-" + endByte + ":" + transmitted);
            //捕获此异常表示拥护停止下载
        } catch (IOException e) {
            e.printStackTrace();
            log.error("用户下载IO异常,Message:{}", e.getLocalizedMessage());
        } finally {
            try {
                if (randomAccessFile != null) {
                    randomAccessFile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }///end try
    }

使用方式

fileChunkDownload(file.getPath(),request,response);

还能实现视频播放的前进后退哦

你可能感兴趣的:(#,SpringBoot,#,Java,java,web,http)