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

前言

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

我们为 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);}}@Overridepublic 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);//gzipboolean zipFlag = isZipEnable(context);try {if(zipFlag) {beforeZip(context, response);}// 写基本信息ctx.write(response);// 零拷贝boolean isZeroCopyEnable = isZeroCopyEnable(context);if(isZeroCopyEnable) {//zero-copydispatchByZeroCopy(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() {@Overridepublic 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 {// 确保在所有操作完成之后再关闭文件通道和RandomAccessFiletry {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 可以考虑加一个 listenertotalRead += 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);@Overrideprotected 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;}@Overrideprotected 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/24878.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言中memset()以及memcpy()函数使用方法

memset( ) 在函数memset&#xff08;void *A&#xff0c;int B&#xff0c;int C&#xff09;&#xff1b;语句中&#xff0c;程序完成了将A指针指向的内容开始往后的C个字节内容置为B。 例如下列函数&#xff0c;最终结果是&#xff1a;cps数组内容为&#xff1a;2222222222…

2024年【R2移动式压力容器充装】考试技巧及R2移动式压力容器充装复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 R2移动式压力容器充装考试技巧参考答案及R2移动式压力容器充装考试试题解析是安全生产模拟考试一点通题库老师及R2移动式压力容器充装操作证已考过的学员汇总&#xff0c;相对有效帮助R2移动式压力容器充装复审考试学…

Linux 35.5 + JetPack v5.1.3@RACER编译安装

Linux 35.5 JetPack v5.1.3RACER编译安装 1. 源由2. 编译&安装Step 1&#xff1a;依赖库安装Step 2&#xff1a;LKH-3安装Step 3&#xff1a;建立工程Step 4&#xff1a;编译工程Step 5&#xff1a;安装工程 3. 问题汇总3.1 组件ros-noetic-multi-map-server问题3.2 swarm…

监控系统如何选择交换机

一、基础知识 01 摄像机码流 选择交换机前&#xff0c;首先要弄清楚每路图像占用多少带宽&#xff0c;而这个就是码流。 02 摄像机的数量 要弄清楚交换机的带宽容量。常用交换机有百兆交换机、千兆交换机。它们的实际带宽一般只有理论值的 60~70% &#xff0c;所以它们端口的可…

记录:linux桌面管理基础-X11协议(X window system)

1、认识X11 X11是X协议&#xff0c;版本号为11。X协议是专门被设计为linux桌面管理服务的&#xff0c;而linux桌面环境不像windows那样作为系统内核的一部分&#xff0c;作为一个普通程序运行在用户态上。该协议的设计初衷是为了linux的图形界面满足跨平台、跨网络、与具体硬件…

回溯算法举例

回溯算法概述 回溯算法是一种系统地搜索问题解空间的方法,通过逐步构建解决方案,并在发现当前解不满足条件时回溯到上一步,从而尝试其他可能的解。回溯算法广泛应用于组合优化问题、约束满足问题等。 N皇后问题:将N个皇后放置在NN的棋盘上,使得它们互不攻击。数独:填充数…

Web前端岗位深度剖析:必备技能、挑战与未来展望

Web前端岗位深度剖析&#xff1a;必备技能、挑战与未来展望 在数字化飞速发展的今天&#xff0c;Web前端岗位已成为互联网行业的核心力量。它不仅仅是技术的代表&#xff0c;更是连接用户与产品、实现业务价值的关键环节。那么&#xff0c;Web前端岗位究竟要求什么&#xff1f…

DOM型xss靶场实验

DOM型xss可以使用js去控制标签中的内容。 我使用的是一个在线的dom型xss平台&#xff0c;靶场链接&#xff1a;Challenges 第一关Ma Spaghet!&#xff1a; Ma Spaghet! 关卡 <h2 id"spaghet"></h2> <script>spaghet.innerHTML (new URL(locatio…

【TB作品】msp430f5529单片机,dht22,温湿度传感器,OLED显示屏

使用DHT22温湿度传感器和OLED显示屏的单片机项目 博客名称 利用MSP430单片机读取DHT22并显示温湿度 作品功能 本项目利用MSP430单片机读取DHT22温湿度传感器的数据&#xff0c;并将温湿度信息显示在OLED显示屏上。通过这个项目&#xff0c;您可以学习如何使用单片机与传感器…

前端开发之中svg图标的使用和实例

svg图标的使用和实例 前言效果图1、安装插件2、vue3中使用2.1、 在components文件夹中,创建公共类SvgIcon/index.vue2.2、创建icons文件,存放svg图标和将所有的svg图标进行引用并注册成全局组件2.3、在man.js 中注册2.4、在vue.config.js中配置svg2.5、在vue中的调用svg图标3…

代码随想录第三十二天打卡|122.买卖股票的最佳时机II,55. 跳跃游戏,45.跳跃游戏II

122.买卖股票的最佳时机II 本题解法很巧妙&#xff0c;大家可以看题思考一下&#xff0c;在看题解。 代码随想录 class Solution { public:int maxProfit(vector<int>& prices) {int min_numINT_MAX;int res0;for (int i0;i<prices.size();i){if (prices[i]<m…

数仓建模—指标体系指标拆解和选取

数仓建模—指标拆解和选取 第一节指标体系初识介绍了什么是指标体系 第二节指标体系分类分级和评价管理介绍了指标体系管理相关的,也就是指标体系的分级分类 这一节我们看一下指标体系的拆解和指标选取,这里我们先说指标选取,其实在整个企业的数字化建设过程中我们其实最…

嵌入式c extern的用法

目录 一、extern关键字的作用 二、使用示例 三、注意事项 四、总结 在嵌入式编程中&#xff0c;特别是在使用C语言进行STM32等嵌入式系统的开发时&#xff0c;extern关键字是一个非常重要的概念。它主要用于声明外部变量或函数&#xff0c;告诉编译器这些变量或函数的定义在…

电脑下载速度很慢怎么解决 电脑下载加速工具测评推荐

电脑下载速度慢&#xff0c;不仅耗时冗长&#xff0c;还会影响工作和学习效率。漫长的等待让人心情焦虑&#xff0c;每一秒都是对耐心的极大考验。有关电脑下载速度很慢怎么解决&#xff0c;电脑下载加速工具评测推荐的问题&#xff0c;本文将进行详细介绍。 一、电脑下载速度…

IO进程线程(七)代码替换函数、守护进程

文章目录 一、代码替换函数&#xff08;一&#xff09;system函数&#xff08;二&#xff09;exec函数族 二、守护进程&#xff08;一&#xff09;创建1. 脱离父进程影响2. 脱离原会话组和进程组的影响3.修改进程工作目录4. 修改进程创建文件的掩码5. 关闭从父进程继承的文件描…

将stanfordcorenlp的tokenizer换成自定义的(或用stanfordcorenlp对自定义tokenizer分词后的结果做ner)

本文是基于中文语料做的&#xff0c;对于英文语料应该也是同理&#xff0c;即同样适用的。 分析stanfordcorenlp的分词结果&#xff0c;可以发现&#xff0c;它好像是对最小的中文词进行分词&#xff0c;即其对中文的分词粒度很小&#xff0c;这对于某些nlp场景可能就不太合适…

每日一练——相同分数的最大操作数目

3038. 相同分数的最大操作数目 I - 力扣&#xff08;LeetCode&#xff09;相同分数的最大操作数目 I3038. 相同分数的最大操作数目 I - 力扣&#xff08;LeetCode&#xff09; 第一版 int maxOperations(int* nums, int numsSize) {if(numsSize < 1)return 0;int temp 0;…

内存管理--4.用幻灯片讲解内存分配器Allocator

用幻灯片讲解内存分配器Allocators Allocators 内存分配器 提供内存分配策略的通用接口委托给 C 运行时&#xff1a;new / delete使用块内存池管理内存使用不同大小的块内存池管理内存 为什么用分配器? 将容器逻辑与内存分配策略解耦速度&#xff1a;内存分配速度慢确保…

嵌入式学习——Linux高级编程复习(文件IO)——day37

1. 文件IO——&#xff08;无缓存、文件描述符&#xff09; 1.1 定义 文件IO是指文件输入/输出&#xff0c;是计算机程序中用于读取和写入文件的操作。通过文件IO&#xff0c;程序可以从文件中读取数据到内存中&#xff0c;或者将内存中的数据写入文件中&#xff0c;文件可以是…

Python3 笔记:字符串的 startswith() 和 endswith()

1、startswith() 方法用于检查字符串是否是以指定子字符串开头&#xff0c;如果是则返回 True&#xff0c;否则返回 False。如果参数 beg 和 end 指定了值&#xff0c;则在指定范围内检查。 语法&#xff1a;str.startswith(substr, beg0,endlen(string)) 参数&#xff1a; s…