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

相关推荐
liulilittle13 分钟前
为什么采用全局管理缓存及状态:麻将客户端状态管理
服务器·网络·游戏·客户端·异步·mahjong·麻将
酣大智27 分钟前
OSPF引入的直连路由在 ospf 路由表中看不到
网络·ospf
2601_9663771327 分钟前
等保2.0最新标准新增安全数据处理要求后怎么办?等保一体机数据安全模块来助力
运维·网络·安全·等保一体机
BullSmall31 分钟前
window系统上报Cannot assign requested address
网络·功能测试
杨云龙UP1 小时前
MySQL Host is blocked because of many connection errors 导致 JDBC 连接失败排查与解决
linux·运维·网络·数据库·sql·mysql·登录失败
执念WRD1 小时前
Fastjson 1.2.83 RCE 漏洞利用
网络·安全·网络安全
xixiaoyunya1 小时前
局域网多电脑文件互备:从传统痛点到自动化方案的技术路径
网络·安全
Lynne3092 小时前
为什么工业数据越来越需要在现场处理?边缘计算正在解决什么问题?
网络·数据分析·数据
m0_734571763 小时前
深入理解5g <十二> pucch
网络·5g
闲云野鹤在人间3 小时前
Docker入门|第3章 镜像详解
linux·网络·docker·容器·centos·云计算·php