前言
大家好,我是老馬。很高興遇到你。
我們為 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
塊中,不能直接放在server
或location
塊中。- 在
map
指令中使用的變數必須在之前已經定義或已經存在。 map
指令中鍵的匹配是按順序進行的,匹配到第一個符合條件的鍵時就會停止匹配。
實際應用
map
指令可以用於很多實際應用場景,比如:
- 根據客戶端 IP 設定訪問限制或調整訪問策略。
- 根據 User-Agent 頭設定不同的響應頭。
- 動態調整快取策略。
- 根據請求路徑或引數動態選擇後端伺服器。
透過 map
指令,Nginx 的配置變得更加靈活和強大,可以根據實際需要進行復雜的條件判斷和變數設定。
為什麼 nginx 中需要 map 指令
在 Nginx 配置中,map
指令用於根據某個變數的值來動態設定另一個變數的值。這在許多情況下都非常有用,尤其是在需要根據請求的不同條件(如 URL、IP 地址、請求頭等)來執行不同的配置或行為時。以下是一些具體的使用場景和map
指令的詳細解釋:
使用場景
-
動態配置
- 透過
map
指令,可以根據請求的特定條件(例如,客戶端 IP 地址、請求路徑、請求頭等)來設定不同的 Nginx 配置項。 - 例如,可以根據訪問路徑設定不同的後端伺服器、不同的快取策略或不同的訪問控制策略。
- 透過
-
簡化配置
map
指令可以簡化複雜的條件判斷邏輯,避免在配置檔案中編寫大量的if
指令。- 透過集中管理對映規則,可以使配置檔案更清晰、更易於維護。
-
負載均衡
- 可以根據請求的屬性(如 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