HTTP-大文件传输处理

数据压缩

浏览器在发送请求时都会带着"AcceptEncoding"头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进"Content-Encoding"响应头里,再把原数据压缩后发给浏览器。

分块传输

这种"化整为零"的思路在 HTTP 协议里就是"chunked"分块传输编码,在响应报文里用头字段"Transfer-Encoding: chunked"来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。

"Transfer-Encoding: chunked"和"Content-Length"是不能同时存在的,因为分快传输长度是未知的。

案例-1

java 复制代码
@RestController
public class TestController {
     @GetMapping("/test")
    void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
         String s = "hello world";

         byte[] bytes = s.getBytes();
         ServletOutputStream outputStream = response.getOutputStream();
         try {
             int chunk = 3;
             int pos = 0;
             while (pos < bytes.length) {
                 int len = Math.min(chunk, bytes.length - pos);
                 outputStream.write(bytes, pos, len);
                 pos += len;
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
         outputStream.flush();
    }
}

我们可以看到上面的代码会tomcat会默认加上chunked,因为我们没有设置Content-Length,所以tomcat会认为是分块传输。如果我们设置了Content-Length 我们就会发现不再是chunked分块传输了,但是要注意长度需要和我们的真实数据一致否则会多数据或者丢数据。

java 复制代码
response.setContentLength(s.getBytes().length);

范围请求

响应头,告诉客户端支持范围请求:

java 复制代码
accept-ranges : bytes

还需要返回这两个信息:

java 复制代码
Content-Range: bytes 0-31/96
content-length : 54325

服务器可以发送"AcceptRanges: none",或者干脆不发送.

请求字段如下:

bash 复制代码
Range : bytes=76744601-76846440

服务器收到请求以后:

  • 第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求"200-300",这就是范围越界了。服务器就会返回状态码416,意思是"你的范围请求有误,我无法处理,请再检查一下"。
  • 第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码"206 Partial Content",和 200 的意思差不多,但表示 body 只是原数据的一部分。
java 复制代码
    @GetMapping("/file")
    void file(HttpServletRequest request, HttpServletResponse response) throws IOException {
         String s = "hello world! Please give me a lot of money!";
        response.setHeader("Accept-Ranges", "bytes");
        byte[] data = s.getBytes();
        response.setHeader("Content-Length", String.valueOf(data.length));
        int len = data.length;
        String header = request.getHeader("Range");
        if (header == null) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentLength(len);
            response.getOutputStream().write(data);
        }
        if (!header.startsWith("bytes=")) {
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
        }

        // 去掉 "bytes="
        header = header.substring(6);
        String[] parts = header.split("-");
        int start = 0;
        int end = len - 1;

        try {
            if (!parts[0].isEmpty()) {
                start = Integer.parseInt(parts[0]);
            }

            if (parts.length > 1 && !parts[1].isEmpty()) {
                end = Integer.parseInt(parts[1]);
            }
        } catch (NumberFormatException e) {
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
        }
        if (start > end || start < 0 || end >= len) {
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return;
        }
        end = Math.min(end, len - 1);
        int contentLength = end - start + 1;
        // 设置部分内容状态码 206
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

        // Content-Range: bytes 0-31/96
        response.setHeader("Content-Range",
                String.format("bytes %d-%d/%d", start, end,len));

        // 正确的 Content-Length
        response.setHeader("Content-Length", String.valueOf(contentLength));

        // 返回对应字节
        ServletOutputStream out = response.getOutputStream();
        out.write(data, start, contentLength);
        out.flush();
    }
}

使用postMan 发送请求:

收到响应:

多段数据

请求头如下:

java 复制代码
Range : bytes=0-9, 20-29

"multipart/byteranges",表示报文的 body 是由多段字节序列组成的,并且还要用一个参数"boundary=xxx"给出段之间的分隔标记。

java 复制代码
@GetMapping("/download")
    public void download(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {

        String s = "hello world! Please give me a lot of money!";
        byte[] data = s.getBytes();
        int dataLen = data.length;

        String rangeHeader = request.getHeader("Range");
        if (rangeHeader == null || !rangeHeader.startsWith("bytes=")) {
            // 没有 Range → 全部文件
            response.setHeader("Content-Length", String.valueOf(dataLen));
            response.setContentLength(dataLen);
            response.getOutputStream().write(data);
            return;
        }

        // 解析多个 Range 范围
        String rangesPart = rangeHeader.substring("bytes=".length());
        String[] rangeStrings = rangesPart.split(",");

        // 多段
        if (rangeStrings.length > 1) {
            String boundary = "MY_BOUNDARY";
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            response.setContentType("multipart/byteranges; boundary=" + boundary);
            ServletOutputStream out = response.getOutputStream();
            for (String r : rangeStrings) {
                long[] range = parseRange(r, dataLen);
                long start = range[0];
                long end = range[1];
                long length = end - start + 1;
                // 头
                out.println("--" + boundary);
                out.println("Content-Type: application/octet-stream");
                out.println("Content-Range: bytes " + start + "-" + end + "/" + dataLen);
                out.println();
                // body 数据
                out.write(data, (int) start, (int) length);
                out.println();
            }
            out.println("--" + boundary + "--");
            out.flush();
            return;
        }

        // 单段 Range
        long[] range = parseRange(rangeStrings[0], dataLen);
        long start = range[0];
        long end = range[1];
        long length = end - start + 1;

        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + dataLen);
        response.setContentLength((int) length);

        response.getOutputStream().write(data, (int) start, (int) length);
    }
    private long[] parseRange(String range, long totalLen) {
        range = range.trim();
        String[] parts = range.split("-");
        long start = Long.parseLong(parts[0]);
        long end = parts[1].isEmpty() ? (totalLen - 1) : Long.parseLong(parts[1]);
        return new long[]{start, end};
    }

请求:

响应:

参考资料:极客时间透视HTTP

相关推荐
لا معنى له2 小时前
残差网络论文学习笔记:Deep Residual Learning for Image Recognition全文翻译
网络·人工智能·笔记·深度学习·学习·机器学习
爬山算法2 小时前
Redis(153)Redis的网络使用如何监控?
网络·redis·bootstrap
极地星光3 小时前
C++链式调用设计:打造优雅流式API
服务器·网络·c++
q***48414 小时前
Nginx中$http_host、$host、$proxy_host的区别
运维·nginx·http
橘子真甜~4 小时前
C/C++ Linux网络编程8 - epoll + ET Reactor TCP服务器
linux·服务器·网络
贝塔实验室4 小时前
红外编解码彻底解析
网络·嵌入式硬件·信息与通信·信号处理·代码规范·基带工程·精益工程
就叫飞六吧4 小时前
“电子公章”:U盾(U-Key)实现身份认证、财务支付思路
网络·笔记
悠悠121384 小时前
NLB WebSocket 连接问题排查与解决方案
websocket·网络协议·php
wanderist.5 小时前
Linux使用经验——离线运行python脚本
linux·网络·python
biter00886 小时前
Ubuntu 22.04 有线网络时好时坏?最终解决方案
linux·网络·ubuntu