從零手寫實現 nginx-25-directive map 條件判斷指令

老马啸西风發表於2024-07-10

前言

大家好,我是老馬。很高興遇到你。

我們為 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<String, String> mapping = new HashMap<>();

        NginxUserMapConfig config = new NginxUserMapConfig();

        List<String> 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<NgxEntry> 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<String, Object> placeholderMap = context.getPlaceholderMap();
        List<NginxUserMapConfig> 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<String, Object> placeholderMap) {
        //1. key
        String matchKey = mapConfig.getPlaceholderMatchKey();
        String matchValue = (String) placeholderMap.get(matchKey);

        String targetKey = mapConfig.getPlaceholderTargetKey();

        // 遍歷
        for(Map.Entry<String, String> 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

相關文章