HTTP-大文件传输处理
数据压缩
浏览器在发送请求时都会带着“AcceptEncoding”头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进“Content-Encoding”响应头里,再把原数据压缩后发给浏览器。
分块传输
这种“化整为零”的思路在 HTTP 协议里就是“chunked”分块传输编码,在响应报文里用头字段“Transfer-Encoding: chunked”来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。
“Transfer-Encoding: chunked”和“Content-Length”是不能同时存在的,因为分快传输长度是未知的。
案例-1
@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分块传输了,但是要注意长度需要和我们的真实数据一致否则会多数据或者丢数据。
response.setContentLength(s.getBytes().length);
范围请求
响应头,告诉客户端支持范围请求:
accept-ranges : bytes
还需要返回这两个信息:
Content-Range: bytes 0-31/96
content-length : 54325
服务器可以发送“AcceptRanges: none”,或者干脆不发送.
请求字段如下:
Range : bytes=76744601-76846440
服务器收到请求以后:
- 第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码416,意思是“你的范围请求有误,我无法处理,请再检查一下”。
- 第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“206 Partial Content”,和 200 的意思差不多,但表示 body 只是原数据的一部分。
@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;// 设置部分内容状态码 206response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);// Content-Range: bytes 0-31/96response.setHeader("Content-Range",String.format("bytes %d-%d/%d", start, end,len));// 正确的 Content-Lengthresponse.setHeader("Content-Length", String.valueOf(contentLength));// 返回对应字节ServletOutputStream out = response.getOutputStream();out.write(data, start, contentLength);out.flush();}
}
使用postMan 发送请求:

收到响应:

多段数据
请求头如下:
Range : bytes=0-9, 20-29
“multipart/byteranges”,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数“boundary=xxx”给出段之间的分隔标记。

@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;}// 单段 Rangelong[] 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
