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

nginx 的占位符

Nginx 是一个高性能的 HTTP 和反向代理服务器。它使用占位符(变量)来动态地生成配置和响应。

这些占位符可以在 Nginx 配置文件中使用,并在运行时被特定的值替换。

以下是对 Nginx 占位符的详细介绍,包括一些常见的变量及其用法。

1. 基本语法

Nginx 变量的语法格式是 $variable_name。例如:

 

nginx

复制代码

server { listen 80; server_name example.com; location / { return 200 "The request URI is $uri\n"; } }

在这个例子中,$uri 是一个占位符,表示请求的 URI。

2. 内置变量

Nginx 提供了许多内置变量,以下是一些常见的内置变量:

  • $args:请求中的参数。
  • $content_length:请求的 Content-Length 头字段。
  • $content_type:请求的 Content-Type 头字段。
  • $document_root:当前请求的根目录或 alias 指定的路径。
  • $host:请求的主机头字段,如果主机头字段不可用,则等于服务器名称。
  • $http_user_agent:客户端的 User-Agent 头字段。
  • $http_cookie:客户端的 Cookie 头字段。
  • $limit_rate:用于限制连接速率。
  • $request_method:请求方法(GET、POST 等)。
  • $remote_addr:客户端 IP 地址。
  • $remote_port:客户端端口。
  • $request_uri:完整的原始请求 URI,包括参数。
  • $scheme:请求使用的协议(http 或 https)。
  • $server_protocol:请求使用的协议版本。
  • $server_addr:服务器的地址。
  • $server_name:服务器名称。
  • $server_port:服务器端口。
  • $uri:不包含请求参数的请求 URI。

3. 自定义变量

除了内置变量,Nginx 还允许用户自定义变量。自定义变量可以在 set 指令中定义。以下是一个示例:

 

nginx

复制代码

server { listen 80; server_name example.com; set $my_variable "Hello, Nginx!"; location / { return 200 "$my_variable\n"; } }

在这个例子中,定义了一个自定义变量 $my_variable,其值为 "Hello, Nginx!",并在响应中返回这个值。

4. 使用变量进行条件控制

Nginx 的 if 指令可以根据变量的值进行条件控制。例如:

 

nginx

复制代码

server { listen 80; server_name example.com; location / { if ($request_method = POST) { return 405 "Method Not Allowed\n"; } return 200 "Hello, Nginx!\n"; } }

在这个例子中,如果请求方法是 POST,服务器会返回 405 状态码和 "Method Not Allowed" 信息,否则返回 200 状态码和 "Hello, Nginx!" 信息。

5. 变量的应用场景

变量在 Nginx 中有许多实际应用场景:

  • 日志记录:可以在日志格式中使用变量记录详细的请求信息。
  • URL 重写:使用变量来实现复杂的 URL 重写规则。
  • 负载均衡:在负载均衡配置中使用变量进行复杂的请求路由。
  • 限速限流:使用变量动态控制请求的限速限流策略。

示例:动态生成响应

下面是一个结合多个变量的示例,用于动态生成响应内容:

 

nginx

复制代码

server { listen 80; server_name example.com; location / { set $custom_message "Your IP is $remote_addr and you are using $http_user_agent"; return 200 "$custom_message\n"; } }

在这个示例中,服务器会返回一个包含客户端 IP 地址和 User-Agent 的消息。

Nginx 的占位符功能强大且灵活,通过合理地使用这些变量,可以实现许多复杂的功能和动态配置。

为什么 nginx 要支持占位符呢?

Nginx 支持占位符(变量)的主要原因是为了提供灵活性和动态性,使其配置能够适应各种复杂的应用场景。以下是 Nginx 支持占位符的一些关键原因和优势:

1. 动态配置

占位符使得 Nginx 配置文件能够根据实际请求动态生成响应或调整行为。例如,可以根据请求的 URI、方法或头信息来动态地路由请求、返回不同的内容或者做其他处理。

示例:动态内容返回

 

nginx

复制代码

server { listen 80; server_name example.com; location / { return 200 "Requested URI: $uri\n"; } }

这个示例会根据请求的 URI 返回相应的消息,使得响应内容是动态生成的。

2. 细粒度控制

通过占位符,Nginx 可以对请求和响应进行细粒度控制。例如,基于客户端 IP 地址、User-Agent 或请求参数进行不同的处理。这种细粒度控制有助于实现精细的访问控制、安全检查、个性化服务等。

示例:基于 IP 地址的访问控制

 

nginx

复制代码

server { listen 80; server_name example.com; location / { if ($remote_addr = "192.168.1.1") { return 403 "Access denied\n"; } return 200 "Welcome\n"; } }

这个示例会根据客户端的 IP 地址进行访问控制。

3. 简化配置管理

占位符可以减少重复配置,提高配置文件的可读性和可维护性。通过定义变量和复用这些变量,可以使得配置文件更简洁、直观。

示例:复用变量简化配置

 

nginx

复制代码

server { listen 80; server_name example.com; set $root_path /var/www/html; location / { root $root_path; } location /images { root $root_path; } }

在这个示例中,通过定义变量 $root_path,避免了重复配置根目录路径。

4. 支持复杂应用场景

Nginx 的占位符使其能够支持复杂的应用场景,例如负载均衡、日志记录、限速限流等。通过使用变量,可以实现更灵活的负载均衡策略、详细的日志记录格式、动态的限速策略等。

示例:自定义日志格式

 

nginx

复制代码

log_format custom '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log custom;

这个示例通过占位符定义了自定义的日志格式,以记录详细的请求信息。

5. 安全与优化

通过使用占位符,可以实现动态的安全检查和优化策略。例如,根据请求头信息或者参数来启用或禁用某些功能,从而增强安全性和性能。

示例:根据 User-Agent 进行优化

 

nginx

复制代码

server { listen 80; server_name example.com; location / { if ($http_user_agent ~* "Mobile") { set $mobile 1; } if ($mobile) { return 200 "Mobile optimization enabled\n"; } return 200 "Standard optimization\n"; } }

这个示例展示了如何根据 User-Agent 实现移动设备优化。

总结

Nginx 支持占位符的主要目的是提供更高的灵活性和动态性,使其能够适应各种复杂的应用场景和需求。

通过占位符,Nginx 的配置能够变得更加简洁、可读、可维护,并且能够实现复杂的控制逻辑,从而提供更强大的功能和更高的性能。

java 如何设计实现?

系统内置的 placeholder 变量

我们首先实现内置的 placeholder 占位符操作。

定义抽象的接口

 

java

复制代码

/** * 占位符处理类 * @since 0.17.0 * * @author 老马啸西风 */ public abstract class AbstractNginxPlaceholder implements INginxPlaceholder { private static final Log logger = LogFactory.getLog(AbstractNginxPlaceholder.class); @Override public void placeholder(NginxRequestDispatchContext context) { // 上下文存储的内容 Map<String, Object> placeholderMap = context.getPlaceholderMap(); // 请求头 FullHttpRequest request = context.getRequest(); String key = getKey(request, context); Object value = extract(request, context); placeholderMap.put(key, value); logger.info("placeholder put key={},value={}", key, value); } /** * 提取值 * @param request 请求头 * @param context 上下文 * @return 结果 */ protected abstract Object extract(FullHttpRequest request, NginxRequestDispatchContext context); /** * 唯一标识 * @param request 请求头 * @param context 上下文 * @return 结果 */ protected abstract String getKey(FullHttpRequest request, NginxRequestDispatchContext context); }

常见内置的实现

  • $args:请求中的参数。
  • $content_length:请求的 Content-Length 头字段。
  • $content_type:请求的 Content-Type 头字段。
  • $document_root:当前请求的根目录或 alias 指定的路径。
  • $host:请求的主机头字段,如果主机头字段不可用,则等于服务器名称。
  • $http_user_agent:客户端的 User-Agent 头字段。
  • $http_cookie:客户端的 Cookie 头字段。
  • $limit_rate:用于限制连接速率。
  • $request_method:请求方法(GET、POST 等)。
  • $remote_addr:客户端 IP 地址。
  • $remote_port:客户端端口。
  • $request_uri:完整的原始请求 URI,包括参数。
  • $scheme:请求使用的协议(http 或 https)。
  • $server_protocol:请求使用的协议版本。
  • $server_addr:服务器的地址。
  • $server_name:服务器名称。
  • $server_port:服务器端口。
  • $uri:不包含请求参数的请求 URI。

以 args 为例,其他的实现类似:

 

java

复制代码

/** * 占位符处理类 * @since 0.17.0 * * @author 老马啸西风 */ public class NginxPlaceholderArgs extends AbstractNginxPlaceholder { private static final Log logger = LogFactory.getLog(NginxPlaceholderArgs.class); @Override protected Object extract(FullHttpRequest request, NginxRequestDispatchContext context) { QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); StringBuilder args = new StringBuilder(); for (Map.Entry<String, List<String>> entry : decoder.parameters().entrySet()) { for (String value : entry.getValue()) { if (args.length() > 0) { args.append("&"); } args.append(entry.getKey()).append("=").append(value); } } return args.toString(); } @Override protected String getKey(FullHttpRequest request, NginxRequestDispatchContext context) { return "$args"; } }

set 的支持

可以看到 nginx 默认支持了 set 操作符,可以设置一个变量。

这个属于操作指令,我们在上一期的指令中进行拓展:

 

java

复制代码

/** * SET 符号,设置一个 $ 变量 * * @since 0.17.0 * @author 老马啸西风 */ public class NginxParamHandleSet extends AbstractNginxParamHandle { private static final Log logger = LogFactory.getLog(NginxParamHandleSet.class); /** * # 设置一个占位符的值 * * set $mobile 1; * * @param configParam 参数 * @param context 上下文 */ @Override public void doBeforeDispatch(NginxUserConfigParam configParam, NginxRequestDispatchContext context) { Map<String, Object> placeholderMap = context.getPlaceholderMap(); // 处理 List<String> values = configParam.getValues(); String headerName = values.get(0); String headerValue = values.get(1); // 变量名必须以 $ 开始 if(!headerName.startsWith(NginxConst.PLACEHOLDER_PREFIX)) { throw new Nginx4jException("SET 指令对应的变量名必须以 $ 开始"); } placeholderMap.put(headerName, headerValue); } //... @Override public boolean doMatch(NginxUserConfigParam configParam, NginxRequestDispatchContext context) { return "set".equalsIgnoreCase(configParam.getName()); } }

我们把 set 对应的指令值,放入到 placeholderMap 占位符中。

set指令 + 占位符的处理的时机

我们放在 request 分发处理前,统一处理:

 

java

复制代码

/** * 请求头的统一处理 * @param context 上下文 * @author 老马啸西风 */ protected void beforeDispatch(final NginxRequestDispatchContext context) { // 参数管理类 final INginxParamManager paramManager = context.getNginxConfig().getNginxParamManager(); // v0.17.0 占位符管理类 final INginxPlaceholderManager placeholderManager = context.getNginxConfig().getNginxPlaceholderManager(); // 提前处理内置的各种参数 placeholderManager.init(context); //1. 当前的配置 NginxUserServerLocationConfig locationConfig = context.getCurrentUserServerLocationConfig(); if(locationConfig == null) { return; } List<NginxUserConfigParam> directives = locationConfig.getDirectives(); if(CollectionUtil.isEmpty(directives)) { return; } // 处理 for(NginxUserConfigParam configParam : directives) { // 占位符处理 placeholderHandle(configParam, placeholderManager, context); List<INginxParamHandle> handleList = paramManager.paramHandleList(configParam, context); if(CollectionUtil.isNotEmpty(handleList)) { for(INginxParamHandle paramHandle : handleList) { paramHandle.beforeDispatch(configParam, context); } } } } /** * 占位符处理 * * SET 问题,这个是按顺序处理的,所以暂时不用特别考虑 * * @param configParam 配置指令 * @param placeholderManager 占位符管理类 * @param context 上下文 * @since 0.17.0 */ protected void placeholderHandle(NginxUserConfigParam configParam, final INginxPlaceholderManager placeholderManager, final NginxRequestDispatchContext context) { String name = configParam.getName(); if(name.equals("set")) { logger.warn("暂时不处理 set 指令对应的操作符替换,后续可考虑改进。"); return; } // name 暂时不添加 $ 处理 // value String value = configParam.getValue(); String actualValue = getPlaceholderStr(value, placeholderManager, context); configParam.setValue(actualValue); // list List<String> valueList = configParam.getValues(); List<String> newValueList = new ArrayList<>(); if(CollectionUtil.isNotEmpty(valueList)) { for(String valueItem : valueList) { String actualValueItem = getPlaceholderStr(valueItem, placeholderManager, context); newValueList.add(actualValueItem); } configParam.setValues(newValueList); } // 结束 } /** * 获取占位符对应的值 * @param value 原始值 * @param placeholderManager 管理类 * @param context 上下文 * @return 结果 */ protected String getPlaceholderStr(String value, final INginxPlaceholderManager placeholderManager, final NginxRequestDispatchContext context) { // value if(value.startsWith(NginxConst.PLACEHOLDER_PREFIX)) { Object actualValue = placeholderManager.getValue(context, value); if(actualValue == null) { logger.error("占位符未初始化 value={}", value); throw new Nginx4jException("占位符未初始化" + value); } // 设置值 String actualValueStr = actualValue.toString(); logger.info("占位符替换 value={}, actualValueStr={}", value, actualValueStr); return actualValueStr; } // 原始值 return value; }

首先初始化所有的占位符策略;

然后依次执行以前的 param 用户指令,这里 set 会按照顺序执行。

我们在占位符策略处理时特意跳过了 set,其实可以细化一点,比如支持 value 值使用 $ 占位符。

测试

完成了上面的实现,本地启动验证一下:

基本访问:http://192.168.1.12:8080/

 

ini

复制代码

信息: [Nginx] channelRead writeAndFlush start request=HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 0, cap: 0, components=0)) GET /favicon.ico HTTP/1.1 Host: 192.168.1.12:8080 Connection: keep-alive User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 Referer: http://192.168.1.12:8080/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 content-length: 0, id=40a5effffe257be0-00002a80-00000004-f834a6fd4eed4fe9-527bc66f 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: 命中普通前缀配置 requestUri=/favicon.ico, value=/ 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$args,value= 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$content_length,value=0 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$content_type,value=null 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$document_root,value=/D:/github/nginx4j/target/classes/ 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$host,value=192.168.1.12:8080 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$http_cookie,value= 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$remote_addr,value=192.168.1.12 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$remote_port,value=54511 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$request_method,value=GET 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$request_uri,value=/favicon.ico 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$schema,value=http 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$server_addr,value=192.168.1.12 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$server_name,value=192.168.1.12:8080 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$server_port,value=192.168.1.12:8080 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$server_protocol,value=HTTP/1.1 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$uri,value=/favicon.ico 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl info 信息: placeholder put key=$user_agent,value=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 六月 09, 2024 8:51:12 下午 com.github.houbb.log.integration.adaptors.commons.JakartaCommonsLoggingImpl warn 警告: 暂时不处理 set 指令对应的操作符替换,后续可考虑改进。 ...

这里的日志量还是比较多的,我们把级别还是从 INFO 调整为 debug 比较合理。

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值