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

本文介绍了如何通过前端请求头中的Range字段实现文件分段返回,后端根据请求范围读取并分段发送,减少手机端观看视频时的流量消耗。文章详细展示了后端Java代码示例和前端处理方式。
摘要由CSDN通过智能技术生成

情况说明

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

前端处理

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

后端分离实现文件切片上传返回上传url的实现过程如下: 1. 前端将大文件切成多个小文件,并将每个小文件按照一定的顺序进行上传,同时携带必要的参数,如文件名、文件大小、文件类型等。 2. 后端接收到每个小文件的上传请求,将其保存到临时文件夹中,并记录文件名、文件大小、文件类型等信息。 3. 当所有小文件上传完成后,后端将这些小文件按照一定的顺序合并成一个完整的大文件,并生成一个唯一的文件ID。 4. 后端文件ID和文件下载URL返回给前端,前端可以通过这个URL进行文件下载。 下面是Java代码实现: 前端代码: ```javascript // 定义一个方法来上传文件 function uploadFile(file) { // 每个分片的大小 var chunkSize = 10 * 1024 * 1024; // 10MB // 文件分片 var chunks = Math.ceil(file.size / chunkSize); // 当前分片 var currentChunk = 0; // 文件唯一标识 var fileId = Math.random().toString(36).substr(2); // 文件上传URL var uploadUrl = 'http://localhost:8080/upload'; // 开始上传 uploadNextChunk(); // 上传下一个分片 function uploadNextChunk() { var start = currentChunk * chunkSize; var end = Math.min(start + chunkSize, file.size); var formData = new FormData(); formData.append('fileId', fileId); formData.append('chunk', currentChunk); formData.append('chunks', chunks); formData.append('file', file.slice(start, end)); $.ajax({ url: uploadUrl, type: 'POST', data: formData, processData: false, contentType: false, success: function (response) { if (currentChunk < chunks - 1) { // 继续上传下一个分片 currentChunk++; uploadNextChunk(); } else { // 所有分片上传完成,合并文件 mergeFile(); } }, error: function (error) { console.error(error); } }); } // 合并文件 function mergeFile() { $.ajax({ url: 'http://localhost:8080/merge', type: 'POST', data: { fileId: fileId }, success: function (response) { // 文件上传成功,返回文件下载URL console.log(response); }, error: function (error) { console.error(error); } }); } } ``` 后端代码: ```java @RestController public class FileController { // 临时文件夹路径 private static final String TEMP_FOLDER = "/temp"; // 文件上传路径 private static final String UPLOAD_FOLDER = "/uploads"; // 文件分片大小 private static final int CHUNK_SIZE = 10 * 1024 * 1024; // 10MB // 上传分片接口 @PostMapping("/upload") public void upload(@RequestParam("fileId") String fileId, @RequestParam("chunk") int chunk, @RequestParam("chunks") int chunks, @RequestParam("file") MultipartFile file) throws IOException { // 检查临时文件夹是否存在 File tempFolder = new File(TEMP_FOLDER); if (!tempFolder.exists()) { tempFolder.mkdirs(); } // 保存分片文件到临时文件夹中 String tempFileName = fileId + "-" + chunk; File tempFile = new File(tempFolder, tempFileName); file.transferTo(tempFile); // 如果所有分片都上传完成,开始合并文件 if (chunk == chunks - 1) { mergeFile(fileId, chunks); } } // 合并文件接口 @PostMapping("/merge") public String merge(@RequestParam("fileId") String fileId) throws IOException { // 检查上传文件夹是否存在 File uploadFolder = new File(UPLOAD_FOLDER); if (!uploadFolder.exists()) { uploadFolder.mkdirs(); } // 获取所有分片文件,并按照文件名排序 File tempFolder = new File(TEMP_FOLDER); File[] files = tempFolder.listFiles((dir, name) -> name.startsWith(fileId)); Arrays.sort(files, Comparator.comparing(File::getName)); // 合并文件 String fileName = files[0].getName().split("-")[0]; File outputFile = new File(uploadFolder, fileName); try (FileOutputStream fos = new FileOutputStream(outputFile)) { for (File file : files) { Files.copy(file.toPath(), fos); } } // 删除临时文件 for (File file : files) { file.delete(); } // 返回文件下载URL String downloadUrl = "http://localhost:8080/download/" + fileName; return downloadUrl; } // 文件下载接口 @GetMapping("/download/{fileName}") public ResponseEntity<Resource> download(@PathVariable("fileName") String fileName) throws IOException { File file = new File(UPLOAD_FOLDER, fileName); Path path = file.toPath(); Resource resource = new UrlResource(path.toUri()); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\""); return ResponseEntity.ok() .headers(headers) .contentLength(file.length()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } // 合并文件 private void mergeFile(String fileId, int chunks) throws IOException { // 获取所有分片文件,并按照文件名排序 File tempFolder = new File(TEMP_FOLDER); File[] files = tempFolder.listFiles((dir, name) -> name.startsWith(fileId)); Arrays.sort(files, Comparator.comparing(File::getName)); // 如果分片数量不足,说明有分片上传失败,删除所有分片文件 if (files.length < chunks) { for (File file : files) { file.delete(); } return; } // 合并文件 String fileName = files[0].getName().split("-")[0]; File outputFile = new File(UPLOAD_FOLDER, fileName); try (FileOutputStream fos = new FileOutputStream(outputFile)) { for (File file : files) { Files.copy(file.toPath(), fos); } } // 删除临时文件 for (File file : files) { file.delete(); } } } ``` 以上代码实现了大文件切片上传,文件合并和文件下载功能。注意,为了节省服务器空间,我们需要定期删除一些过期的临时文件和上传文件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值