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

相关推荐
..过云雨11 分钟前
网络计算器实现 - 自定义套接字+序列化+守护进程
网络·网络协议·tcp/ip
hugerat1 小时前
在AI的帮助下,用C++构造微型http server
linux·c++·人工智能·http·嵌入式·嵌入式linux
三两肉1 小时前
HTTPS ECDHE 握手全解析
网络协议·https·github·rsa·echde
小宇的天下1 小时前
HBM(高带宽内存)深度解析:先进封装视角的技术指南
网络·人工智能
txinyu的博客2 小时前
HTTP服务实现用户级窗口限流
开发语言·c++·分布式·网络协议·http
ha20428941942 小时前
Linux操作系统学习记录之----自定义协议(网络计算器)
linux·网络·学习
糖~醋排骨2 小时前
DHCP服务的搭建
linux·服务器·网络
huohaiyu2 小时前
网络中的一些基本概念
运维·服务器·网络
llddycidy2 小时前
峰值需求预测中的机器学习:基础、趋势和见解(最新文献)
网络·人工智能·深度学习
小林一直冲2 小时前
华为设备配置与命令
网络