java后端切片的方式返回文件二进制流的实现方式

情况说明

一般情况下,我们返回文件的方式都是前端调用接口,然后后端获取文件,转为二进制流,再返回给前端。
这样处理的方式会有问题,如果是手机端需要看视频,如果每次请求都是返回整个文件的流,那么消耗的流量会非常大,所以我们会采用分段返回的方式实现。(下图的截图来自于B站的请求接截图)

实现思路

前端会在请求头中告诉后端,当前请求的文件数据范围,用Range字段表示,具体的格式:
range
后端接收到请求后,会读取指定的文件,解析请求头中Range字段, 读取指定范围内的文件内容,通过流返回给前端,前端不断调用接口的方式,分段获取文件流,后端会在响应头中告知文件的总大小,当前获取流的区间范围,具体的格式如下:
resp

后端代码演示

我们会采用自定义的缓冲区实现,这样可以根据实际情况去更改缓冲区的大小。
注意一下Content-Type这个响应头,如果我们能够确认返回的视频格式是MP4,我们可以设置为video/mp4。如果不知道返回的是什么类型,可以用application/octet-stream,它代表通用的二进制流文件,不确定类型。
新建一个FileUtil工具类

public class FileUtil {

    /**
     * 缓冲区大小(字节)
     */
    static int bufferSize = 1024 * 8;

    /**
     * 根据前端的请求分段输出
     * @param fileName 文件名
     * @param range 分段范围
     * @param response http响应
     * @throws IOException
     */
    public static void rangeRead(String fileName, String range, HttpServletResponse response) throws IOException {
        System.out.println("-----分段读取范围:" + range);
        //读取类路径下的文件按
        ClassPathResource classPath = new ClassPathResource("file/" + fileName);
        File file = classPath.getFile();
        //获取文件总大小
        long total = file.length();
        //创建缓冲区
        byte[] buffer = new byte[bufferSize];
        //获取分段读取的区间
        String[] split = range.substring(6).split("-");
        long start = Long.parseLong(split[0]);
        //默认结束是文件总长度
        long end = total;
        if (split.length == 2) {
            //如果请求有结束长度,则用请求的长度
            end = Long.parseLong(split[1]);
        }
        //计算需要读取的长度
        long readLength = end - start +1;

        //设置响应头
        //206代表部分资源
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        response.addHeader("Content-Type", "application/octet-stream");
        response.addHeader("Content-Length", String.valueOf(readLength));
        response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + total);

        try (FileInputStream input = new FileInputStream(file)) {
            //跳过前面已经读取过的字节
            input.skip(start);
            //记录剩下的长度
            long remaining = readLength;
            while (remaining > 0) {
                //将文件写入缓冲池中
                int bytesRead = input.read(buffer, 0, (int) Math.min(bufferSize, remaining));
                if (bytesRead < 0) {
                    //如果写入的实际长度小于0,则退出循环
                    break;
                }
                //将缓冲池的数据写入输出流中
                response.getOutputStream().write(buffer, 0, bytesRead);
                remaining -= bytesRead;
            }
            //将输出流的数据强制写出
            response.getOutputStream().flush();
        }
        //最后关闭输出流
        response.getOutputStream().close();
    }

    /**
     * 处理整个资源请求
     * @param fileName
     * @param response
     * @throws IOException
     */
    public static void allRead(String fileName, HttpServletResponse response) throws IOException {
        ClassPathResource classPath = new ClassPathResource("file/" + fileName);
        File file = classPath.getFile();
        //获取文件总大小
        long total = file.length();
        //创建缓冲区
        byte[] buffer = new byte[bufferSize];
        //设置响应头
        response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        response.addHeader("Content-Type", "video/mp4");
        response.addHeader("Content-Length", String.valueOf(total));
        try (FileInputStream input = new FileInputStream(file)) {
            int bytesRead;
            while ((bytesRead = input.read(buffer)) != -1) {
                //将缓冲池的数据写入输出流中
                response.getOutputStream().write(buffer, 0, bytesRead);
            }
            //将输出流的数据强制写出
            response.getOutputStream().flush();
        }
        //最后关闭输出流
        response.getOutputStream().close();
    }
}

在controller中解析请求头的range字段, 确认是分段读取还是一次性返回:

    @GetMapping("/video")
    public void getVideo(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //判断读取的视频是分段还是整读
        // 检查客户端是否支持范围请求
        String range = request.getHeader("Range");
        if (range != null && range.startsWith("bytes=")) {
        	//读取指定范围的数据
            FileUtil.rangeRead("xxx.mp4", range, response);
        } else {
        	//一次性返回
            FileUtil.allRead("xxx.mp4", response);
        }
    }

前端处理

完整的视频流的话,直接请求接口,浏览器可以自动解析然后播放。但是切片的视频流,浏览器没有办法直接解析,所以还需要前端用其他组件支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值