前言
各人好,我是老马。很高兴遇到你。
我们为 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 主要实在是开始位置和长度,和普通的处理存在差别。
基础文件实现
我们对常见的部分抽象出来,便于子类拓展- /**
- * 文件
- *
- * @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);
- }
- }
- }
复制代码 小结
模板方法对于代码的复用利益还是很大的,不然后续拓展特性,很多地方都需要修改多次。
下一节,我们思量实现一下 HTTP keep-alive 的支持。
我是老马,期待与你的下次重逢。
开源地址
为了便于各人学习,已经将 nginx 开源
https://github.com/houbb/nginx4j
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |