首页 > 系统相关 >从零手写实现 nginx-11-文件处理逻辑与 range 范围查询合并

从零手写实现 nginx-11-文件处理逻辑与 range 范围查询合并

时间:2024-06-08 22:44:18浏览次数:32  
标签:11 long response nginx range context 手写 final

前言

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

我们为 java 开发者实现了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 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

从零手写实现 nginx-17-nginx 默认配置优化

从零手写实现 nginx-18-nginx 请求头响应头的处理

背景

最初感觉范围处理和文件的处理不是相同的逻辑,所以做了拆分。

但是后来发现有很多公共的逻辑。

主要两种优化方式:

  1. 把范围+文件合并到同一个文件中处理。添加各种判断代码

  2. 采用模板方法,便于后续拓展修改。

这里主要尝试下第 2 种,便于后续的拓展。

代码的相似之处

首先,我们要找到二者的相同之处。

range 主要其实是开始位置和长度,和普通的处理存在差异。

基础文件实现

我们对常见的部分抽象出来,便于子类拓展

/**
 * 文件
 *
 * @since 0.10.0
 * @author 老马笑西风
 */
public class AbstractNginxRequestDispatchFile extends AbstractNginxRequestDispatch {

    /**
     * 获取长度
     * @param context 上下文
     * @return 结果
     */
    protected long getActualLength(final NginxRequestDispatchContext context) {
        final File targetFile = context.getFile();
        return targetFile.length();
    }

    /**
     * 获取开始位置
     * @param context 上下文
     * @return 结果
     */
    protected long getActualStart(final NginxRequestDispatchContext context) {
        return 0L;
    }

    protected void fillContext(final NginxRequestDispatchContext context) {
        long actualLength = getActualLength(context);
        long actualStart = getActualStart(context);

        context.setActualStart(actualStart);
        context.setActualFileLength(actualLength);
    }

    /**
     * 填充响应头
     * @param context 上下文
     * @param response 响应
     * @since 0.10.0
     */
    protected void fillRespHeaders(final NginxRequestDispatchContext context,
                                   final HttpRequest request,
                                   final HttpResponse response) {
        final File targetFile = context.getFile();
        final long fileLength = context.getActualFileLength();

        // 文件比较大,直接下载处理
        if(fileLength > NginxConst.BIG_FILE_SIZE) {
            logger.warn("[Nginx] fileLength={} > BIG_FILE_SIZE={}", fileLength, NginxConst.BIG_FILE_SIZE);
            response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"" + targetFile.getName() + "\"");
        }

        // 如果请求中有KEEP ALIVE信息
        if (HttpUtil.isKeepAlive(request)) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentTypeWithCharset(targetFile, context.getNginxConfig().getCharset()));
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
    }

    protected HttpResponse buildHttpResponse(NginxRequestDispatchContext context) {
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        return response;
    }



    /**
     * 是否需要压缩处理
     * @param context 上下文
     * @return 结果
     */
    protected boolean isZipEnable(NginxRequestDispatchContext context) {
        return InnerGzipUtil.isMatchGzip(context);
    }

    /**
     * gzip 的提前预处理
     * @param context  上下文
     * @param response 响应
     */
    protected void beforeZip(NginxRequestDispatchContext context, HttpResponse response) {
        File compressFile = InnerGzipUtil.prepareGzip(context, response);
        context.setFile(compressFile);
    }

    /**
     * gzip 的提前预处理
     * @param context  上下文
     * @param response 响应
     */
    protected void afterZip(NginxRequestDispatchContext context, HttpResponse response) {
        InnerGzipUtil.afterGzip(context, response);
    }

    protected boolean isZeroCopyEnable(NginxRequestDispatchContext context) {
        final NginxConfig nginxConfig = context.getNginxConfig();

        return EnableStatusEnum.isEnable(nginxConfig.getNginxSendFileConfig().getSendFile());
    }

    protected void writeAndFlushOnComplete(final ChannelHandlerContext ctx,
                                           final NginxRequestDispatchContext context) {
        // 传输完毕,发送最后一个空内容,标志传输结束
        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        // 如果不支持keep-Alive,服务器端主动关闭请求
        if (!HttpUtil.isKeepAlive(context.getRequest())) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void doDispatch(NginxRequestDispatchContext context) {
        final FullHttpRequest request = context.getRequest();
        final File targetFile = context.getFile();
        final ChannelHandlerContext ctx = context.getCtx();

        logger.info("[Nginx] start dispatch, path={}", targetFile.getAbsolutePath());
        // 长度+开始等基本信息
        fillContext(context);

        // 响应
        HttpResponse response = buildHttpResponse(context);

        // 添加请求头
        fillRespHeaders(context, request, response);

        //gzip
        boolean zipFlag = isZipEnable(context);
        try {
            if(zipFlag) {
                beforeZip(context, response);
            }

            // 写基本信息
            ctx.write(response);

            // 零拷贝
            boolean isZeroCopyEnable = isZeroCopyEnable(context);
            if(isZeroCopyEnable) {
                //zero-copy
                dispatchByZeroCopy(context);
            } else {
                // 普通
                dispatchByRandomAccessFile(context);
            }
        } finally {
            // 最后处理
            if(zipFlag) {
                afterZip(context, response);
            }
        }
    }

    /**
     * Netty 之 FileRegion 文件传输: https://www.jianshu.com/p/447c2431ac32
     *
     * @param context 上下文
     */
    protected void dispatchByZeroCopy(NginxRequestDispatchContext context) {
        final ChannelHandlerContext ctx = context.getCtx();
        final File targetFile = context.getFile();

        // 分块传输文件内容
        final long actualStart = context.getActualStart();
        final long actualFileLength = context.getActualFileLength();

        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r");
            FileChannel fileChannel = randomAccessFile.getChannel();

            // 使用DefaultFileRegion进行零拷贝传输
            DefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, actualStart, actualFileLength);
            ChannelFuture transferFuture = ctx.writeAndFlush(fileRegion);

            // 监听传输完成事件
            transferFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    try {
                        if (future.isSuccess()) {
                            writeAndFlushOnComplete(ctx, context);
                        } else {
                            // 处理传输失败
                            logger.error("[Nginx] file transfer failed", future.cause());
                            throw new Nginx4jException(future.cause());
                        }
                    } finally {
                        // 确保在所有操作完成之后再关闭文件通道和RandomAccessFile
                        try {
                            fileChannel.close();
                            randomAccessFile.close();
                        } catch (Exception e) {
                            logger.error("[Nginx] error closing file channel", e);
                        }
                    }
                }
            });

            // 记录传输进度(如果需要,可以通过监听器或其他方式实现)
            logger.info("[Nginx] file process >>>>>>>>>>> {}", actualFileLength);

        } catch (Exception e) {
            logger.error("[Nginx] file meet ex", e);
            throw new Nginx4jException(e);
        }
    }

    // 分块传输文件内容

    /**
     * 分块传输-普通方式
     * @param context 上下文
     */
    protected void dispatchByRandomAccessFile(NginxRequestDispatchContext context) {
        final ChannelHandlerContext ctx = context.getCtx();
        final File targetFile = context.getFile();

        // 分块传输文件内容
        long actualFileLength = context.getActualFileLength();
        // 分块传输文件内容
        final long actualStart = context.getActualStart();

        long totalRead = 0;

        try(RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r")) {
            // 开始位置
            randomAccessFile.seek(actualStart);

            ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);
            while (totalRead <= actualFileLength) {
                int bytesRead = randomAccessFile.read(buffer.array());
                if (bytesRead == -1) { // 文件读取完毕
                    logger.info("[Nginx] file read done.");
                    break;
                }

                buffer.limit(bytesRead);

                // 写入分块数据
                ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));
                buffer.clear(); // 清空缓冲区以供下次使用

                // process 可以考虑加一个 listener
                totalRead += bytesRead;
                logger.info("[Nginx] file process >>>>>>>>>>> {}/{}", totalRead, actualFileLength);
            }

            // 最后的处理
            writeAndFlushOnComplete(ctx, context);
        } catch (Exception e) {
            logger.error("[Nginx] file meet ex", e);
            throw new Nginx4jException(e);
        }
    }

}

这样原来的普通文件类只需要直接继承。

范围类重置如下方法即可:

/**
 * 文件范围查询
 *
 * @since 0.7.0
 * @author 老马啸西风
 */
public class NginxRequestDispatchFileRange extends AbstractNginxRequestDispatchFile {

    private static final Log logger = LogFactory.getLog(AbstractNginxRequestDispatchFullResp.class);

    @Override
    protected HttpResponse buildHttpResponse(NginxRequestDispatchContext context) {
        long start = context.getActualStart();

        // 构造HTTP响应
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
                start < 0 ? HttpResponseStatus.OK : HttpResponseStatus.PARTIAL_CONTENT);

        return response;
    }

    @Override
    protected void fillContext(NginxRequestDispatchContext context) {
        final long fileLength = context.getFile().length();
        final HttpRequest httpRequest = context.getRequest();

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

        long[] range = parseRange(rangeHeader, fileLength);
        long start = range[0];
        long end = range[1];
        long actualLength = end - start + 1;

        context.setActualStart(start);
        context.setActualFileLength(actualLength);
    }

    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}; // 表示无效的范围请求
    }

}

小结

模板方法对于代码的复用好处还是很大的,不然后续拓展特性,很多地方都需要修改多次。

下一节,我们考虑实现一下 HTTP keep-alive 的支持。

我是老马,期待与你的下次重逢。

开源地址

为了便于大家学习,已经将 nginx 开源

https://github.com/houbb/nginx4j

标签:11,long,response,nginx,range,context,手写,final
From: https://www.cnblogs.com/houbbBlogs/p/18239050

相关文章

  • Spring Boot、MongoDB、Vue 2和Nginx实现一个小说网站
    在本篇文章中,我们将带你逐步实现一个完备的小说网站项目,技术栈包括SpringBoot、MongoDB、Vue2和Nginx。1.项目概述我们将实现一个基本的小说网站,包含以下主要部分:后端API:使用SpringBoot实现,负责处理数据和业务逻辑。数据库:使用MongoDB存储小说数据。前端页面:使用Vue2实......
  • 【python】OpenCV—Blob Detection(11)
    学习来自OpenCV基础(10)使用OpenCV进行Blob检测文章目录1、cv2.SimpleBlobDetector_create中文文档2、默认parameters3、配置parameters附录——cv2.drawKeypoints1、cv2.SimpleBlobDetector_create中文文档cv2.SimpleBlobDetector_create是OpenCV库中用于创......
  • centos 环境 nginx 安装及常用命令简介
    一、引言Nginx是一个高性能的HTTP和反向代理服务器,因其卓越的性能、丰富的功能集、简单的配置和低资源消耗而广受欢迎。本文将详细介绍如何在CentOS系统中安装Nginx,并简要介绍一些常用的Nginx命令。二、在CentOS中安装Nginx1.先检查服务器GCC环境是否安装gcc-v2.......
  • Nginx访问日志
         Nginx日志是NginxWeb服务器产生的记录文件,主要用于跟踪和分析服务器的访问情况以及错误信息。Nginx日志主要分为两大类:访问日志(access_log):访问日志记录了每一次客户端对Nginx服务器的HTTP请求的详细信息,这对于统计分析、流量监控、用户行为分析等非常有用......
  • nginx监控
    1.监控nginx链接数状态status#1.开启status页面功能cat>/etc/nginx/conf.d/status.conf<<'EOF'server{listen80;server_namelocalhost;location/nginx_status{stub_statuson;access_logoff;}}EOF#2.访问测试[r......
  • nginx的负载均衡方式
    Nginx是一种高性能的HTTP和反向代理服务器,它具有强大的负载均衡功能。Nginx支持多种负载均衡策略,包括轮询、权重轮询、最少连接、IP哈希等。1.轮询(RoundRobin)轮询是Nginx的默认负载均衡方式,它将请求依次分配给每个后端服务器。配置:http{upstreamweb{ser......
  • Win11系统提示找不到navshutdown.dll文件的解决办法
    其实很多用户玩单机游戏或者安装软件的时候就出现过这种问题,如果是新手第一时间会认为是软件或游戏出错了,其实并不是这样,其主要原因就是你电脑系统的该dll文件丢失了或没有安装一些系统软件平台所需要的动态链接库,这时你可以下载这个navshutdown.dll文件(挑选合适的版本文件)把......
  • Win11系统提示找不到NDKPing.exe文件的解决办法
    其实很多用户玩单机游戏或者安装软件的时候就出现过这种问题,如果是新手第一时间会认为是软件或游戏出错了,其实并不是这样,其主要原因就是你电脑系统的该dll文件丢失了或没有安装一些系统软件平台所需要的动态链接库,这时你可以下载这个NDKPing.exe文件(挑选合适的版本文件)把它放......
  • 利用WinSW将Nginx 作为可正常启动/停止的windows服务
    下载winsw程序,Releases·winsw/winsw(github.com)将下载的exe文件放置到nginx.exe的同级目录,名字可以修改为nginx-service.exe(也可不修改)新建txt文本文档,并将其名称改为winsw程序一模一样的名称(不包含.和后缀),填写如下内容1<service>2<id>nginx</id>3<......