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

惊落一身雪  金牌会员 | 2024-6-8 22:49:21 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 860|帖子 860|积分 2580

前言

各人好,我是老马。很高兴遇到你。
我们为 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 请求头响应头的处理
背景

最初感觉范围处理和文件的处理不是雷同的逻辑,所以做了拆分。
但是后来发现有很多公共的逻辑。
主要两种优化方式:

  • 把范围+文件合并到同一个文件中处理。添加各种判断代码
  • 采用模板方法,便于后续拓展修改。
这里主要尝试下第 2 种,便于后续的拓展。
代码的相似之处

首先,我们要找到二者的雷同之处。
range 主要实在是开始位置和长度,和普通的处理存在差别。
基础文件实现

我们对常见的部分抽象出来,便于子类拓展
  1. /**
  2. * 文件
  3. *
  4. * @since 0.10.0
  5. * @author 老马笑西风
  6. */
  7. public class AbstractNginxRequestDispatchFile extends AbstractNginxRequestDispatch {
  8.     /**
  9.      * 获取长度
  10.      * @param context 上下文
  11.      * @return 结果
  12.      */
  13.     protected long getActualLength(final NginxRequestDispatchContext context) {
  14.         final File targetFile = context.getFile();
  15.         return targetFile.length();
  16.     }
  17.     /**
  18.      * 获取开始位置
  19.      * @param context 上下文
  20.      * @return 结果
  21.      */
  22.     protected long getActualStart(final NginxRequestDispatchContext context) {
  23.         return 0L;
  24.     }
  25.     protected void fillContext(final NginxRequestDispatchContext context) {
  26.         long actualLength = getActualLength(context);
  27.         long actualStart = getActualStart(context);
  28.         context.setActualStart(actualStart);
  29.         context.setActualFileLength(actualLength);
  30.     }
  31.     /**
  32.      * 填充响应头
  33.      * @param context 上下文
  34.      * @param response 响应
  35.      * @since 0.10.0
  36.      */
  37.     protected void fillRespHeaders(final NginxRequestDispatchContext context,
  38.                                    final HttpRequest request,
  39.                                    final HttpResponse response) {
  40.         final File targetFile = context.getFile();
  41.         final long fileLength = context.getActualFileLength();
  42.         // 文件比较大,直接下载处理
  43.         if(fileLength > NginxConst.BIG_FILE_SIZE) {
  44.             logger.warn("[Nginx] fileLength={} > BIG_FILE_SIZE={}", fileLength, NginxConst.BIG_FILE_SIZE);
  45.             response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename="" + targetFile.getName() + """);
  46.         }
  47.         // 如果请求中有KEEP ALIVE信息
  48.         if (HttpUtil.isKeepAlive(request)) {
  49.             response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
  50.         }
  51.         response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentTypeWithCharset(targetFile, context.getNginxConfig().getCharset()));
  52.         response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
  53.     }
  54.     protected HttpResponse buildHttpResponse(NginxRequestDispatchContext context) {
  55.         HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
  56.         return response;
  57.     }
  58.     /**
  59.      * 是否需要压缩处理
  60.      * @param context 上下文
  61.      * @return 结果
  62.      */
  63.     protected boolean isZipEnable(NginxRequestDispatchContext context) {
  64.         return InnerGzipUtil.isMatchGzip(context);
  65.     }
  66.     /**
  67.      * gzip 的提前预处理
  68.      * @param context  上下文
  69.      * @param response 响应
  70.      */
  71.     protected void beforeZip(NginxRequestDispatchContext context, HttpResponse response) {
  72.         File compressFile = InnerGzipUtil.prepareGzip(context, response);
  73.         context.setFile(compressFile);
  74.     }
  75.     /**
  76.      * gzip 的提前预处理
  77.      * @param context  上下文
  78.      * @param response 响应
  79.      */
  80.     protected void afterZip(NginxRequestDispatchContext context, HttpResponse response) {
  81.         InnerGzipUtil.afterGzip(context, response);
  82.     }
  83.     protected boolean isZeroCopyEnable(NginxRequestDispatchContext context) {
  84.         final NginxConfig nginxConfig = context.getNginxConfig();
  85.         return EnableStatusEnum.isEnable(nginxConfig.getNginxSendFileConfig().getSendFile());
  86.     }
  87.     protected void writeAndFlushOnComplete(final ChannelHandlerContext ctx,
  88.                                            final NginxRequestDispatchContext context) {
  89.         // 传输完毕,发送最后一个空内容,标志传输结束
  90.         ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
  91.         // 如果不支持keep-Alive,服务器端主动关闭请求
  92.         if (!HttpUtil.isKeepAlive(context.getRequest())) {
  93.             lastContentFuture.addListener(ChannelFutureListener.CLOSE);
  94.         }
  95.     }
  96.     @Override
  97.     public void doDispatch(NginxRequestDispatchContext context) {
  98.         final FullHttpRequest request = context.getRequest();
  99.         final File targetFile = context.getFile();
  100.         final ChannelHandlerContext ctx = context.getCtx();
  101.         logger.info("[Nginx] start dispatch, path={}", targetFile.getAbsolutePath());
  102.         // 长度+开始等基本信息
  103.         fillContext(context);
  104.         // 响应
  105.         HttpResponse response = buildHttpResponse(context);
  106.         // 添加请求头
  107.         fillRespHeaders(context, request, response);
  108.         //gzip
  109.         boolean zipFlag = isZipEnable(context);
  110.         try {
  111.             if(zipFlag) {
  112.                 beforeZip(context, response);
  113.             }
  114.             // 写基本信息
  115.             ctx.write(response);
  116.             // 零拷贝
  117.             boolean isZeroCopyEnable = isZeroCopyEnable(context);
  118.             if(isZeroCopyEnable) {
  119.                 //zero-copy
  120.                 dispatchByZeroCopy(context);
  121.             } else {
  122.                 // 普通
  123.                 dispatchByRandomAccessFile(context);
  124.             }
  125.         } finally {
  126.             // 最后处理
  127.             if(zipFlag) {
  128.                 afterZip(context, response);
  129.             }
  130.         }
  131.     }
  132.     /**
  133.      * Netty 之 FileRegion 文件传输: https://www.jianshu.com/p/447c2431ac32
  134.      *
  135.      * @param context 上下文
  136.      */
  137.     protected void dispatchByZeroCopy(NginxRequestDispatchContext context) {
  138.         final ChannelHandlerContext ctx = context.getCtx();
  139.         final File targetFile = context.getFile();
  140.         // 分块传输文件内容
  141.         final long actualStart = context.getActualStart();
  142.         final long actualFileLength = context.getActualFileLength();
  143.         try {
  144.             RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r");
  145.             FileChannel fileChannel = randomAccessFile.getChannel();
  146.             // 使用DefaultFileRegion进行零拷贝传输
  147.             DefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, actualStart, actualFileLength);
  148.             ChannelFuture transferFuture = ctx.writeAndFlush(fileRegion);
  149.             // 监听传输完成事件
  150.             transferFuture.addListener(new ChannelFutureListener() {
  151.                 @Override
  152.                 public void operationComplete(ChannelFuture future) {
  153.                     try {
  154.                         if (future.isSuccess()) {
  155.                             writeAndFlushOnComplete(ctx, context);
  156.                         } else {
  157.                             // 处理传输失败
  158.                             logger.error("[Nginx] file transfer failed", future.cause());
  159.                             throw new Nginx4jException(future.cause());
  160.                         }
  161.                     } finally {
  162.                         // 确保在所有操作完成之后再关闭文件通道和RandomAccessFile
  163.                         try {
  164.                             fileChannel.close();
  165.                             randomAccessFile.close();
  166.                         } catch (Exception e) {
  167.                             logger.error("[Nginx] error closing file channel", e);
  168.                         }
  169.                     }
  170.                 }
  171.             });
  172.             // 记录传输进度(如果需要,可以通过监听器或其他方式实现)
  173.             logger.info("[Nginx] file process >>>>>>>>>>> {}", actualFileLength);
  174.         } catch (Exception e) {
  175.             logger.error("[Nginx] file meet ex", e);
  176.             throw new Nginx4jException(e);
  177.         }
  178.     }
  179.     // 分块传输文件内容
  180.     /**
  181.      * 分块传输-普通方式
  182.      * @param context 上下文
  183.      */
  184.     protected void dispatchByRandomAccessFile(NginxRequestDispatchContext context) {
  185.         final ChannelHandlerContext ctx = context.getCtx();
  186.         final File targetFile = context.getFile();
  187.         // 分块传输文件内容
  188.         long actualFileLength = context.getActualFileLength();
  189.         // 分块传输文件内容
  190.         final long actualStart = context.getActualStart();
  191.         long totalRead = 0;
  192.         try(RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r")) {
  193.             // 开始位置
  194.             randomAccessFile.seek(actualStart);
  195.             ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);
  196.             while (totalRead <= actualFileLength) {
  197.                 int bytesRead = randomAccessFile.read(buffer.array());
  198.                 if (bytesRead == -1) { // 文件读取完毕
  199.                     logger.info("[Nginx] file read done.");
  200.                     break;
  201.                 }
  202.                 buffer.limit(bytesRead);
  203.                 // 写入分块数据
  204.                 ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));
  205.                 buffer.clear(); // 清空缓冲区以供下次使用
  206.                 // process 可以考虑加一个 listener
  207.                 totalRead += bytesRead;
  208.                 logger.info("[Nginx] file process >>>>>>>>>>> {}/{}", totalRead, actualFileLength);
  209.             }
  210.             // 最后的处理
  211.             writeAndFlushOnComplete(ctx, context);
  212.         } catch (Exception e) {
  213.             logger.error("[Nginx] file meet ex", e);
  214.             throw new Nginx4jException(e);
  215.         }
  216.     }
  217. }
复制代码
小结

模板方法对于代码的复用利益还是很大的,不然后续拓展特性,很多地方都需要修改多次。
下一节,我们思量实现一下 HTTP keep-alive 的支持。
我是老马,期待与你的下次重逢。
开源地址

为了便于各人学习,已经将 nginx 开源
https://github.com/houbb/nginx4j

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊落一身雪

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表