百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Nginx/OpenResty详解,Nginx Lua编程,重定向与内部子请求

mhr18 2025-03-28 18:31 23 浏览 0 评论

重定向与内部子请求

Nginx的rewrite指令不仅可以在Nginx内部的server、location之间进行跳转,还可以进行外部链接的重定向。通过ngx_lua模块的Lua函数除了能实现Nginx的rewrite指令的功能之外,还能顺利完成内部子请求、并发子请求等复杂功能。

实战案例运行准备:本节涉及的配置文件为源码工程的nginxlua-demo.conf文件。在运行本节实例前需要修改启动脚本openrestystart.bat(或openresty-start.sh)中的PROJECT_CONF变量的值,将其改为nginx-lua-demo.conf,然后重启OpenRestry。

Nginx Lua内部重定向

ngx_lua模块可以实现Nginx的rewrite指令类似的功能,该模块提供了两个对应的API来实现重定向的功能,主要有:

(1)ngx.exec(uri,args?):内部重定向。

(2)ngx.redirect(uri,status?):外部重定向。

首先看第一个ngx.exec(uri,args?)内部重定向方法,其等价于下面的rewrite指令:

rewrite regrex replacement last;

下面是3个使用ngx.exec进行重定向的例子。

第一个例子是一个不带参数的重定向:

#重定向到/internal/sum
ngx.exec('/internal/sum');
第二个例子是一个使用字符串作为追加参数的重定向:
#重定向到/internal/sum?a=3&b=5,并且追加参数c=6
ngx.exec('/internal/sum?a=3&b=5', 'c=6');
第三个例子是一个使用Lua table作为追加参数的重定向:#重定向到/internal/sum,并且追加参数 ?a=3&b=5&c=6
ngx.exec('/internal/sum', {a=3, b=5,c=6});

下面是一个完整的ngx.exec重定向的演示例子,通过内部重定向完成3个参数的累加,具体代码如下:

 location /internal/sum {
 internal; #只允许内部调用
 content_by_lua_block {
 --通过ngx.var访问Nginx变量
 local arg_a = tonumber(ngx.var.arg_a);
 local arg_b = tonumber(ngx.var.arg_b);
 local arg_c = tonumber(ngx.var.arg_c);
 --3个参数值求和
 local sum = arg_a + arg_b+ arg_c;
 --输出结果
 ngx.say(arg_a, "+", arg_b, "+", arg_c, "=",sum);
 }
 }
 location /sum {
 content_by_lua_block {
 -- local res = ngx.exec("/internal/sum", 'a = 100&b=10&c=1');
 -- 内部重定向到/internal/sum
 return ngx.exec("/internal/sum", {a = 100, b = 10, c = 1});
 }
 }

以上代码处于nginx-lua-demo.conf文件中,修改后需重启OpenRestry,然后可以使用浏览器访问/sum,具体的访问结果如图8-15所示。

ngx.exec的使用需要注意以下两点:

(1)如果有args参数,参数可以是字符串的形式,也可以是Luatable的形式,代码如下:

ngx.exec("/internal/sum",'a=100&b=5'); --参数是字符串的形式
ngx.exec("/internal/sum", {a=100, b=5}); --参数是Lua table的形式

(2)该方法可能不会主动返回,因此建议在调用该方法时显式加上return,代码如下:

return ngx.exec(...)

Nginx Lua外部重定向

ngx_lua模块的外部重定向方法为ngx.redirect,它的语法格式如下:

ngx.redirect(uri, status?)

ngx.redirect外部重定向方法与ngx.exec内部重定向方法不同,外部重定向将通过客户端进行二次跳转,所以ngx.redirect方法会产生额外的网络流量,该方法的第二个参数为响应状态码,可以传递301/302/303/307/308重定向状态码。其中,301、302是HTTP 1.0协议定义的响应码,303、307、308是HTTP 1.1协议定义的响应码。

如果不指定status值,那么该方法默认的响应状态为302(
ngx.HTTP_MOVED_TEMPORARILY)临时重定向。下面是一个通过ngx.redirect方法与rewrite指令达到一模一样跳转效果的实例,代码如下:

 location /sum2 {
 content_by_lua_block {
 -- 外部重定向
 return ngx.redirect("/internal/sum?a=100&b=10&c=1");
 }
 }
 location /sum3 {
 rewrite ^/sum3 "/internal/sum?a=100&b=10&c=1" redirect;
 }

以上代码处于nginx-lua-demo.conf文件中,修改后需要重启OpenRestry,然后可以使用浏览器访问/sum2或者/sum3,具体的访问结果如图8-16所示。

如果指定status值为301,那么对应的常量为
ngx.HTTP_MOVED_PERMANENTLY永久重定向,对应到rewrite指令的标志位为permanent。下面的例子中,ngx.redirect方法与rewrite指令达到了一模一样的跳转效果,代码如下:

 location /sum4 {
 content_by_lua_block {
 -- 外部重定向
 return ngx.redirect("/internal/sum?a=100&b=10&c=1", ngx.HTTP_MOVED_PERMANENTLY);
 }
 }
 location /sum5{
 rewrite ^/sum5 "/internal/sum?a=100&b=10&c=1" permanent;
 }

由于通过浏览器访问时已经发生了二次跳转,因此它的“检查”面板已经查看不到跳转前链接(如/sum4、/sum5)的响应码,但是可以通过抓包工具查看。/sum5的响应码具体如图8-17所示。

下面有一个综合性的跳转演示实例,通过ngx.redirect方法与rewrite指令进行3种方式的外部跳转,跳转到博客园网站(www.cnblogs.com)。

具体的代码如下:

 #使用location指令后面的正则表达式进行URL后缀捕获
 location ~*/blog/(.*) {
 content_by_lua_block {
 --使用ngx.redirect方法进行外部重定向
 --博客URI为正则捕获组1
 return ngx.redirect("https://www.cnblogs.com/"..ngx.var[1]);
 }
}
 location ~*/blog1/*{
 #使用rewrite指令后面的正则表达式进行URL后缀捕获
 rewrite ^/blog1/(.*) $1 break;
 content_by_lua_block {
 --使用ngx.redirect方法进行外部重定向
 --博客URI为正则捕获组1
 return ngx.redirect("https://www.cnblogs.com/"..ngx.var[1]);
 }
 }
 location ~*/blog2/*{
 #使用rewrite指令进行外部重定向,并捕获博客URI
 rewrite ^/blog2/(.*) https://www.cnblogs.com/$1 redirect;
 }
 }

以疯狂创客圈社群的博客首页为例,外部跳转演示需要用到的4个地址,分别如下:

 #以下为疯狂创客圈社群的博客首页,原地址为
https://www.cnblogs.com/crazymakercircle/p/9904544.html
 #以下为 /blog/(.*) 配置块的二次跳转演示地址
http://nginx.server/blog/crazymakercircle/p/9904544.html
 #以下为 /blog1/*配置块的二次跳转演示地址
http://nginx.server/blog1/crazymakercircle/p/9904544.html
 #以下为 /blog2/*配置块的二次跳转演示地址
http://nginx.server/blog2/crazymakercircle/p/9904544.html

通过浏览器访问以上二次跳转演示地址(主机名nginx.server需要指向Nginx的IP),发现都能正常地跳转到原地址(疯狂创客圈社群的博客首页)。

以上代码中,通过location指令、rewrite指令进行了正则捕获,并使用ngx.var[捕获组编号]访问捕获到的捕获组,也就是博客地址的URI部分。

通过浏览器访问以上4个地址,最终的结果都为疯狂创客圈社群的博客首页,只是后面的3个经过了跳转而已。跳转的结果如图8-18所示。

ngx.redirect方法不会主动返回,因此建议在调用该方法时显式加上return,具体如下:

return ngx.redirect("https://www.cnblogs.com/"..ngx.var[1]);

ngx.location.capture子请求

Nginx子请求并非HTTP协议的标准实现,是Nginx特有的设计,主要是为了提高内部对单个客户端请求处理的并发能力。

如果某个客户端的请求(可以理解为主请求)访问了多个内部资源,为了提高效率,可以为每一个内部资源访问建立单个子请求,并让所有子请求同时进行。

子请求并不是由客户端直接发起的,它是由Nginx服务器在处理客户端请求时根据自身逻辑需要而内部建立的新请求。因此,子请求只在Nginx服务器内部进行处理,不会与客户端进行交互。

通常情况下,为保护子请求所定义的内部接口,会把这些接口设置为internal,防止外部直接访问。这么做的主要好处是可以让这个内部接口相对独立,不受外界干扰。

发起单个子请求,可以使用的Lua API为ngx.location.capture方法,它的格式如下:

ngx.location.capture (uri, options?)

capture方法的第二个参数options是一个table容器,用于设置子请求相关的选项,有如下可以设置的选项:

(1)method:子请求的方法,默认为ngx.HTTP_GET常量。

(2)body:传给子请求的请求体,仅限于string或nil。(3)args:传给子请求的请求参数,支持string或table。

(4)vars:传给子请求的变量表,仅限于table。

(5)ctx:父子请求共享的变量表table。

(6)copy_all_vars:复制所有变量给子请求。

(7)share_all_vars:父子请求共享所有变量。

(8)always_forward_body:用于设置是否转发请求体。

下面是一个综合性实例,包含两个请求接口,具体如下:

外部访问接口:/goods/detail/100?foo=bar。
内部访问接口:/internal/detail/100。

外部接口专供外部访问,在准备好必要的请求参数、上下文环境变量、请求体之后,调用内部访问接口获取执行结果,然后返回给客户端。外部接口的演示代码具体如下:

 #向外公开的请求
location ~ /goods/detail/([0-9]+) {
 set $goodsId $1; #将location的正则捕获组1赋值到变量 $goodsId
 set $var1 '';
 set $var2 '';
 content_by_lua_block {
 --解析body参数之前一定要先读取request body
 ngx.req.read_body();
 --组装uri
 local uri = "/internal/detail/".. ngx.var.goodsId;
 local request_method = ngx.var.request_method;
 --获取父请求的参数
 local args = ngx.req.get_uri_args();
 local shareCtx = {c1 = "v1", other = "other value"}
 local res = ngx.location.capture(uri,{
 method = ngx.HTTP_GET,
 args = args, --转发父请求的参数给子请求 body = 'customed request body',
 vars = {var1 = "value1", var2 = "value2"}, --传递的变量
 always_forward_body = true, --转发父请求的request body
 ctx = shareCtx, --共享给子请求的上下文table
 });
 ngx.say(" child res.status :", res.status);
 ngx.say(res.body);
 ngx.say("
shareCtx.c1 =", shareCtx.c1); } }

内部接口用于模拟上游的服务(如Java容器服务),外部客户端是不能直接访问内部接口的。内部接口的演示代码具体如下:

 #内部请求
 location ~ /internal/detail/([0-9]+) {
 internal; #此指令限制外部客户端是不能直接访问内部接口
 #将捕获组1的值放到自定义Nginx变量$goodsId中
 set $goodsId $1;
 content_by_lua_block {
 ngx.req.read_body();
 ngx.say(" 

child start: "); --访问父请求传递的参数 local args = ngx.req.get_uri_args() ngx.say(",
foo =", args.foo); --访问父请求传递的请求体 local data = ngx.req.get_body_data() ngx.say(",
data =", data); --访问Nginx定义的变量 ngx.say("
goodsId =", ngx.var.goodsId); --访问父请求传递的变量 ngx.say(",
var.var1 =", ngx.var.var1); --访问父请求传递的共享上下文,并修改其属性 ngx.say(",
ngx.ctx.c1 =", ngx.ctx.c1); ngx.say("
child end
"); ngx.ctx.c1 = "changed value by child"; } }

以上代码处于nginx-lua-demo.conf文件中,修改后需重启OpenRestry,然后可以使用浏览器访问/goods/detail/100?foo=bar,具体的访问结果如图8-19所示。

capture方法的第二个参数options是一个table容器,用于设置子请求的选项。options的method属性用于指定子请求的method类型,具体示例如下:

local res = ngx.location.capture(uri,{
 method = ngx.HTTP_PUT, --method为PUT类型的请求
 ...
});

method属性值只接收Nginx Lua内部定义的请求类型的常量,如ngx.HTTP_POST表示POST类型的请求,ngx.HTTP_GET表示GET类型的请求。

options的body属性指定子请求的请求体(仅接收字符串值),其请求体的内容仅限于string或nil,具体示例如下:

local res = ngx.location.capture(uri,{
 body = ' customed request body', --转发给子请求的请求体
 ...
});

options的args属性用于指定子请求的URI请求参数(可以是字符串或者Lua表容器),具体示例如下:

local res = ngx.location.capture(uri,{
 args = ngx.req.get_uri_args(), --将父请求的参数table转发给子请求
 ...
});

上面的例子假定了父请求的类型为HTTP GET,使用ngx.req.get_uri_args()获取父请求的参数列表,原样转发给子请求。

options的vars属性是一个Lua表容器,用于设置传递给子请求中的Nginx变量。具体示例如下:

...
set $var1 ''; #提前定义好变量
set $var2 ''; #提前定义好变量
content_by_lua_block {
 ...
 local res = ngx.location.capture(uri,{
 vars = { var1 = "value1", var2 = "value2"}, --传递的Nginx变量
 ...
 });
}

在通过vars向子请求中传递Nginx变量时,变量需要提前进行定义,否则将报出变量未定义的错误。

options的ctx上下文属性指定一个Lua表作为子请求的ngx.ctx表。

当然,可以直接将ctx属性值设置为当前请求的ngx.ctx上下文表。

options的ctx使用示例如下:

local c = {c1="v1",other="other value"}
local res = ngx.location.capture(uri,{
 ...
 ctx = c, --设置子请求的ngx.ctx上下文表
});

父请求如果修改了ctx表中的成员,那么子请求可以通过ngx.ctx获取;反过来,子请求也可以修改ngx.ctx中的成员,父请求可以通过ctx表获取。通过ctx属性值可以方便地让父请求和子请求进行上下文变量共享。

options的always_forward_body属性用于设置是否转发请求体。当设置为true时,父请求中的请求体request body将转发到子请求。

always_forward_body属性的使用示例如下:

local res = ngx.location.capture(uri,{
 method = ngx.HTTP_GET,
 always_forward_body = true, --转发父请求的request body
});

ngx.location.capture只能发起到当前Nginx服务器的内部路径的子请求,如果需要发起外部HTTP路径的子请求,就需要与location(或者upstream)反向代理配置配合实现。

ngx.location.capture_multi并发子请求

经过解耦之后,微服务架构将提供大量的细粒度接口,一次客户端(例如App、网页端)请求往往调用多个微服务接口才能获取到完整的页面内容。

这种场景下可以通过网关(如Nginx)进行上游接口合并。

在OpenResty中,
ngx.location.capture_multi可以用于上游接口合并的场景,该方法可以完成内部多个子请求和并发访问。它的格式如下:

ngx.location.capture_multi ({ {uri, options?}, {uri, options?}, ... })

capture_multi可以一次发送多个内部子请求,每一个子请求的参数使用方式与capture方法相同。调用capture_multi前可以把所有的子请求加入一个table容器表中,作为调用参数传入;capture_multi返回后可以将其结果再用花括号“{}”包装成一个table,方便后面的迭代处理。

下面是一个综合性实例,通过capture_multi方法一次并发地请求两个内部接口,具体代码如下:

 #发起两个子请求:一个是get;另一个是post
 location /capture_multi_demo {
 content_by_lua_block {
 local postBody = ngx.encode_args({post_k1 = 32, post_k2 = "post_v2"});
 local reqs = {};
 table.insert(reqs, { "/print_get_param", { args = "a=3&b=4" }});
 table.insert(reqs, { "/print_post_param",{ method = ngx.HTTP_POST, body = postBody}});
 --统一发并发请求,然后等待结果
 local resps = {ngx.location.capture_multi(reqs)};
 --迭代结果列表
 for i, res in ipairs(resps) do
 ngx.say(" child res.status :", res.status,"
"); ngx.say(" child res.body :", res.body,"

"); end } }

两个内部接口用于模拟上游的服务(如Java容器服务),客户端是不能直接访问内部接口的。两个内部接口的代码具体如下:

 #模拟上游接口一:输出get请求的参数
 location /print_get_param {
 internal;
 content_by_lua_block {
 ngx.say(" 

child start: "); local arg = ngx.req.get_uri_args() for k, v in pairs(arg) do ngx.say("
[GET ] key:", k, " v:", v) end ngx.say("
child end
"); } } #模拟上游接口二:输出post请求的参数 location /print_post_param { internal; content_by_lua_block { ngx.say("

child start: "); ngx.req.read_body() --解析body参数之前一定要先读取body local arg = ngx.req.get_post_args(); for k, v in pairs(arg) do ngx.say("
[POST] key:", k, " v:", v) end ngx.say("
child end
"); } }

两个内部接口的功能很简单,主要是为了获取请求参数(或者请求体),然后输出到客户端。以上代码处于nginx-lua-demo.conf文件中,修改后需要重启OpenRestry,然后可以使用浏览器访问外部接口/capture_multi_demo,具体的访问结果如图8-20所示。

在所有子请求终止之前,
ngx.location.capture_multi(...)函数不会返回。此函数的耗时是单个子请求的最长延迟,而不是所有子请求的耗时总和,因为所有子请求是并发执行的。

上面的例子中,利用
ngx.location.capture_multi(...)完成了两个子请求并行执行。当两个请求没有先后依赖时,这个方法可以极大地提高请求效率。如果两个请求各自需要500毫秒,顺序执行需要1000毫秒,那么通过并发子请求可以在500毫秒完成两个请求。

本文给大家讲解的内容是Nginx/OpenResty详解,Nginx Lua编程,重定向与内部子请求

  1. 下篇文章给大家讲解的是 Nginx/OpenResty详解,Nginx Lua编程, Nginx Lua操作Redis;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

相关推荐

Redis合集-使用benchmark性能测试

采用开源Redis的redis-benchmark工具进行压测,它是Redis官方的性能测试工具,可以有效地测试Redis服务的性能。本次测试使用Redis官方最新的代码进行编译,详情请参见Redis...

Java简历总被已读不回?面试挂到怀疑人生?这几点你可能真没做好

最近看了几十份简历,发现大部分人不是技术差,而是不会“卖自己”——一、简历死穴:你写的不是经验,是岗位说明书!反面教材:ד使用SpringBoot开发项目”ד负责用户模块功能实现”救命写法:...

redission YYDS(redission官网)

每天分享一个架构知识Redission是一个基于Redis的分布式Java锁框架,它提供了各种锁实现,包括可重入锁、公平锁、读写锁等。使用Redission可以方便地实现分布式锁。red...

从数据库行锁到分布式事务:电商库存防超卖的九重劫难与破局之道

2023年6月18日我们维护的电商平台在零点刚过3秒就遭遇了严重事故。监控大屏显示某爆款手机SKU_IPHONE13_PRO_MAX在库存仅剩500台时,订单系统却产生了1200笔有效订单。事故复盘发...

SpringBoot系列——实战11:接口幂等性的形而上思...

欢迎关注、点赞、收藏。幂等性不仅是一种技术需求,更是数字文明对确定性追求的体现。在充满不确定性的网络世界中,它为我们建立起可依赖的存在秩序,这或许正是技术哲学最深刻的价值所在。幂等性的本质困境在支付系...

如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享

如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享在高流量场景下。首先,我需要回忆一下常见的优化策略,比如负载均衡、缓存、数据库优化、微服务拆分这些。不过,可能还需要考虑用户的具体情况,比...

Java面试题: 项目开发中的有哪些成长?该如何回答

在Java面试中,当被问到“项目中的成长点”时,面试官不仅想了解你的技术能力,更希望看到你的问题解决能力、学习迭代意识以及对项目的深度思考。以下是回答的策略和示例,帮助你清晰、有说服力地展示成长点:一...

互联网大厂后端必看!Spring Boot 如何实现高并发抢券逻辑?

你有没有遇到过这样的情况?在电商大促时,系统上线了抢券活动,结果活动刚一开始,服务器就不堪重负,出现超卖、系统崩溃等问题。又或者用户疯狂点击抢券按钮,最后却被告知无券可抢,体验极差。作为互联网大厂的后...

每日一题 |10W QPS高并发限流方案设计(含真实代码)

面试场景还原面试官:“如果系统要承载10WQPS的高并发流量,你会如何设计限流方案?”你:“(稳住,我要从限流算法到分布式架构全盘分析)…”一、为什么需要限流?核心矛盾:系统资源(CPU/内存/数据...

Java面试题:服务雪崩如何解决?90%人栽了

服务雪崩是指微服务架构中,由于某个服务出现故障,导致故障在服务之间不断传递和扩散,最终造成整个系统崩溃的现象。以下是一些解决服务雪崩问题的常见方法:限流限制请求速率:通过限流算法(如令牌桶算法、漏桶算...

面试题官:高并发经验有吗,并发量多少,如何回复?

一、有实际高并发经验(建议结构)直接量化"在XX项目中,系统日活用户约XX万,核心接口峰值QPS达到XX,TPS处理能力为XX/秒。通过压力测试验证过XX并发线程下的稳定性。"技术方案...

瞬时流量高并发“保命指南”:这样做系统稳如泰山,老板跪求加薪

“系统崩了,用户骂了,年终奖飞了!”——这是多少程序员在瞬时大流量下的真实噩梦?双11秒杀、春运抢票、直播带货……每秒百万请求的冲击,你的代码扛得住吗?2025年了,为什么你的系统一遇高并发就“躺平”...

其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。

其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。比如上周有个小伙伴找我,五年经验但简历全是'参与系统设计''优化接口性能'这种空话。我就问他:你做的秒杀...

PHP技能评测(php等级考试)

公司出了一些自我评测的PHP题目,现将题目和答案记录于此,以方便记忆。1.魔术函数有哪些,分别在什么时候调用?__construct(),类的构造函数__destruct(),类的析构函数__cal...

你的简历在HR眼里是青铜还是王者?

你的简历在HR眼里是青铜还是王者?兄弟,简历投了100份没反应?面试总在第三轮被刷?别急着怀疑人生,你可能只是踩了这些"隐形求职雷"。帮3630+程序员改简历+面试指导和处理空窗期时间...

取消回复欢迎 发表评论: