• 从零手写实现 nginx-25-directive map 条件判断指令


    前言

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

    我们为 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 请求头+响应头操作

    从零手写实现 nginx-19-nginx cors

    从零手写实现 nginx-20-nginx 占位符 placeholder

    从零手写实现 nginx-21-nginx modules 模块信息概览

    从零手写实现 nginx-22-nginx modules 分模块加载优化

    从零手写实现 nginx-23-nginx cookie 的操作处理

    从零手写实现 nginx-24-nginx IF 指令

    从零手写实现 nginx-25-nginx map 指令

    前言

    大家好,我是老马。

    这一节我们将配置的加载,拆分为不同的模块加载处理,便于后续拓展。

    if

    详细介绍一下 nginx 的 map 指令

    Nginx 的 map 指令是一个强大的工具,用于根据变量的值来设置另一个变量的值。

    它可以用于很多场景,比如基于请求的某些特征来动态设置变量,从而影响后续的处理逻辑。

    以下是关于 map 指令的详细介绍:

    语法和基本用法

    map 指令的基本语法如下:

    map $variable_to_test $variable_to_set {
        default value;
        key value;
        ...
    }
    • $variable_to_test:要测试的变量。
    • $variable_to_set:要设置的变量。
    • default:如果没有找到匹配的键,则使用默认值。
    • key value:键值对,根据 $variable_to_test 的值来设置 $variable_to_set

    示例

    假设我们想根据请求的主机名设置一个变量,进而用这个变量来决定后续的行为。

    可以这样使用 map 指令:

    http {
        map $http_host $backend_server {
            default         backend1.example.com;
            "www.example.com" backend2.example.com;
            "api.example.com" backend3.example.com;
        }
    
        server {
            listen 80;
            server_name example.com;
    
            location / {
                proxy_pass http://$backend_server;
            }
        }
    }

    在这个例子中:

    • 根据 $http_host 的值(请求头中的主机名),将 $backend_server 变量设置为不同的后端服务器。
    • 如果主机名是 www.example.com,则 $backend_server 设置为 backend2.example.com
    • 如果主机名是 api.example.com,则 $backend_server 设置为 backend3.example.com
    • 如果主机名不匹配任何键,则使用默认值 backend1.example.com

    复杂匹配

    map 指令支持更复杂的匹配模式,包括正则表达式。

    示例如下:

    http {
        map $request_uri $file_extension {
            "~*\.jpg$"  image;
            "~*\.png$"  image;
            "~*\.css$"  stylesheet;
            "~*\.js$"   javascript;
            default     other;
        }
    
        server {
            listen 80;
            server_name example.com;
    
            location / {
                set $content_type $file_extension;
                # 在此可以根据 $content_type 变量进行不同的处理
            }
        }
    }

    在这个例子中:

    • 根据请求的 URI,将 $file_extension 变量设置为不同的值。
    • 如果 URI 以 .jpg.png 结尾,则设置为 image
    • 如果 URI 以 .css 结尾,则设置为 stylesheet
    • 如果 URI 以 .js 结尾,则设置为 javascript
    • 如果 URI 不匹配任何模式,则使用默认值 other

    注意事项

    • map 指令必须放在 http 块中,不能直接放在 serverlocation 块中。
    • map 指令中使用的变量必须在之前已经定义或已经存在。
    • map 指令中键的匹配是按顺序进行的,匹配到第一个符合条件的键时就会停止匹配。

    实际应用

    map 指令可以用于很多实际应用场景,比如:

    • 根据客户端 IP 设置访问限制或调整访问策略。
    • 根据 User-Agent 头设置不同的响应头。
    • 动态调整缓存策略。
    • 根据请求路径或参数动态选择后端服务器。

    通过 map 指令,Nginx 的配置变得更加灵活和强大,可以根据实际需要进行复杂的条件判断和变量设置。

    为什么 nginx 中需要 map 指令

    在 Nginx 配置中,map 指令用于根据某个变量的值来动态设置另一个变量的值。这在许多情况下都非常有用,尤其是在需要根据请求的不同条件(如 URL、IP 地址、请求头等)来执行不同的配置或行为时。以下是一些具体的使用场景和map指令的详细解释:

    使用场景

    1. 动态配置

      • 通过map指令,可以根据请求的特定条件(例如,客户端 IP 地址、请求路径、请求头等)来设置不同的 Nginx 配置项。
      • 例如,可以根据访问路径设置不同的后端服务器、不同的缓存策略或不同的访问控制策略。
    2. 简化配置

      • map指令可以简化复杂的条件判断逻辑,避免在配置文件中编写大量的if指令。
      • 通过集中管理映射规则,可以使配置文件更清晰、更易于维护。
    3. 负载均衡

      • 可以根据请求的属性(如 User-Agent 或 Cookie)将请求分配到不同的后端服务器,实现更灵活的负载均衡策略。

    map 指令的语法和用法

    map指令的基本语法如下:

    map $variable_to_map $result_variable {
        default value;  # 设置默认值
        condition1 value1;  # 条件1 对应的值
        condition2 value2;  # 条件2 对应的值
        ...
    }
    • $variable_to_map:要根据其值进行映射的变量。
    • $result_variable:映射结果存储到的变量。
    • default value:如果没有匹配的条件,使用的默认值。
    • condition value:条件和值的对,满足条件时将值赋给$result_variable

    示例

    假设我们需要根据不同的主机名来设置不同的后端服务器:

    http {
        map $host $backend {
            default web1.example.com;
            host1.example.com web2.example.com;
            host2.example.com web3.example.com;
        }
    
        server {
            listen 80;
    
            location / {
                proxy_pass http://$backend;
            }
        }
    }

    在这个示例中:

    • 根据请求的主机名($host),将 $backend 变量设置为不同的后端服务器。
    • 默认情况下,$backend 会被设置为 web1.example.com
    • 如果请求的主机名是 host1.example.com$backend 会被设置为 web2.example.com
    • 如果请求的主机名是 host2.example.com$backend 会被设置为 web3.example.com

    结论

    map指令在 Nginx 中是一个强大的工具,可以根据请求的条件动态设置变量,从而实现更灵活和可维护的配置。

    通过合理使用map指令,可以简化配置文件,增强 Nginx 的功能,使其能够更好地适应各种复杂的应用场景。

    java 实现

    配置的解析

    我们以一个比较全的配置为例

    http {
        # 定义一个 map 指令,根据请求的主机名设置后端服务器
        map $host $backend {
            default web1.example.com;
            host1.example.com web2.example.com;
            host2.example.com web3.example.com;
        }
    
        # 定义另一个 map 指令,根据用户代理设置变量
        map $http_user_agent $mobile {
            default 0;
            "~*iphone|android" 1;
        }
    
        # others
    }

    配置加载

    直接放在 http 的全局配置中,解析如下:

    /**
     * @since 0.22.0
     * @author 老马啸西风
     */
    public class NginxUserMapConfigLoadFile implements INginxUserMapConfigLoad {
    
        //conf
    
        @Override
        public NginxUserMapConfig load() {
            Map mapping = new HashMap<>();
    
            NginxUserMapConfig config = new NginxUserMapConfig();
    
            List values = mapBlock.getValues();
            if(values.size() != 2) {
                throw new Nginx4jException("map 指令的 values 必须为 2,形如 map $key1 $key2");
            }
            config.setPlaceholderMatchKey(values.get(0));
            config.setPlaceholderTargetKey(values.get(1));
    
            Collection entryList = mapBlock.getEntries();
            if(CollectionUtil.isEmpty(entryList)) {
                throw new Nginx4jException("map 指令的映射关系不可为空,可以配置 default xxx");
            }
    
            for(NgxEntry entry : entryList) {
                if(entry instanceof NgxParam) {
                    NgxParam ngxParam = (NgxParam) entry;
                    String name = ngxParam.getName();
                    String value = ngxParam.getValue();
    
                    // 对比
                    if("default".equals(name)) {
                        config.setDefaultVal(value);
                    } else {
                        mapping.put(name, value);
                    }
                }
            }
    
            config.setMapping(mapping);
            return config;
        }
    
    }

    map 指令的实现

    目前实现简单的,在 dispatch 前触发 map 指令。

    /**
     * @since 0.22.0
     * @author 老马啸西风
     */
    public class NginxMapDirectiveDefault implements NginxMapDirective {
    
        private static final Log logger = LogFactory.getLog(NginxMapDirectiveDefault.class);
    
        @Override
        public void map(NginxRequestDispatchContext context) {
            Map placeholderMap = context.getPlaceholderMap();
            List mapConfigList = context.getNginxConfig().getNginxUserConfig().getMapConfigs();
            if(CollectionUtil.isEmpty(mapConfigList)) {
                // 忽略
                logger.info("mapConfigList 为空,忽略处理 map 指令");
                return;
            }
    
            for(NginxUserMapConfig mapConfig : mapConfigList) {
                processMap(mapConfig, placeholderMap);
            }
        }
    
        protected void processMap(NginxUserMapConfig mapConfig,
                                  Map placeholderMap) {
            //1. key
            String matchKey = mapConfig.getPlaceholderMatchKey();
            String matchValue = (String) placeholderMap.get(matchKey);
    
            String targetKey = mapConfig.getPlaceholderTargetKey();
    
            // 遍历
            for(Map.Entry mapEntry : mapConfig.getMapping().entrySet()) {
                if(matchValue == null) {
                    logger.info("matchValue is null, ignore match");
                    break;
                }
    
                String key = mapEntry.getKey();
                String value = mapEntry.getValue();
                if(key.equals(matchValue)) {
                    // fast-return
                    placeholderMap.put(targetKey, value);
                    logger.info("命中相等 {}={}, {}={}", matchKey, matchValue, targetKey, value);
                    return;
                } else if(matchValue.matches(key)) {
                    placeholderMap.put(targetKey, value);
                    logger.info("命中正则 {}={}, {}={}", matchKey, matchValue, targetKey, value);
                    return;
                }
            }
    
            // 默认值
            placeholderMap.put(targetKey, mapConfig.getDefaultVal());
            logger.info("命中默认值 {}={}", targetKey, mapConfig.getDefaultVal());
        }
    
    }

    测试验证

    直接本地启用访问 http://192.168.1.13:8080/

    日志:

    信息: 命中默认值 $backend=web1.example.com
    信息: 命中默认值 $mobile=0

    小结

    map 指令是 Nginx 中一个强大的工具,用于根据请求属性动态设置变量。

    通过合理使用 map 指令,可以简化配置,提高性能和灵活性。

    使用 Java 库 nginxparser 可以动态解析和处理 Nginx 配置文件,进一步增强配置管理的自动化和灵活性。

    我们后续考虑继续学习下 rewrite try_files 等指令。

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

    开源地址

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

    https://github.com/houbb/nginx4j

  • 相关阅读:
    bootstrap系列-6.BooStrap表单(上)
    Spring和SpringBoot比较,解惑区别
    RGB565 转 RGB888 的2种实现方式
    CentOS7安装MySQL8(Red Hat版)
    攻击角度多样化,放置类游戏安全风险分析
    Go 并发可视化解释 - Semaphore
    FreeRTOS源码阅读笔记1--task.c
    从零开始学前端:垃圾回收机制,闭包,案例 --- 今天你学习了吗?(JS:Day11)
    opencv 暴力特征匹配+FLANN特征匹配
    opencv:从0到实现人脸识别
  • 原文地址:https://blog.csdn.net/ryo1060732496/article/details/140309716