从零手写实现 nginx-08-range 范围查询

前言

大家好,我是老马。很高兴遇到你。

我们希望实现最简单的 http 服务信息,可以处理静态文件。

如果你想知道 servlet 如何处理的,可以参考我的另一个项目:

手写从零实现简易版 tomcat minicat

手写 nginx 系列

如果你对 nginx 原理感兴趣,可以阅读:

从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?

从零手写实现 nginx-02-nginx 的核心能力

从零手写实现 nginx-03-nginx 基于 Netty 实现

从零手写实现 nginx-04-基于 netty http 出入参优化处理

从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)

从零手写实现 nginx-06-文件夹自动索引

从零手写实现 nginx-07-大文件下载

从零手写实现 nginx-08-范围查询

从零手写实现 nginx-09-文件压缩

从零手写实现 nginx-10-sendfile 零拷贝

从零手写实现 nginx-11-file+range 合并

从零手写实现 nginx-12-keep-alive 连接复用

从零手写实现 nginx-13-nginx.conf 配置文件介绍

从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?

从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?

从零手写实现 nginx-16-nginx 支持配置多个 server

什么是 http 范围查询?

HTTP范围请求(Range Requests)是一种让客户端可以请求资源(如文件)的一部分而不是全部的机制。

这在处理大文件时特别有用,例如,视频点播服务或大文件下载,用户可以请求文件的特定部分进行播放或下载。

范围请求通过在HTTP请求中添加Range头来实现。以下是Range头的一些关键点:

  1. 语法Range头的语法遵循以下格式:

    vbnet 复制代码
    Range: bytes=<start-byte>-<end-byte>

    其中<start-byte><end-byte>指定了请求的字节范围(包含)。如果<end-byte>被省略,服务器将从<start-byte>发送到文件的末尾。

  2. 例子

    • 请求文件的前8192字节:

      ini 复制代码
      Range: bytes=0-8191
    • 请求文件从第5120个字节开始到第10239个字节:

      ini 复制代码
      Range: bytes=5120-10239
    • 请求文件从第2048个字节开始到文件末尾:

      ini 复制代码
      Range: bytes=2048-
  3. 响应 : 如果服务器支持范围请求,并且请求的范围有效,服务器将返回状态码206 Partial Content,并将请求范围内的数据发送给客户端。

    服务器还需要在响应中包含Content-Range头,指示实际发送的数据范围。

  4. Content-RangeContent-Range头的格式如下:

    arduino 复制代码
    Content-Range: bytes <start-byte>-<end-byte>/<total-file-size>
    • <start-byte><end-byte>与请求中的范围对应。
    • <total-file-size>是资源的总大小。
  5. 不支持范围请求 : 如果服务器不支持范围请求,或者请求的范围无效(例如,开始字节大于文件大小),服务器将返回状态码200 OK,并发送资源的全部内容。

  6. 多范围请求 : HTTP协议也支持请求多个非连续的范围,但这需要特定的服务器支持。多范围请求的Range头会包含多个范围,用逗号分隔:

    ini 复制代码
    Range: bytes=500-600,601-700

    对于多范围请求,服务器可能返回多个部分,每个部分都有自己的Content-Range头,并且包装在multipart/byterangesContent-Type中。

  7. 用例: 范围请求常用于以下场景:

    • 恢复中断的下载。
    • 视频点播服务中的"快进"功能。
    • 大文件的增量更新或备份。
  8. 注意事项

    • 并非所有的服务器都支持范围请求,这取决于服务器的配置和能力。
    • 对于不支持范围请求的资源,客户端仍然可以使用分块下载技术来实现类似的功能。

范围请求是HTTP协议中一个强大且灵活的特性,它为客户端提供了对资源访问的细粒度控制。

netty 实现

流程

在Netty中实现HTTP范围请求(Range Requests),你需要处理HTTP请求,解析Range头,并根据请求的范围发送相应的响应。以下是实现这一功能的步骤:

  1. 解析HTTP请求 :首先,你需要解析客户端发送的HTTP请求,特别是Range请求头。

  2. 处理Range头 :根据Range头指定的范围,确定要发送的字节区间。

  3. 构造HTTP响应 :创建HTTP响应,如果范围有效,设置状态码为206 Partial Content,否则使用200 OK

  4. 设置Content-Range头 :在响应中添加Content-Range头,指示实际发送的数据范围。

  5. 发送数据:使用适当的方式发送请求范围内的数据。

  6. 结束响应 :发送结束标记,如LastHttpContent.EMPTY_LAST_CONTENT

核心实现

java 复制代码
    public void doDispatch(NginxRequestDispatchContext context) {
        final HttpRequest request = context.getRequest();
        final File file = context.getFile();
        final ChannelHandlerContext ctx = context.getCtx();

        // 解析Range头
        String rangeHeader = request.headers().get("Range");
        logger.info("[Nginx] fileRange start rangeHeader={}", rangeHeader);

        long fileLength = file.length(); // 假设file是你要发送的File对象
        long[] range = parseRange(rangeHeader, fileLength);
        long start = range[0];
        long end = range[1];

        // 构造HTTP响应
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
                start < 0 ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT);
        // 设置Content-Type
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentType(file));

        if (start >= 0) {
            // 设置Content-Range
            if (end < 0) {
                end = fileLength - 1;
            }
            response.headers().set(HttpHeaderNames.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileLength);

            // 设置Content-Length
            int contentLength = (int) (end - start + 1);
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentLength);

            // 发送响应头
            ctx.write(response);

            try (FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ)) {
                fileChannel.position(start); // 设置文件通道的起始位置

                ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);
                while (end >= start) {
                    // 读取文件到ByteBuffer
                    int bytesRead = fileChannel.read(buffer);
                    if (bytesRead == -1) { // 文件读取完毕
                        break;
                    }
                    buffer.flip(); // 切换到读模式
                    ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));
                    buffer.compact(); // 保留未读取的数据,并为下次读取腾出空间
                    start += bytesRead; // 更新下一个读取的起始位置
                }
                ctx.flush(); // 确保所有数据都被发送

                // 发送结束标记
                ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
                        .addListener(ChannelFutureListener.CLOSE); // 如果连接断开,则关闭
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
    }

范围处理如下:

java 复制代码
    protected long[] parseRange(String rangeHeader, long totalLength) {
        // 简单解析Range头,返回[start, end]
        // Range头格式为: "bytes=startIndex-endIndex"
        if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
            String range = rangeHeader.substring("bytes=".length());
            String[] parts = range.split("-");
            long start = parts[0].isEmpty() ? totalLength - 1 : Long.parseLong(parts[0]);
            long end = parts.length > 1 ? Long.parseLong(parts[1]) : totalLength - 1;
            return new long[]{start, end};
        }
        return new long[]{-1, -1}; // 表示无效的范围请求
    }

请求测试

yaml 复制代码
>curl -i -H "Range: bytes=0-" http://192.168.1.12:8080/mime/1.css
HTTP/1.1 206 Partial Content
content-type: text/css
content-range: bytes 0-198/199
content-length: 199

body {
       font-family: Arial, sans-serif;
       margin: 0;
       padding: 0;
   }

   h1 {
       color: #333333;
   }

   .container {
       width: 80%;
       margin: auto;
   }

>curl -i -H "Range: bytes=127-" http://192.168.1.12:8080/mime/1.css
HTTP/1.1 206 Partial Content
content-type: text/css
content-range: bytes 127-198/199
content-length: 72


   }

   .container {
       width: 80%;
       margin: auto;
   }

或者过长的内容:

go 复制代码
>curl -i -H "Range: bytes=255-" http://192.168.1.12:8080/mime/1.css
HTTP/1.1 206 Partial Content
content-type: text/css
content-range: bytes 255-198/199
curl: (8) Invalid Content-Length: value
相关推荐
艾迪的技术之路2 分钟前
redisson使用lock导致死锁问题
java·后端·面试
今天背单词了吗98020 分钟前
算法学习笔记:8.Bellman-Ford 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·开发语言·后端·算法·最短路径问题
天天摸鱼的java工程师23 分钟前
使用 Spring Boot 整合高德地图实现路线规划功能
java·后端
东阳马生架构38 分钟前
订单初版—2.生单链路中的技术问题说明文档
java
咖啡啡不加糖1 小时前
暴力破解漏洞与命令执行漏洞
java·后端·web安全
风象南1 小时前
SpringBoot敏感配置项加密与解密实战
java·spring boot·后端
DKPT1 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
Percep_gan1 小时前
idea的使用小技巧,个人向
java·ide·intellij-idea
缘来是庄1 小时前
设计模式之迭代器模式
java·设计模式·迭代器模式