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.newffi.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 级别的内存释放。
  • 请尽可能使用最新版本的 Luajitx86_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 stackupvalue,或者 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.metatypeffi.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)相关推荐

  1. 高性能web平台【OpenResty入门与实战】

    一.OpenResty概述 1 OpenResty 背景 随着宽带网络的快速普及和移动互联网的高速发展,网站需要为越来越多的用户提供服务,处理越来越多的并发请求,要求服务器必须具有很高的性能才能应对不 ...

  2. 又拍云叶靖:OpenResty 在又拍云存储中的应用

    2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,又拍云平台开发部负责人叶靖在活动上做了<OpenRest ...

  3. nginx限制了你的想象?那么请用openresty

    nginx 限制了你的想象?那么请用openresty 1. nginx应用及开发 2. openresty如何扩展nginx的功能 3. openresty实战案例讲解 [Linux服务器开发系列] ...

  4. 福禄科技罗宇翔:OpenResty 游戏反外挂应用

    2019 年 5 月 11 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙武汉站,福禄科技服务端研发工程师罗宇翔在活动上做了< OpenR ...

  5. apisix实际应用_Apache APISIX 的高性能实践

    2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,OpenResty 软件基金会联合创始人王院生在活动上做了&l ...

  6. apisix实际应用_OpenResty 社区王院生:APISIX 的高性能实践

    2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,OpenResty 软件基金会联合创始人王院生在活动上做了&l ...

  7. 3、查询性能优化技术之多级缓存

    5.1本章目标 5.2缓存设计原则概览 缓存设计原则: 用快速存取设备,用内存 将缓存推到离用户最近的地方 脏缓存清理 我们的项目采用多级缓存的架构 第一级 Redis缓存 Redis缓存有集中管理缓 ...

  8. 路由包含#号导致的nginx_分布式实战:Nginx缓存之OpenResty部署

    本文首发于Ressmix个人站点:https://www.tpvlog.com 经过前面几章的讲解,我已经通过代码实现了三级缓存架构中的JVM本地缓存和Redis分布式缓存.本章,我将讲解最后的一层- ...

  9. OpenResty 最佳实践学习--实战演习笔记(4)

    本篇简单记录openresty连接redis数据库和缓存的一些东西,也基本上是官网上的一些例子和知识,作为整理方便自己后续回顾! openresty连接redis 因为我本地服务器安装了redis,这 ...

最新文章

  1. python下载文件暂停恢复_python下载文件记录黑名单的实现代码
  2. linux关于ftp权限问题
  3. EOJ_1017_座位分配
  4. JS编程建议——2:正确辨析JavaScript句法中的词、句和段
  5. 基于 Blazui 的 Blazor 后台管理模板 BlazAdmin 正式尝鲜
  6. php查询mysql返回大量数据结果集导致内存溢出的解决方法
  7. 论文浅尝 | 主题驱动的分子图表示对比学习
  8. 信息学奥赛一本通 提高篇 第一部分 基础算法 第2章 二分与三分
  9. 5.7(财务应用程序:计算将来的程序)
  10. python玩跳一跳_python玩跳一跳
  11. Python隐形马尔科夫实战_通过Python的Networkx和Sklearn来介绍隐性马尔科夫模型
  12. 无人车之美——技术要点速览
  13. 董明珠揭示:未来2年这个行业盈利最大,马总点赞说,又要出富翁
  14. 硬件设计4---什么是电感、磁珠?
  15. 基因数据分析主流软件与基因预测方法步骤-搬运工
  16. 手机App开发的基础概念
  17. 实用干货!正规的问答推广平台有哪些及其优势
  18. 屏蔽百度搜索结果中的广告
  19. Google Maps API Key申请办法(最新)
  20. C语言,数组的类型,大小

热门文章

  1. 5G跃升激发数字经济新活力,体现了5G的巨大经济价值!
  2. android 关闭odex优化,[Android] 配置安卓模拟器,使得dex文件不被优化成odex
  3. python比赛积分类算法题_Python算法题(一)——青蛙跳台阶
  4. Laravel框架中使用Service模式
  5. @Service指定参数与不指定参数的细节问题
  6. 51Nod 欢乐手速场1 A Pinball[DP 线段树]
  7. android入门书籍-------第一行代码免费下载
  8. Android 从入门到进阶
  9. php实现防垃圾手机号注册功能(接入阿里云风险识别)
  10. js字符串加入千分号