极客时间已完结课程限时免费阅读

17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键

17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键-极客时间

17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键

讲述:温铭

时长13:13大小10.58M

你好,我是温铭。经过前面内容的铺垫后, 相信你已经对 OpenResty 的概念和如何学习它有了基本的认识。今天这节课,我们来看一下 OpenResty 如何处理终端请求和响应。
虽然 OpenResty 是基于 NGINX 的 Web 服务器,但它与 NGINX 却有本质的不同:NGINX 由静态的配置文件驱动,而 OpenResty 是由 Lua API 驱动的,所以能提供更多的灵活性和可编程性。
下面,就让我来带你领略 Lua API 带来的好处吧。

API 分类

首先我们要知道,OpenResty 的 API 主要分为下面几个大类:
处理请求和响应;
SSL 相关;
shared dict;
cosocket;
处理四层流量;
process 和 worker;
获取 NGINX 变量和配置;
字符串、时间、编解码等通用功能。
这里,我建议你同时打开 OpenResty 的 Lua API 文档,对照着其中的 API 列表 ,看看是否能和这个分类联系起来。
OpenResty 的 API 不仅仅存在于 lua-nginx-module 项目中,也存在于 lua-resty-core 项目中,比如 ngx.ssl、ngx.base64、ngx.errlog、ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore、ngx.ocsp 这些 API 。
而对于不在 lua-nginx-module 项目中的 API,你需要单独 require 才能使用。举个例子,比如你想使用 split 这个字符串分割函数,就需要按照下面的方法来调用:
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
当然,这可能会给你带来一个困惑:在 lua-nginx-module 项目中,明明有 ngx.re.sub、ngx.re.find 等好几个 ngx.re 开头的 API,为什么单单是 ngx.re.split 这个 API ,需要 require 后才能使用呢?
事实上,在前面 lua-resty-core 章节中,我们也提到过,OpenResty 新的 API 都是通过 FFI 的方式在 lua-rety-core 仓库中实现的,所以难免就会存在这种割裂感。自然,我也很期待 lua-nginx-module 和 lua-resty-core 这两个项目以后可以合并,彻底解决此类问题。

请求

接下来,我们具体了解下 OpenResty 是如何处理终端请求和响应的。先来看下处理请求的 API,不过,以 ngx.req 开头的 API 有 20 多个,该怎么下手呢?
我们知道,HTTP 请求报文由三部分组成:请求行、请求头和请求体,所以下面我就按照这三部分来对 API 做介绍。

请求行

首先是请求行,HTTP 的请求行中包含请求方法、URI 和 HTTP 协议版本。在 NGINX 中,你可以通过内置变量的方式,来获取其中的值;而在 OpenResty 中对应的则是 ngx.var.* 这个 API。我们来看两个例子。
$scheme 这个内置变量,在 NGINX 中代表协议的名字,是 “http” 或者 “https”;而在 OpenResty 中,你可以通过 ngx.var.scheme 来返回同样的值。
$request_method 代表的是请求的方法,“GET”、“POST” 等;而在 OpenResty 中,你可以通过 ngx.var. request_method 来返回同样的值。
至于完整的 NGINX 内置变量列表,你可以访问 NGINX 的官方文档来获取:http://nginx.org/en/docs/http/ngx_http_core_module.html#variables
那么问题就来了:既然可以通过ngx.var.* 这种返回变量值的方法,来得到请求行中的数据,为什么 OpenResty 还要单独提供针对请求行的 API 呢?
这其实是很多方面因素的综合考虑结果:
首先是对性能的考虑。ngx.var 的效率不高,不建议反复读取;
也有对程序友好的考虑,ngx.var 返回的是字符串,而非 Lua 对象,遇到获取 args 这种可能返回多个值的情况,就不好处理了;
另外是对灵活性的考虑,绝大部分的 ngx.var 是只读的,只有很少数的变量是可写的,比如 $argslimit_rate,可很多时候,我们会有修改 method、URI 和 args 的需求。
所以, OpenResty 提供了多个专门操作请求行的 API,它们可以对请求行进行改写,以便后续的重定向等操作。
我们先来看下,如何通过 API 来获取 HTTP 协议版本号。OpenResty 的 API ngx.req.http_version 和 NGINX 的 $server_protocol 变量的作用一样,都是返回 HTTP 协议的版本号。不过这个 API 的返回值是数字格式,而非字符串,可能的值是 2.0、1.0、1.1 和 0.9,如果结果不在这几个值的范围内,就会返回 nil。
再来看下获取请求行中的请求方法。刚才我们提到过,ngx.req.get_method 和 NGINX 的 $request_method 变量的作用、返回值一样,都是字符串格式的方法名。
但是,改写当前 HTTP 请求方法的 API,也就是 ngx.req.set_method,它接受的参数格式却并非字符串,而是内置的数字常量。比如,下面的代码,把请求方法改写为 POST:
ngx.req.set_method(ngx.HTTP_POST)
为了验证 ngx.HTTP_POST 这个内置常量,确实是数字而非字符串,你可以打印出它的值,看输出是否为 8:
$ resty -e 'print(ngx.HTTP_POST)'
这样一来,get 方法的返回值为字符串,而 set 方法的输入值却是数字,就很容易让你在写代码的时候想当然了。如果是 set 时候传值混淆的情况还好,API 会崩溃报出 500 的错误;但如果是下面这种判断逻辑的代码:
if (ngx.req.get_method() == ngx.HTTP_POST) then
-- do something
end
这种代码是可以正常运行的,不会报出任何错误,甚至在 code review 时也很难发现。不幸的是,我就犯过类似的错误,对此记忆犹新:当时已经经过了两轮 code review,还有不完整的测试案例尝试覆盖,然而,最终还是因为线上环境异常才追踪到了这里。
碰到这类情况,除了自己多小心,或者再多一层封装外,并没有什么有效的方法来解决。平常你在设计自己的业务 API 时,也可以多做一些这方面的考虑,尽量保持 get、set 方法的参数格式一致,即使这会牺牲一些性能。
另外,在改写请求行的方法中,还有 ngx.req.set_uringx.req.set_uri_args 这两个 API,可以用来改写 uri 和 args。我们来看下这个 NGINX 配置:
rewrite ^ /foo?a=3? break;
那么,如何用等价的 Lua API 来解决呢?答案就是下面这两行代码。
ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")
其实,如果你看过官方文档,就会发现 ngx.req.set_uri 还有第二个参数:jump,默认是 false。如果设置为 true,就等同于把 rewrite 指令的 flag 设置为 last,而非上面示例中的 break
不过,我个人并不喜欢 rewrite 指令的 flag 配置,看不懂也记不住,远没有代码来的直观和好维护。

请求头

再来看下和请求头有关的 API。我们知道,HTTP 的请求头是 key : value 格式的,比如:
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
在 OpenResty 中,你可以使用 ngx.req.get_headers 来解析和获取请求头,返回值的类型则是 table:
local h, err = ngx.req.get_headers()
if err == "truncated" then
-- one can choose to ignore or reject the current request here
end
for k, v in pairs(h) do
...
end
这里默认返回前 100 个 header,如果请求头超过了 100 个,就会返回 truncated 的错误信息,由开发者自己决定如何处理。你可能会好奇为什么会有这样的处理,这一点先留个悬念,在后面安全漏洞的章节中我会提到。
不过,需要注意的是,OpenResty 并没有提供获取某一个指定请求头的 API,也就是没有 ngx.req.header['host'] 这种形式。如果你有这样的需求,那就需要借助 NGINX 的变量 $http_xxx 来实现了,那么在 OpenResty 中,就是 ngx.var.http_xxx 这样的获取方式。
看完了获取请求头,我们再来看看应该如何改写和删除请求头,这两种操作的 API 其实都很直观:
ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")
当然,官方文档中也提到了其他方法来删除请求头,比如把 header 的值设置为 nil 等,但为了代码更加清晰的考虑,我还是推荐统一用 clear_header 来操作。

请求体

最后来看请求体。出于性能考虑,OpenResty 不会主动读取请求体的内容,除非你在 nginx.conf 中强制开启了 lua_need_request_body 指令。此外,对于比较大的请求体,OpenResty 会把内容保存在磁盘的临时文件中,所以读取请求体的完整流程是下面这样的:
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
local tmp_file = ngx.req.get_body_file()
-- io.open(tmp_file)
-- ...
end
这段代码中有读取磁盘文件的 IO 阻塞操作。你应该根据实际情况来调整 client_body_buffer_size 配置的大小(64 位系统下默认是 16 KB),尽量减少阻塞的操作;你也可以把 client_body_buffer_sizeclient_max_body_size 配置成一样的,完全在内存中来处理,当然,这取决于你内存的大小和处理的并发请求数。
另外,请求体也可以被改写,ngx.req.set_body_datangx.req.set_body_file 这两个 API,分别接受字符串和本地磁盘文件做为输入参数,来完成请求体的改写。不过,这类操作并不常见,你可以查看文档来获取更详细的内容。

响应

处理完请求后,我们就需要发送响应返回给客户端了。和请求报文一样,响应报文也由几个部分组成,即状态行、响应头和响应体。同样的,接下来我会按照这三部分来介绍相应的 API。

状态行

状态行中,我们主要关注的是状态码。在默认情况下,返回的 HTTP 状态码是 200,也就是 OpenResty 中内置的常量 ngx.HTTP_OK。但在代码的世界中,处理异常情况的代码总是占比最多的。
如果你检测了请求报文,发现这是一个恶意的请求,那么你需要终止请求:
ngx.exit(ngx.HTTP_BAD_REQUEST)
不过,OpenResty 的 HTTP 状态码中,有一个特别的常量:ngx.OK。当 ngx.exit(ngx.OK) 时,请求会退出当前处理阶段,进入下一个阶段,而不是直接返回给客户端。
当然,你也可以选择不退出,只使用 ngx.status 来改写状态码,比如下面这样的写法:
ngx.status = ngx.HTTP_FORBIDDEN
如果你想了解更多的状态码常量,可以从文档中查询到。

响应头

说到响应头,其实,你有两种方法来设置它。第一种是最简单的:
ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- 删除
这里的 ngx.header 保存了响应头的信息,可以读取、修改和删除。
第二种设置响应头的方法是 ngx_resp.add_header ,来自 lua-resty-core 仓库,它可以增加一个头信息,用下面的方法来调用:
local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")
与第一种方法的不同之处在于,add header 不会覆盖已经存在的同名字段。

响应体

最后看下响应体,在 OpenResty 中,你可以使用 ngx.sayngx.print 来输出响应体:
ngx.say('hello, world')
这两个 API 的功能是一致的,唯一的不同在于, ngx.say 会在最后多一个换行符。
为了避免字符串拼接的低效,ngx.say / ngx.print 不仅支持字符串作为参数,也支持数组格式:
$ resty -e 'ngx.say({"hello", ", ", "world"})'
hello, world
这样在 Lua 层面就跳过了字符串的拼接,把这个它不擅长的事情丢给了 C 函数去处理。

写在最后

到此,让我们回顾下今天的内容。我们按照请求报文和响应报文的内容,依次介绍了与之相关的 OpenResty API。你可以看得出来,和 NGINX 的指令相比,OpenResty API 更加灵活和强大。
那么,在你处理 HTTP 请求时,OpenResty 提供的 Lua API 是否足够满足你的需求呢?欢迎留言一起探讨,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 4

提建议

上一篇
16 | 秒杀大多数开发问题的两个利器:文档和测试案例
下一篇
18 | worker间的通信法宝:最重要的数据结构之shared dict
unpreview
 写留言

精选留言(10)

  • helloworld
    2019-07-08
    老师,文中的这句“OpenResty 的 HTTP 状态码中,有一个特别的常量:ngx.OK。当 ngx.exit(ngx.OK) 时,请求会退出当前处理阶段,进入下一个阶段,而不是直接返回给客户端。” ngx.OK应该不能算是HTTP状态码吧,它对应的值是0; 我下面的理解对不对: ngx.exit(ngx.OK)、ngx.exit(ngx.ERROR)和ngx.exit(ngx.DECLINED)时,请求会退出当前处理阶段,进入下一个阶段; 而当ngx.exit(ngx.HTTP_*)以ngx.HTTP_*各种HTTP状态码作为参数时,会直接响应给客户端。
    展开

    作者回复: ngx.ok 确实不是http状态码,它是 OpenResty 中的一个常量,值是0. ngx.exit 的官方文档正好可以解答你的问题: When status >= 200 (i.e., ngx.HTTP_OK and above), it will interrupt the execution of the current request and return status code to nginx. When status == 0 (i.e., ngx.OK), it will only quit the current phase handler (or the content handler if the content_by_lua* directive is used) and continue to run later phases (if any) for the current request. 不过,里面并没有提到对于ngx.exit(ngx.ERROR)和ngx.exit(ngx.DECLINED)是如何处理的,我们可以自己来做个测试: location /lua { rewrite_by_lua "ngx.exit(ngx.ERROR)"; echo hello; } 访问这个 location,可以看到 http 响应码为空,响应体也是空。并没有引入下一个执行阶段。

    10
  • HelloBug
    2019-07-03
    利用ngx.exit返回非200时,请求会退出当前请求处理阶段,然后直接返回给客户端。在测试的时候发现一个特点是无论ngx.exit返回什么应答码,log_by_lua*阶段都会执行。本来以为直接返回给客户端,不会执行log阶段的,因为log阶段也是处理流程中的一个阶段。其实log阶段是在返回给客户端之后才会执行的一个阶段是吧。仔细想来也应该是这样:)

    作者回复: 👍

    7
  • 小强
    2019-07-30
    老师,openresty 可以做api鉴权吗?现在项目中网关用的spring cloud gateway和jwt,只做鉴权和代理,性能较差,适合往openresty迁移吗?

    作者回复: 当然可以,现在流行的 API 网关不少都是基于 OpenResty 的。JWT 的可以参考这里:https://github.com/iresty/apisix/blob/master/doc/plugins/jwt-auth-cn.md

    2
  • Geek_c0ea3b
    2021-09-04
    老师,请问作为反向代理时,获取上游服务器的响应,一般是通过ngx.arg[1]这样吗?对于较大的响应,ngx.arg[1]可能需要获取多次,性能上可能不是最优。像一般软WAF在处理响应进行规制匹配时,是怎么获取响应的呢?
  • J.Smile
    2019-07-05
    看到这里,才觉得接近了实战。^_^
    1
  • 2xshu
    2019-07-05
    老师请教个 ngx.location.capture问题,看lua api官方文档如下: This API function (as well as ngx.location.capture_multi) always buffers the whole response body of the subrequest in memory. 想问下,这个缓存的response body,是parent request结束就会释放掉吗?有什么办法可以验证我的猜想吗?

    作者回复: 是的,在 OpenResty 中 Lua 层面的内存都是自动管理的。 是否可以使用反证法,如果在结束的时候没有释放,那么内存就是一直增长。用 wrk 或者 ab 发送几百个请求,然后看看内存的占用。

    1
  • psoracle
    2019-07-04
    一直没搞明白为什么luajit提供的shell命令行中不能使用local 本地变量,可以定义声明,但使用时还是尝试从global index中查找,所以找不到。 ![](http://wimgss.oss-cn-hangzhou.aliyuncs.com/2019-07-04-024647.png)

    作者回复: 官方 LuaJIT 并没有这个问题,OpenResty 带的 LuaJIT 确实会如此。我也不太清楚这么做的原因,可能是个 bug,你可以到https://github.com/openresty/luajit2提交 issue 询问下。

    共 3 条评论
    1
  • 小飞哥 ‍超級會員
    2019-07-03
    看到这里,已经不知道应该如何操作了。
  • psoracle
    2019-07-03
    如果在resty中熟悉处理http请求的api,如ngx.req.http_version,我是使用resty启动一个mini http server,代码中打印当前访问的http 信息,使用curl -XMETHOD来调用 ![](http://wimgss.oss-cn-hangzhou.aliyuncs.com/2019-07-03-034013.png)

    作者回复: content by lua 这个是 nginx 的指令,不能在 resty 里面调用

    共 3 条评论
  • manatee
    2019-07-03
    对于不同contenttype的请求体,有什么需要特别注意的吗

    作者回复: 并没有特别要注意的地方

    共 2 条评论