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

相关推荐
Mr_Xuhhh3 小时前
应用层协议HTTP(1)
网络·网络协议·http
..空空的人3 小时前
C++基于websocket的多用户网页五子棋 --- 认识依赖库
网络·websocket·网络协议
心态特好3 小时前
详解:长连接/短连接/Cookie/Session/WebSocket
网络·websocket·网络协议
Orange_sparkle3 小时前
关于dify中http节点下载文件时,文件名不为原始文件名问题解决
人工智能·http·chatgpt·dify
遇见火星5 小时前
Linux 网络配置实战:RHEL/CentOS 7+ 永久静态路由配置与优先级调整全攻略
linux·网络·centos·静态路由·centos 7
Fr2ed0m6 小时前
Nginx防御HTTP Host头注入漏洞:实战配置漏洞修复教程
运维·nginx·http
陌路206 小时前
Linux33 网络编程-多线程TCP并发
网络·算法
AORO202511 小时前
智能三防手机哪款好?22000mAh+夜视+露营灯打造专业户外装备
服务器·网络·智能手机·电脑·1024程序员节
Hello.Reader11 小时前
Data Sink定义、参数与可落地示例
java·前端·网络