Openresty实战应用(2)
Openresty实战应用
- Lua函数
- 函数的定义
- 函数的参数
- 按值传递
- 变长参数
- 具名参数
- 按引用传递
- 函数的返回值
- 全动态函数调用
- 使用场景
- 牛刀小试
- Lua模块
- require函数
- Lua特别之处
- Lua下标从1开始
- 拼接字符串
- 只有table一种数据结构
- 默认全局变量
- FFI
- 简介
- ffi.* API
- ffi.cdef
- ffi.typeof
- ffi.new
- ffi.fill
- ffi.cast
- cdata 对象的垃圾回收
- 调用C函数
- 使用C数据结构
- 小心内存泄漏
- LuaJIT&Lua
- LuaJIT
- Lua&JIT
- LuaJIT特别之处
- 初步认识OpenResty
- OpenResty的发展
- OpenResty三大特性
- 详尽的文档和测试用例
- 同步非阻塞
- 动态
- OpenResty学习重点
- OpenResty安装部署
- Windows安装部署
- Linux安装部署
- MacOS安装部署
- Hello World
Lua函数
- 在 Lua 中,函数是一种对语句和表达式进行抽象的主要机制。函数既可以完成某项特定的任务,也可以只做一些计算并返回结果。在第一种情况中,一句函数调用被视为一条语句;而在第二种情况中,则将其视为一句表达式。
- 示例代码:
print("hello world!") -- 用 print() 函数输出 hello world!
local m = math.max(1, 5) -- 调用数学库函数 max,用来求 1,5 中的最大值,并返回赋给变量 m
- 使用函数的好处:
- 降低程序的复杂性:把函数作为一个独立的模块,写完函数后,只关心它的功能,而不再考虑函数里面的细节。
- 增加程序的可读性:当我们调用
math.max()
函数时,很明显函数是用于求最大值的,实现细节就不关心了。 - 避免重复代码:当程序中有相同的代码部分时,可以把这部分写成一个函数,通过调用函数来实现这部分代码的功能,节约空间,减少代码长度。
- 隐含局部变量:在函数中使用局部变量,变量的作用范围不会超出函数,这样它就不会给外界带来干扰。
函数的定义
- Lua 使用关键字 function 定义函数,语法如下:
function function_name (arc) -- arc 表示参数列表,函数的参数列表可以为空-- body
end
- 上面的语法定义了一个全局函数,名为
function_name
。全局函数本质上就是函数类型的值赋给了一个全局变量,即上面的语法等价于:
function_name = function (arc)-- body
end
- 由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上
local
修饰符:
local function function_name (arc)-- body
end
- 由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。
- 示例代码:
-- 定义函数 max,用来求两个数的最大值,并返回
local function max(a, b)-- 使用局部变量 temp,保存最大值local temp = nilif (a > b) thentemp = aelsetemp = bend-- 返回最大值return temp
end-- 调用函数 max,找去 -12 和 20 中的最大值
local m = max(-12, 20)
print(m) -- output: 20
- 如果参数列表为空,必须使用
()
表明是函数调用。示例代码:
local function func()--形参为空print("no parameter")
endfunc() --函数调用,圆扩号不能省
--> output:
no parameter
- 在定义函数时要注意几点:
- 利用名字来解释函数、变量的目的,使人通过名字就能看出来函数、变量的作用。
- 每个函数的长度要尽量控制在一个屏幕内,一眼可以看明白。
- 让代码自己说话,不需要注释最好。
- 由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如:
function foo.bar(a, b, c)-- body ...
end
- 此时我们是把一个函数类型的值赋给了
foo
表的bar
字段。换言之,上面的定义等价于:
foo.bar = function(a, b, c)print(a, b, c)
end
- 对于此种形式的函数定义,不能再使用
local
修饰符了,因为不存在定义新的局部变量了。
函数的参数
按值传递
- Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。
- 示例代码:
-- 定义函数swap,函数内部进行交换两个变量的值
local function swap(a, b)local temp = aa = bb = tempprint(a, b)
endlocal x = "hello"
local y = 20
print(x, y)
-- 调用swap函数,x和y的值并没有交换
print(swap(x, y))
print(x, y)-->output
hello 20
20 hello
hello 20
- 在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则是:若实参个数大于形参个数,从左向右,多余的实参被忽略;若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。
- 示例代码:
local function fun1(a, b)--两个形参,多余的实参被忽略掉print(a, b)
endlocal function fun2(a, b, c, d)--四个形参,没有被实参初始化的形参,用nil初始化print(a, b, c, d)
endlocal x = 1
local y = 2
local z = 3fun1(x, y, z) -- z被函数fun1忽略掉了,参数变成 x, y
fun2(x, y, z) -- 后面自动加上一个nil,参数变成 x, y, z, nil-->output
1 2
1 2 3 nil
变长参数
- 上面函数的参数都是固定的,其实 Lua 还支持变长参数。若形参为
...
,表示该函数可以接收不同长度的参数。访问参数的时候也要使用...
。 - 示例代码:
local function func(...)-- 形参为 ... ,表示函数采用变长参数local temp = { ... } -- 访问的时候也要使用 ...local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数-- 组内容使用 " " 拼接成字符串。print(ans)
endfunc(1, 2) -- 传递了两个参数
func(1, 2, 3, 4) -- 传递了四个参数-->output
1 2
1 2 3 4
- 值得一提的是,LuaJIT 2 尚不能 JIT 编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式。
具名参数
- Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。
- 示例代码:
local function change(arg)-- change 函数,改变长方形的长和宽,使其各增长一倍arg.width = arg.width * 2arg.height = arg.height * 2return arg
endlocal rectangle = { width = 20, height = 15 }
print("before change:", "width =", rectangle.width, "height = ", rectangle.height)
rectangle = change(rectangle)
print("after change:", "width =", rectangle.width, "height = ", rectangle.height)-->output
before change: width = 20 height = 15
after change: width = 40 height = 30
按引用传递
- 当函数参数是 table 类型时,传递进来的是实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效,而无需自己返回结果和让调用者进行赋值。 我们把上面改变长方形长和宽的例子修改一下。
- 示例代码:
function change(arg)--change函数,改变长方形的长和宽,使其各增长一倍--表arg不是表rectangle的拷贝,他们是同一个表arg.width = arg.width * 2arg.height = arg.height * 2-- 没有return语句了
endlocal rectangle = { width = 20, height = 15 }
print("before change:", "width = ", rectangle.width, "height = ", rectangle.height)
change(rectangle)
print("after change:", "width = ", rectangle.width, "height =", rectangle.height)
- 在常用基本类型中,除了 table 是按址传递类型外,其它的都是按值传递参数。 用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。
函数的返回值
- Lua 具有一项与众不同的特性,允许函数返回多个值。Lua 的库函数中,有一些就是返回多个值。
- 示例代码:使用库函数
string.find
,在源字符串中查找目标字符串,若查找成功,则返回目标字符串在源字符串中的起始位置和结束位置的下标。
local s, e = string.find("hello, world", "llo")print(s, e) -->output 3 5
- 返回多个值时,值之间用“,”隔开。
-- 定义一个函数,实现两个变量交换值
local function swap(a, b)-- 定义函数 swap,实现两个变量交换值return b, a -- 按相反顺序返回变量的值
endlocal x = 1
local y = 20
x, y = swap(x, y) -- 调用 swap 函数
print(x, y) --> output 20 1
- 当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。调整规则: 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉; 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。
- 示例代码:
function init()--init 函数 返回两个值 1 和 "lua"return 1, "lua"
endx = init()
print(x)x, y, z = init()
print(x, y, z)--output
1
1 lua nil
- 当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值,也就是第一个返回值。
local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
print(x, y, z) -->output 1 2 nillocal a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c) -->output 2 1 lua
- 函数调用的实参列表也是一个列表表达式。考虑下面的例子:
print(init(), 2) -->output 1 2
print(2, init()) -->output 2 1 lua
- 如果你确保只取函数返回值的第一个值,可以使用括号运算符,例如:
print((init()), 2) -->output 1 2
print(2, (init())) -->output 2 1
- 值得一提的是,如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。
全动态函数调用
- 调用回调函数,并把一个数组参数作为回调函数的参数。
local args = {...} or {}
method_name(unpack(args, 1, table.maxn(args)))
使用场景
- 如果你的实参 table 中确定没有 nil 空洞,则可以简化为:
method_name(unpack(args))
-- 你要调用的函数参数是未知的;
-- 函数的实际参数的类型和数目也都是未知的。
-- 伪代码
add_task(end_time, callback, params)if os.time() >= endTime thencallback(unpack(params, 1, table.maxn(params)))
end
- 值得一提的是,
unpack
内建函数还不能为 LuaJIT 所 JIT 编译,因此这种用法总是会被解释执行。对性能敏感的代码路径应避免这种用法。
牛刀小试
local function run(x, y)print('run', x, y)
endlocal function attack(targetId)print('targetId', targetId)
endlocal function do_action(method, ...)local args = {...} or {}method(unpack(args, 1, table.maxn(args)))
enddo_action(run, 1, 2) -- output: run 1 2
do_action(attack, 1111) -- output: targetId 1111
Lua模块
- 从 Lua 5.1 语言添加了对模块和包的支持。一个 Lua 模块的数据结构是用一个 Lua 值(通常是一个 Lua 表或者 Lua 函数)。一个 Lua 模块代码就是一个会返回这个 Lua 值的代码块。 可以使用内建函数
require()
来加载和缓存模块。简单的说,一个代码模块就是一个程序库,可以通过require
来加载。模块加载后的结果通常是一个 Lua table,这个表就像是一个命名空间,其内容就是模块中导出的所有东西,比如函数和变量。require
函数会返回 Lua 模块加载后的结果,即用于表示该 Lua 模块的 Lua 值。
require函数
- Lua 提供了一个名为
require
的函数用来加载模块。要加载一个模块,只需要简单地调用require
“file” 就可以了,file 指模块所在的文件名。这个调用会返回一个由模块函数组成的 table,并且还会定义一个包含该 table 的全局变量。 - 在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。相当于将导出的函数作为 table 的一个字段,在 Lua 中函数是第一类值,提供了天然的优势。
- 创建一个 lua 文件
my.lua
,内容如下:
local _M = {}local function get_name()return "Lucy"
endfunction _M.greeting()print("hello " .. get_name())
endreturn _M
- 再定义一个 lua 文件
main.lua
,调用上面的模块,内容如下:
local my = require("my")
my.greeting() -->output: hello Lucy
- 注意:对于需要导出给外部使用的公共模块,处于安全考虑,是要避免全局变量的出现。 我们可以使用 lj-releng 或 luacheck 工具完成全局变量的检测。
- 另一个要注意的是,由于在 LuaJIT 中,require 函数内不能进行上下文切换,所以不能够在模块的顶级上下文中调用 cosocket 一类的 API。 否则会报
attempt to yield across C-call boundary
错误。
Lua特别之处
Lua下标从1开始
- Lua 是我知道的唯一一个下标从 1 开始的编程语言。这一点,虽然对于非程序员背景的人来说更好理解,但却容易导致程序的 bug。
- 举个例子:
resty -e 't={100}; ngx.say(t[0])'
- 你自然期望打印出 100,或者报错说下标 0 不存在。但结果出乎意料,什么都没有打印出来,也没有报错。既然如此,让我们加上 type 命令,来看下输出到底是什么:
resty -e 't={100};ngx.say(type(t[0]))'
-- nil
- 原来是空值。事实上,在 OpenResty 中,对于空值的判断和处理也是一个容易让人迷惑的点。
拼接字符串
- 和大部分语言使用 + 不同,Lua 中使用两个点号来拼接字符串:
resty -e "ngx.say('hello' .. ', world')"
-- hello, world
- 在实际的项目开发中,我们一般都会使用多种开发语言,而Lua 这种不走寻常路的设计,总是会让开发者的思维,在字符串拼接的时候卡顿一下,也是让人哭笑不得。
只有table一种数据结构
- 不同于 Python 这种内置数据结构丰富的语言,Lua 中只有一种数据结构,那就是 table,它里面可以包括数组和哈希表:
local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
- 如果不显式地用_键值对_的方式赋值,table 就会默认用数字作为下标,从 1 开始。所以 color[1] 就是 blue。另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:
local t1 = { 1, 2, 3 }
print("Test1 " .. table.getn(t1))
local t2 = { 1, a = 2, 3 }
print("Test2 " .. table.getn(t2))
local t3 = { 1, nil }
print("Test3 " .. table.getn(t3))
local t4 = { 1, nil, 2 }
print("Test4 " .. table.getn(t4))
- 使用 resty 运行的结果如下
Test1 3
Test2 2
Test3 1
Test4 1
- 你可以看到,除了第一个返回长度为 3 的测试案例外,后面的测试都是我们预期之外的结果。事实上,想要在Lua 中获取 table 长度,必须注意到,只有在 table 是 序列 的时候,才能返回正确的值。
- 那什么是序列呢?首先序列是数组(array)的子集,也就是说,table 中的元素都可以用正整数下标访问到,不存在键值对的情况。对应到上面的代码中,除了 t2 外,其他的 table 都是 array。其次,序列中不包含空洞(hole),即 nil。综合这两点来看,上面的 table 中, t1 是一个序列,而 t3 和 t4 是 array,却不是序列(sequence)。
- 到这里,你可能还有一个疑问,为什么 t4 的长度会是 1 呢?其实这是因为,在遇到 nil 时,获取长度的逻辑就不继续往下运行,而是直接返回了。
默认全局变量
- 我想先强调一点,除非你相当确定,否则在 Lua 中声明变量时,前面都要加上 local。这是因为在 Lua 中,变量默认是全局的,会被放到名为 _G 的 table 中。不加 local 的变量会在全局表中查找,这是昂贵的操作。如果再加上一些变量名的拼写错误,就会造成难以定位的 bug。
- 所以,在 OpenResty 编程中,我强烈建议你总是使用 local 来声明变量,即使在 require module 的时候也是一样:
-- 推荐
local xxx = require('xxx')
-- 避免
require('xxx')
FFI
- FFI 库,是 LuaJIT 中最重要的一个扩展库。它允许从纯 Lua 代码调用外部 C 函数,使用 C 数据结构。有了它,就不用再像 Lua 标准
math
库一样,编写 Lua 扩展库。把开发者从开发 Lua 扩展 C 库(语言/功能绑定库)的繁重工作中释放出来。
简介
- 简单解释一下 Lua 扩展 C 库,对于那些能够被 Lua 调用的 C 函数来说,它的接口必须遵循 Lua 要求的形式,就是
typedef int (*lua_CFunction)(lua_State* L)
,这个函数包含的参数是lua_State
类型的指针 L 。可以通过这个指针进一步获取通过 Lua 代码传入的参数。这个函数的返回值类型是一个整型,表示返回值的数量。需要注意的是,用 C 编写的函数无法把返回值返回给 Lua 代码,而是通过虚拟栈来传递 Lua 和 C 之间的调用参数和返回值。不仅在编程上开发效率变低,而且性能上比不上 FFI 库调用 C 函数。 - FFI 库最大限度的省去了使用 C 手工编写繁重的
Lua/C
绑定的需要。不需要学习一门独立/额外的绑定语言——它解析普通 C 声明。这样可以从 C 头文件或参考手册中,直接剪切,粘贴。它的任务就是绑定很大的库,但不需要捣鼓脆弱的绑定生成器。 - FFI 紧紧的整合进了 LuaJIT(几乎不可能作为一个独立的模块)。
JIT
编译器在 C 数据结构上所产生的代码,等同于一个 C 编译器应该生产的代码。在JIT
编译过的代码中,调用 C 函数,可以被内连处理,不同于基于Lua/C API
函数调用。 - ffi库词汇:
noun | Explanation |
---|---|
cdecl | A definition of an abstract C type(actually, is a lua string) |
ctype | C type object |
cdata | C data object |
ct | C type format, is a template object, may be cdecl, cdata, ctype |
cb | callback object |
VLA | An array of variable length |
VLS | A structure of variable length |
ffi.* API
- **Lua ffi 库的 API,与 LuaJIT 不可分割。**毫无疑问,在
lua
文件中使用ffi
库的时候,必须要有下面的一行。
local ffi = require "ffi"
ffi.cdef
- 语法: ffi.cdef(def)
- 功能: 声明 C 函数或者 C 的数据结构,数据结构可以是结构体、枚举或者是联合体,函数可以是 C 标准函数,或者第三方库函数,也可以是自定义的函数,注意这里只是函数的声明,并不是函数的定义。声明的函数应该要和原来的函数保持一致。
ffi.cdef[[
typedef struct foo { int a, b; } foo_t; /* Declare a struct and typedef. */
int printf(const char *fmt, ...); /* Declare a typical printf function. */
]]
- 注意: 所有使用的库函数都要对其进行声明,这和我们写 C 语言时候引入 .h 头文件是一样的。
- 顺带一提的是,并不是所有的 C 标准函数都能满足我们的需求,那么如何使用 第三方库函数 或 自定义的函数 呢,这会稍微麻烦一点,不用担心,你可以很快学会。: ) 首先创建一个
myffi.c
,其内容是:
int add(int x, int y)
{return x + y;
}
- 接下来在 Linux 下生成动态链接库:
gcc -g -o libmyffi.so -fpic -shared myffi.c
- 为了方便我们测试,我们在
LD_LIBRARY_PATH
这个环境变量中加入了刚刚库所在的路径,因为编译器在查找动态库所在的路径的时候其中一个环节就是在LD_LIBRARY_PATH
这个环境变量中的所有路径进行查找。命令如下所示:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:your_lib_path
- 在 Lua 代码中要增加如下的行:
ffi.load(name [,global])
ffi.load
会通过给定的name
加载动态库,返回一个绑定到这个库符号的新的 C 库命名空间,在POSIX
系统中,如果global
被设置为ture
,这个库符号被加载到一个全局命名空间。另外这个name
可以是一个动态库的路径,那么会根据路径来查找,否则的话会在默认的搜索路径中去找动态库。在POSIX
系统中,如果在name
这个字段中没有写上点符号.
,那么.so
将会被自动添加进去,例如ffi.load("z")
会在默认的共享库搜寻路径中去查找libz.so
,在windows
系统,如果没有包含点号,那么.dll
会被自动加上。- 除此之外,还能使用
ffi.C
(调用ffi.cdef
中声明的系统函数) 来直接调用add
函数,记得要在ffi.load
的时候加上参数true
,例如ffi.load('myffi', true)
。 - 下面是一个完整的例子:
local ffi = require "ffi"
ffi.load('myffi', true)ffi.cdef [[
int add(int x, int y); /* don't forget to declare */
]]local res = ffi.C.add(1, 2)
print(res) -- output: 3 Note: please use luajit to run this script.
ffi.typeof
- 语法: ctype = ffi.typeof(ct)
- 功能: 创建一个 ctype 对象,会解析一个抽象的 C 类型定义。
local uintptr_t = ffi.typeof("uintptr_t")
local c_str_t = ffi.typeof("const char*")
local int_t = ffi.typeof("int")
local int_array_t = ffi.typeof("int[?]")
ffi.new
- 语法: cdata = ffi.new(ct [,nelem] [,init…])
- 功能: 开辟空间,第一个参数为 ctype 对象,ctype 对象最好通过 ctype = ffi.typeof(ct) 构建。
- 顺便一提,可能很多人会有疑问,到底
ffi.new
和ffi.C.malloc
有什么区别呢? - 如果使用
ffi.new
分配的cdata
对象指向的内存块是由垃圾回收器LuaJIT GC
自动管理的,所以不需要用户去释放内存。 - 如果使用
ffi.C.malloc
分配的空间便不再使用 LuaJIT 自己的分配器了,所以不是由LuaJIT GC
来管理的,但是,要注意的是ffi.C.malloc
返回的指针本身所对应的cdata
对象还是由LuaJIT GC
来管理的,也就是这个指针的cdata
对象指向的是用ffi.C.malloc
分配的内存空间。这个时候,你应该通过ffi.gc()
函数在这个 C 指针的cdata
对象上面注册自己的析构函数,这个析构函数里面你可以再调用ffi.C.free
,这样的话当 C 指针所对应的cdata
对象被Luajit GC
管理器垃圾回收时候,也会自动调用你注册的那个析构函数来执行 C 级别的内存释放。 - 请尽可能使用最新版本的
Luajit
,x86_64
上由LuaJIT GC
管理的内存已经由1G->2G
,虽然管理的内存变大了,但是如果要使用很大的内存,还是用ffi.C.malloc
来分配会比较好,避免耗尽了LuaJIT GC
管理内存的上限,不过还是建议不要一下子分配很大的内存。
local int_array_t = ffi.typeof("int[?]")
local bucket_v = ffi.new(int_array_t, bucket_sz)local queue_arr_type = ffi.typeof("lrucache_pureffi_queue_t[?]")
local q = ffi.new(queue_arr_type, size + 1)
ffi.fill
- 语法: ffi.fill(dst, len [,c])
- 功能: 填充数据,此函数和 memset(dst, c, len) 类似,注意参数的顺序。
ffi.fill(self.bucket_v, ffi_sizeof(int_t, bucket_sz), 0)
ffi.fill(q, ffi_sizeof(queue_type, size + 1), 0)
ffi.cast
- 语法: cdata = ffi.cast(ct, init)
- 功能: 创建一个 scalar cdata 对象。
local c_str_t = ffi.typeof("const char*")
local c_str = ffi.cast(c_str_t, str) -- 转换为指针地址local uintptr_t = ffi.typeof("uintptr_t")
tonumber(ffi.cast(uintptr_t, c_str)) -- 转换为数字
cdata 对象的垃圾回收
- 所有由显式的
ffi.new(), ffi.cast() etc.
或者隐式的accessors
所创建的cdata
对象都是能被垃圾回收的,当他们被使用的时候,你需要确保有在Lua stack
,upvalue
,或者Lua table
上保留有对cdata
对象的有效引用,一旦最后一个cdata
对象的有效引用失效了,那么垃圾回收器将自动释放内存(在下一个GC
周期结束时候)。另外如果你要分配一个cdata
数组给一个指针的话,你必须保持这个持有这个数据的cdata
对象活跃,下面给出一个官方的示例:
ffi.cdef[[
typedef struct { int *a; } foo_t;
]]local s = ffi.new("foo_t", ffi.new("int[10]")) -- WRONG!local a = ffi.new("int[10]") -- OK
local s = ffi.new("foo_t", a)
-- Now do something with 's', but keep 'a' alive until you're done.
- 相信看完上面的
API
你已经很累了,再坚持一下吧!休息几分钟后,让我们来看看下面对官方文档中的示例做剖析,希望能再加深你对ffi
的理解。
调用C函数
- 真的很容易去调用一个外部 C 库函数,示例代码:
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
- 以上操作步骤,如下:
- ① 加载 FFI 库。
- ② 为函数增加一个函数声明。这个包含在
中括号
对之间的部分,是标准 C 语法。 - ③ 调用命名的 C 函数——非常简单。
- 事实上,背后的实现远非如此简单:③ 使用标准 C 库的命名空间
ffi.C
。通过符号名printf
索引这个命名空间,自动绑定标准 C 库。索引结果是一个特殊类型的对象,当被调用时,执行printf
函数。传递给这个函数的参数,从 Lua 对象自动转换为相应的 C 类型。 - 再来一个源自官方的示例代码:
local ffi = require("ffi")
ffi.cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen,const uint8_t *source, unsigned long sourceLen, int level);
int uncompress(uint8_t *dest, unsigned long *destLen,const uint8_t *source, unsigned long sourceLen);
]]
local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")local function compress(txt)local n = zlib.compressBound(#txt)local buf = ffi.new("uint8_t[?]", n)local buflen = ffi.new("unsigned long[1]", n)local res = zlib.compress2(buf, buflen, txt, #txt, 9)assert(res == 0)return ffi.string(buf, buflen[0])
endlocal function uncompress(comp, n)local buf = ffi.new("uint8_t[?]", n)local buflen = ffi.new("unsigned long[1]", n)local res = zlib.uncompress(buf, buflen, comp, #comp)assert(res == 0)return ffi.string(buf, buflen[0])
end-- Simple test code.
local txt = string.rep("abcd", 1000)
print("Uncompressed size: ", #txt)
local c = compress(txt)
print("Compressed size: ", #c)
local txt2 = uncompress(c, #txt)
assert(txt2 == txt)
- 解释一下这段代码。我们首先使用
ffi.cdef
声明了一些被 zlib 库提供的 C 函数。然后加载 zlib 共享库,在 Windows 系统上,则需要我们手动从网上下载 zlib1.dll 文件,而在 POSIX 系统上 libz 库一般都会被预安装。因为ffi.load
函数会自动填补前缀和后缀,所以我们简单地使用 z 这个字母就可以加载了。我们检查ffi.os
,以确保我们传递给ffi.load
函数正确的名字。 - 一开始,压缩缓冲区的最大值被传递给
compressBound
函数,下一行代码分配了一个要压缩字符串长度的字节缓冲区。[?]
意味着他是一个变长数组。它的实际长度由ffi.new
函数的第二个参数指定。 - 我们仔细审视一下
compress2
函数的声明就会发现,目标长度是用指针传递的!这是因为我们要传递进去缓冲区的最大值,并且得到缓冲区实际被使用的大小。 - 在 C 语言中,我们可以传递变量地址。但因为在 Lua 中并没有地址相关的操作符,所以我们使用只有一个元素的数组来代替。我们先用最大缓冲区大小初始化这唯一一个元素,接下来就是很直观地调用
zlib.compress2
函数了。使用ffi.string
函数得到一个存储着压缩数据的 Lua 字符串,这个函数需要一个指向数据起始区的指针和实际长度。实际长度将会在buflen
这个数组中返回。因为压缩数据并不包括原始字符串的长度,所以我们要显式地传递进去。
使用C数据结构
cdata
类型用来将任意 C 数据保存在 Lua 变量中。这个类型相当于一块原生的内存,除了赋值和相同性判断,Lua 没有为之预定义任何操作。然而,通过使用metatable
(元表),程序员可以为cdata
自定义一组操作。cdata
不能在 Lua 中创建出来,也不能在 Lua 中修改。这样的操作只能通过 C API。这一点保证了宿主程序完全掌管其中的数据。- 我们将 C 语言类型与
metamethod
(元方法)关联起来,这个操作只用做一次。ffi.metatype
会返回一个该类型的构造函数。原始 C 类型也可以被用来创建数组,元方法会被自动地应用到每个元素。 - 尤其需要指出的是,
metatable
与 C 类型的关联是永久的,而且不允许被修改,__index
元方法也是。 - 下面是一个使用 C 数据结构的实例:
local ffi = require("ffi")
ffi.cdef[[
typedef struct { double x, y; } point_t;
]]local point
local mt = {__add = function(a, b) return point(a.x+b.x, a.y+b.y) end,__len = function(a) return math.sqrt(a.x*a.x + a.y*a.y) end,__index = {area = function(a) return a.x*a.x + a.y*a.y end,},
}
point = ffi.metatype("point_t", mt)local a = point(3, 4)
print(a.x, a.y) --> 3 4
print(#a) --> 5
print(a:area()) --> 25
local b = a + point(0.5, 8)
print(#b) --> 12.5
- 附表:Lua 与 C 语言语法对应关系
Idiom | C code | Lua code |
---|---|---|
Pointer dereference | x = *p | x = p[0] |
int *p | *p = y | p[0] = y |
Pointer indexing | x = p[i] | x = p[i] |
int i, *p | p[i+1] = y | p[i+1] = y |
Array indexing | x = a[i] | x = a[i] |
int i, a[] | a[i+1] = y | a[i+1] = y |
struct/union dereference | x = s.field | x = s.field |
struct foo s | s.field = y | s.field = y |
struct/union pointer deref | x = sp->field | x = sp.field |
struct foo *sp | sp->field = y | s.field = y |
int i, *p | y = p - i | y = p - i |
Pointer dereference | x = p1 - p2 | x = p1 - p2 |
Array element pointer | x = &a[i] | x = a + i |
小心内存泄漏
- 所谓“能力越大,责任越大”,FFI 库在允许我们调用 C 函数的同时,也把内存管理的重担压到我们的肩上。 还好 FFI 库提供了很好用的
ffi.gc
方法。该方法允许给 cdata 对象注册在 GC 时调用的回调,它能让你在 Lua 领域里完成 C 手工释放资源的事。 - C++ 提倡用一种叫 RAII 的方式管理你的资源。简单地说,就是创建对象时获取,销毁对象时释放。我们可以在 LuaJIT 的 FFI 里借鉴同样的做法,在调用
resource = ffi.C.xx_create
等申请资源的函数之后,立即补上一行ffi.gc(resource, ...)
来注册释放资源的函数。尽量避免尝试手动释放资源!即使不考虑error
对执行路径的影响,在每个出口都补上一模一样的逻辑会够你受的(用goto
也差不多,只是稍稍好一点)。 - 有些时候,
ffi.C.xx_create
返回的不是具体的 cdata,而是整型的 handle。这会儿需要用ffi.metatype
把ffi.gc
包装一下:
local resource_type = ffi.metatype("struct {int handle;}", {__gc = free_resource
})local function free_resource(handle)...
endresource = ffi.new(resource_type)
resource.handle = ffi.C.xx_create()
- 如果你没能把申请资源和释放资源的步骤放一起,那么内存泄露多半会在前方等你。写代码的时候切记这一点。
LuaJIT&Lua
LuaJIT
- OpenResty 的另一块基石:LuaJIT。
- 当然,在 OpenResty 中,写出正确的 LuaJIT 代码的门槛并不高,但要写出高效的 LuaJIT 代码绝非易事。
- OpenResty 的 worker 进程都是 fork master 进程而得到的, 其实, master 进程中的LuaJIT 虚拟机也会一起 fork 过来。在同一个 worker 内的所有协程,都会共享这个 LuaJIT 虚拟机,Lua 代码的执行也是在这个虚拟机中完成的。
Lua&JIT
- 先把重要的事情放在前面说:标准 Lua 和 LuaJIT 是两回事儿,LuaJIT 只是兼容了 Lua 5.1 的语法。
- 标准 Lua 现在的最新版本是 5.3,LuaJIT 的最新版本则是 2.1.0-beta3。在 OpenResty 几年前的老版本中,编译的时候,你可以选择使用标准 Lua VM ,或者 LuaJIT VM 来作为执行环境,不过,现在已经去掉了对标准 Lua 的支持,只支持 LuaJIT。
- LuaJIT 的语法兼容 Lua 5.1,并对 Lua 5.2 和 5.3 做了选择性支持。所以我们应该先学习 Lua 5.1 的语法,并在此基础上学习 LuaJIT 的特性。
LuaJIT特别之处
- 明白了Lua这四点特别之处,我们继续来说LuaJIT。除了兼容 Lua 5.1 的语法并支持 JIT 外,LuaJIT 还紧密结合了 FFI(Foreign Function Interface),可以让你直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。
- 例如:
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
- 短短这几行代码,就可以直接在 Lua 中调用 C 的 printf 函数,打印出 Hello world!。你可以使用resty 命令来运行它,看下是否成功。
- 类似的,我们可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。实际上,FFI 方式比传统的 Lua/C API 方式的性能更优,这也是 lua-resty-core 项目存在的意义。
初步认识OpenResty
OpenResty的发展
- OpenResty 并不像其他的开发语言一样从零开始搭建,而是基于成熟的开源组件——NGINX 和 LuaJIT。
- OpenResty 诞生于 2007 年,不过,它的第一个版本并没有选择 Lua,而是用了 Perl,这跟作者章亦春的技术偏好有很大关系。
- 但 Perl 的性能远远不能达到要求,于是,在第二个版本中,Perl 就被 Lua 给替换了。 不过, 在 OpenResty 官方的项目中,Perl 依然占据着重要的角色,OpenResty 工程化方面都是用 Perl 来构建,比如测试框架、Linter、CLI 等,后面我们也会逐步介绍。
- 后来,章亦春离开了淘宝,加入了美国的 CDN 公司 Cloudflare。因为 OpenResty 高性能和动态的优势很适合 CDN 的业务需求,很快 OpenResty 就成为 CDN 的技术标准。通过丰富的 lua-resty 库,OpenResty 开始逐渐摆脱 NGINX 的影子,形成自己的生态体系,在 API 网关、软WAF 等领域被广泛使用。
- OpenResty 是一个被广泛使用的技术,但它并不能算得上是热门技术,这听上去有点矛盾,到底什么意思呢?
- 说它应用广,是因为 OpenResty 现在是全球排名第五的 Web 服务器。我们经常用到的 12306 的余票查询功能,或者是京东的商品详情页,这些高流量的背后,其实都是 OpenResty 在默默地提供服务。
- 说它并不热门,那是因为使用 OpenResty 来构建业务系统的比例并不高。使用者大都用 OpenResty 来处理入口流量,并没有深入到业务里面去,自然,对于 OpenResty 的使用也是浅尝辄止,满足当前的需求就可以了。这当然也与 OpenResty 没有像 Java、Python 那样有成熟的 Web 框架和生态有关。
OpenResty三大特性
详尽的文档和测试用例
- 没错,文档和测试是判断开源项目是否靠谱的关键指标,甚至是排在代码质量和性能之前的。
- OpenResty 的文档非常详细,作者把每一个需要注意的点都写在了文档中。绝大部分时候,我们只需要仔细查看文档,就能解决遇到的问题,而不用谷歌搜索或者是跟踪到源码中。为了方便起见,OpenResty 还自带了一个命令行工具
restydoc
,专门用来帮助你通过 shell 查看文档,避免编码过程被打断。 - 不过,文档中只会有一两个通用的代码片段,并没有完整和复杂的示例,到哪里可以找到这样的例子呢?
- 对于 OpenResty 来说,自然是/t目录,它里面就是所有的测试案例。每一个测试案例都包含完整的 NGINX 配置和 Lua 代码,以及测试的输入数据和预期的输出数据。不过,OpenResty 使用的测试框架,与其他断言风格的测试框架完全不同。
同步非阻塞
- 协程,是很多脚本语言为了提升性能,在近几年新增的特性。但它们实现得并不完美,有些是语法糖,有些还需要显式的关键字声明。
- OpenResty 则没有历史包袱,在诞生之初就支持了协程,并基于此实现了 同步非阻塞的编程模式。这一点是很重要的,毕竟,程序员也是人,代码应该更符合人的思维习惯。显式的回调和异步关键字会打断思路,也给调试带来了困难。
- 这里我解释一下,什么是同步非阻塞。先说同步,这个很简单,就是按照代码来顺序执行。比如下面这段伪:
local res, err = query-mysql(sql)
local value, err = query-redis(key)
- 在同一请求连接中,如果要等 MySQL 的查询结果返回后,才能继续去查询 Redis,那就是同步;如果不用等 MySQL 的返回,就能继续往下走,去查询 Redis,那就是异步。对于 OpenResty 来说,绝大部分都是同步操作,只有 ngx.timer 这种后台定时器相关的 API,才是异步操作。
- 再来说说非阻塞,这是一个很容易和“异步”混淆的概念。这里我们说的“阻塞”,特指阻塞操作系统线程。我们继续看上面的例子,假设查询 MySQL 需要1s 的时间,如果在这1s 内,操作系统的资源(CPU)是空闲着并傻傻地等待返回,那就是阻塞;如果 CPU 趁机去处理其他连接的请求,那就是非阻塞。非阻塞也是 C10K、C100K 这些高并发能够实现的关键。
- 同步非阻塞这个概念很重要,建议你仔细琢磨一下。我认为,这一概念最好不要通过类比来理解,因为不恰当的类比,很可能把你搞得更糊涂。
- 在 OpenResty 中,上面的伪码就可以直接实现同步非阻塞,而不用任何显式的关键字。这里也再次体现了,让开发者用起来更简单,是 OpenResty 的理念之一。
动态
- OpenResty 有一个非常大的优势,并且还没有被充分挖掘,就是它的 动态。
- 传统的 Web 服务器,比如 NGINX,如果发生任何的变动,都需要你去修改磁盘上的配置文件,然后重新加载才能生效,这也是因为它们并没有提供 API,来控制运行时的行为。所以,在需要频繁变动的微服务领域,NGINX 虽然有多次尝试,但毫无建树。而异军突起的 Envoy, 正是凭着 xDS 这种动态控制的 API,大有对 NGINX 造成降维攻击的威胁。
- 和 NGINX 、 Envoy 不同的是,OpenResty 是由脚本语言 Lua 来控制逻辑的,而动态,便是 Lua 天生的优势。通过 OpenResty 中 lua-nginx-module 模块中提供的 Lua API,我们可以动态地控制路由、上游、SSL 证书、请求、响应等。甚至更进一步,你可以在不重启 OpenResty 的前提下,修改业务的处理逻辑,并不局限于 OpenResty 提供的 Lua API。
- 这里有一个很合适的类比,可以帮你理解上面关于动态的说明。你可以把 Web 服务器当做是一个正在高速公路上飞驰的汽车,NGINX 需要停车才能更换轮胎,更换车漆颜色;Envoy 可以一边跑一边换轮胎和颜色;而 OpenResty 除了具备前者能力外,还可以在不停车的情况下,直接把汽车从 SUV 变成跑车。
- 显然,掌握这种“逆天”的能力后,OpenResty 的能力圈和想象力就扩展到了其他领域,比如 Serverless和边缘计算等。
OpenResty学习重点
- 讲了这么多OpenResty的重点特性,你又该怎么学呢?我认为,学习需要抓重点,围绕主线来展开,而不是眉毛胡子一把抓,这样,你才能构建出脉络清晰的知识体系。
- 要知道,不管多么全面的课程,都不可能覆盖所有问题,更不能直接帮你解决线上的每个 bug 和异常。
- 回到OpenResty的学习,在我看来,想要学好 OpenResty,你必须理解下面几个重点:
- 同步非阻塞的编程模式;
- lua使用方法;
- 性能优化。
OpenResty安装部署
Windows安装部署
- 下载 Windows 版的 OpenResty 压缩包,github地址:https://github.com/LomoX-Offical/nginx-openresty-windows,这里我下载的是Openresty_For_Windows_1.13.5.1001_64Bit
https://github.com/LomoX-Offical/nginx-openresty-windows/releases/download/1.13.5.1001/Openresty_For_Windows_1.13.5.1001_64Bit.zip
- 安装:解压到要安装的目录,进入到 openresty 解压的根目录,双击执行 nginx.exe 或者使用命令 start nginx 启动 nginx,如果没有错误现在 nginx 已经开始运行了。
- 验证 nginx 是否成功启动:
tasklist /fi "imagename eq nginx.exe"
- 在浏览器的地址栏输入 localhost,加载 nginx 的欢迎页面。成功加载说明 nginx 正在运行。如下图:
- 另外当 nginx 成功启动后,master 进程的 pid 存放在
logs\nginx.pid
文件中。
Linux安装部署
- 官网地址:http://openresty.org
- centos 安装 openresty:https://openresty.org/en/linux-packages.html#centos
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
sudo yum check-update
sudo yum install openresty
sudo yum install openresty-resty# openresty -v
nginx version: openresty/1.19.3.1# find / -name openresty
/var/lib/yum/repos/x86_64/7/openresty
/var/cache/yum/x86_64/7/openresty
/usr/bin/openresty
/usr/local/openresty
/usr/local/openresty/bin/openresty# find / -name nginx.conf
/usr/local/openresty/nginx/conf/nginx.conf
- 列出所有 openresty 相关安装包:
yum --disablerepo="*" --enablerepo="openresty" list available
- 启动OpenResty
MacOS安装部署
# 如果已经从 `homebrew/nginx` 安装了OpenResty,首先执行如下命令
brew untap homebrew/nginx
# 安装openresty
brew tap openresty/brew
brew install openresty
# 开发环境,可以按照debug包
brew install openresty-debug# find / -name nginx.conf
/usr/local/etc/openresty/nginx.conf
Hello World
- 使用安装目录下resty来进行字符串的输出:
# ./resty -e "ngx.say('Hello World')"
Hello World
- 使用content_by_lua来引入lua 的使用方式:
# nginx.conf配置文件中使用此配置即可
location /lua {default_type text/html;content_by_lua 'ngx.say("hello lua!!")';
}
- 把lua代码从nginx.conf里面抽取出来,保持代码的可读性和可维护性:
# 编写lua脚本
# cat ../lua/01.lua
ngx.say("Hello World!!")
# 请求转发
location /lua1 {default_type text/html;# 注意这里的lua/01.lua文件是相对于openresty安装的根目录,即 /usr/local/openresty/nginx/lua/01.luacontent_by_lua_file lua/01.lua;
}# cat ../lua/02.lua
local args = ngx.reg.get_uri_args()
ngx.say("Hello OpenResty! Lua is so easy!==="..args.id)
# 请求转发
location /lua2 {content_by_lua_file lua/02.lua;
}cat ../lua/03.lua
ngx.exec('/seckill/goods/detail/1');
# 请求转发
location /lua3 {content_by_lua_file lua/03.lua;
}
Openresty实战应用(2)相关推荐
- 高性能web平台【OpenResty入门与实战】
一.OpenResty概述 1 OpenResty 背景 随着宽带网络的快速普及和移动互联网的高速发展,网站需要为越来越多的用户提供服务,处理越来越多的并发请求,要求服务器必须具有很高的性能才能应对不 ...
- 又拍云叶靖:OpenResty 在又拍云存储中的应用
2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,又拍云平台开发部负责人叶靖在活动上做了<OpenRest ...
- nginx限制了你的想象?那么请用openresty
nginx 限制了你的想象?那么请用openresty 1. nginx应用及开发 2. openresty如何扩展nginx的功能 3. openresty实战案例讲解 [Linux服务器开发系列] ...
- 福禄科技罗宇翔:OpenResty 游戏反外挂应用
2019 年 5 月 11 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙武汉站,福禄科技服务端研发工程师罗宇翔在活动上做了< OpenR ...
- apisix实际应用_Apache APISIX 的高性能实践
2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,OpenResty 软件基金会联合创始人王院生在活动上做了&l ...
- apisix实际应用_OpenResty 社区王院生:APISIX 的高性能实践
2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,OpenResty 软件基金会联合创始人王院生在活动上做了&l ...
- 3、查询性能优化技术之多级缓存
5.1本章目标 5.2缓存设计原则概览 缓存设计原则: 用快速存取设备,用内存 将缓存推到离用户最近的地方 脏缓存清理 我们的项目采用多级缓存的架构 第一级 Redis缓存 Redis缓存有集中管理缓 ...
- 路由包含#号导致的nginx_分布式实战:Nginx缓存之OpenResty部署
本文首发于Ressmix个人站点:https://www.tpvlog.com 经过前面几章的讲解,我已经通过代码实现了三级缓存架构中的JVM本地缓存和Redis分布式缓存.本章,我将讲解最后的一层- ...
- OpenResty 最佳实践学习--实战演习笔记(4)
本篇简单记录openresty连接redis数据库和缓存的一些东西,也基本上是官网上的一些例子和知识,作为整理方便自己后续回顾! openresty连接redis 因为我本地服务器安装了redis,这 ...
最新文章
- python下载文件暂停恢复_python下载文件记录黑名单的实现代码
- linux关于ftp权限问题
- EOJ_1017_座位分配
- JS编程建议——2:正确辨析JavaScript句法中的词、句和段
- 基于 Blazui 的 Blazor 后台管理模板 BlazAdmin 正式尝鲜
- php查询mysql返回大量数据结果集导致内存溢出的解决方法
- 论文浅尝 | 主题驱动的分子图表示对比学习
- 信息学奥赛一本通 提高篇 第一部分 基础算法 第2章 二分与三分
- 5.7(财务应用程序:计算将来的程序)
- python玩跳一跳_python玩跳一跳
- Python隐形马尔科夫实战_通过Python的Networkx和Sklearn来介绍隐性马尔科夫模型
- 无人车之美——技术要点速览
- 董明珠揭示:未来2年这个行业盈利最大,马总点赞说,又要出富翁
- 硬件设计4---什么是电感、磁珠?
- 基因数据分析主流软件与基因预测方法步骤-搬运工
- 手机App开发的基础概念
- 实用干货!正规的问答推广平台有哪些及其优势
- 屏蔽百度搜索结果中的广告
- Google Maps API Key申请办法(最新)
- C语言,数组的类型,大小
热门文章
- 5G跃升激发数字经济新活力,体现了5G的巨大经济价值!
- android 关闭odex优化,[Android] 配置安卓模拟器,使得dex文件不被优化成odex
- python比赛积分类算法题_Python算法题(一)——青蛙跳台阶
- Laravel框架中使用Service模式
- @Service指定参数与不指定参数的细节问题
- 51Nod 欢乐手速场1 A Pinball[DP 线段树]
- android入门书籍-------第一行代码免费下载
- Android 从入门到进阶
- php实现防垃圾手机号注册功能(接入阿里云风险识别)
- js字符串加入千分号