OpenResty 高性能网关开发教程 / 第 04 章 - Lua 语言基础
第 04 章 - Lua 语言基础
4.1 为什么是 Lua?
OpenResty 选择 Lua 作为嵌入语言有以下原因:
| 特性 | 说明 |
|---|---|
| 极轻量 | LuaJIT 运行时 < 1MB,启动纳秒级 |
| 快速 FFI | 直接调用 C 函数,无需绑定层 |
| 低 GC 停顿 | 增量式 GC,停顿微秒级 |
| JIT 编译 | LuaJIT 将热点代码编译为机器码 |
| 协程支持 | 原生协程,配合 OpenResty 实现同步风格的异步编程 |
| 简单易学 | 语法精简,几小时可上手 |
4.2 基本语法
4.2.1 变量与类型
-- 全局变量(不需要声明,直接赋值)
name = "OpenResty"
version = 1.25
is_stable = true
-- 局部变量(推荐使用 local,性能更好且作用域更小)
local port = 8080
local host = "localhost"
local enabled = true
-- nil 表示"无值"
local result = nil -- 等同于未定义
-- 类型检查
print(type(name)) -- "string"
print(type(version)) -- "number"
print(type(is_stable)) -- "boolean"
print(type(result)) -- "nil"
-- 字符串操作
local str = "Hello, OpenResty!"
print(string.len(str)) -- 18
print(string.upper(str)) -- "HELLO, OPENRESTY!"
print(string.find(str, "Open")) -- 8 11
print(string.sub(str, 1, 5)) -- "Hello"
print(string.format("Port: %d", port)) -- "Port: 8080"
-- 多行字符串(Lua 特有)
local template = [[
<html>
<head><title>Gateway</title></head>
<body><h1>Welcome</h1></body>
</html>
]]
注意:Lua 中只有
false和nil是假值,其他所有值(包括0和"")都是真值。这与 Python 不同。
4.2.2 数字与精度
-- Lua 5.1 的数字都是双精度浮点数
local a = 10
local b = 3.14
-- LuaJIT 支持整数模式
local big_int = 9007199254740991 -- 安全整数范围
-- 位运算(LuaJIT 扩展)
local bit = require "bit"
local flags = bit.bor(0x01, 0x02) -- 按位或: 3
local masked = bit.band(flags, 0x01) -- 按位与: 1
local shifted = bit.lshift(1, 10) -- 左移: 1024
4.2.3 控制结构
-- if-elseif-else
local status = 200
if status == 200 then
print("OK")
elseif status == 301 then
print("Moved Permanently")
elseif status == 404 then
print("Not Found")
else
print("Status: " .. status)
end
-- 三元运算符模拟(Lua 没有三元运算符)
local msg = (status == 200) and "OK" or "Error"
-- for 循环(数值型)
for i = 1, 10 do
print(i)
end
for i = 10, 1, -1 do -- 递减
print(i)
end
-- for 循环(泛型,遍历表)
local config = {host = "localhost", port = 8080, debug = true}
for k, v in pairs(config) do
print(k .. " = " .. tostring(v))
end
-- while 循环
local retry = 0
while retry < 3 do
retry = retry + 1
-- 执行重试逻辑
end
-- repeat-until(至少执行一次)
local attempts = 0
repeat
attempts = attempts + 1
until attempts >= 3
4.3 表(Table):Lua 的核心数据结构
Table 是 Lua 中唯一的数据结构,可以同时作为数组、字典、对象使用。
4.3.1 数组模式
-- 数组(索引从 1 开始,这是 Lua 的约定)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple"(不是 fruits[0])
print(#fruits) -- 3(长度操作符)
-- 添加元素
table.insert(fruits, "date") -- 追加到末尾
table.insert(fruits, 2, "avocado") -- 插入到位置 2
-- 删除元素
table.remove(fruits, 1) -- 删除第一个元素
-- 遍历数组(推荐 ipairs)
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
4.3.2 字典模式
-- 字典(键值对)
local user = {
id = 12345,
name = "张三",
email = "zhangsan@example.com",
roles = {"admin", "user"},
}
-- 访问
print(user.name) -- "张三"
print(user["email"]) -- "zhangsan@example.com"
-- 动态键
local key = "id"
print(user[key]) -- 12345
-- 添加/修改
user.last_login = os.time()
user.email = "new@example.com"
-- 删除
user.email = nil
-- 遍历字典(使用 pairs)
for k, v in pairs(user) do
print(k, ":", v)
end
4.3.3 混合模式
-- 同时作为数组和字典
local config = {
-- 字典部分
host = "localhost",
port = 8080,
-- 数组部分
"default_server", -- [1]
"backup_server", -- [2]
-- 嵌套表
ssl = {
enabled = true,
cert = "/etc/ssl/cert.pem",
},
}
print(config[1]) -- "default_server"
print(config.host) -- "localhost"
print(config.ssl.cert) -- "/etc/ssl/cert.pem"
4.3.4 表操作技巧
-- 表浅拷贝
local function shallow_copy(t)
local copy = {}
for k, v in pairs(t) do
copy[k] = v
end
return copy
end
-- 表深拷贝
local function deep_copy(t)
if type(t) ~= "table" then return t end
local copy = {}
for k, v in pairs(t) do
copy[deep_copy(k)] = deep_copy(v)
end
return setmetatable(copy, getmetatable(t))
end
-- 表合并
local function merge_tables(t1, t2)
local result = {}
for k, v in pairs(t1) do result[k] = v end
for k, v in pairs(t2) do result[k] = v end
return result
end
-- 检查表是否包含值
local function has_value(t, val)
for _, v in pairs(t) do
if v == val then return true end
end
return false
end
-- 表转字符串(调试用)
local function dump_table(t, indent)
indent = indent or ""
local parts = {}
for k, v in pairs(t) do
if type(v) == "table" then
table.insert(parts, indent .. tostring(k) .. " = {")
table.insert(parts, dump_table(v, indent .. " "))
table.insert(parts, indent .. "}")
else
table.insert(parts, indent .. tostring(k) .. " = " .. tostring(v))
end
end
return table.concat(parts, "\n")
end
4.4 函数
4.4.1 基本函数
-- 标准定义
local function add(a, b)
return a + b
end
-- 函数变量(等价写法)
local add = function(a, b)
return a + b
end
-- 多返回值
local function split_uri(uri)
local path = uri:match("^([^%?]*)") -- 路径部分
local query = uri:match("%?(.+)$") -- 查询字符串
return path, query
end
local path, query = split_uri("/api/users?page=1")
-- path = "/api/users"
-- query = "page=1"
-- 可变参数
local function log(level, ...)
local args = {...}
local parts = {}
for i, v in ipairs(args) do
table.insert(parts, tostring(v))
end
print("[" .. level .. "] " .. table.concat(parts, " "))
end
log("INFO", "Request from", "192.168.1.1", "status:", 200)
4.4.2 闭包(Closure)
闭包是 Lua 函数式编程的基础,也是 OpenResty 中常用模式:
-- 闭包示例:生成计数器
local function make_counter(initial)
local count = initial or 0
return function()
count = count + 1
return count
end
end
local counter = make_counter(0)
print(counter()) -- 1
print(counter()) -- 2
print(counter()) -- 3
-- 实际应用:限流器工厂
local function make_rate_limiter(max_requests, window_seconds)
local requests = {}
local window = window_seconds
return function(key)
local now = os.time()
-- 清理过期记录
if requests[key] then
local new_requests = {}
for _, ts in ipairs(requests[key]) do
if now - ts < window then
table.insert(new_requests, ts)
end
end
requests[key] = new_requests
else
requests[key] = {}
end
-- 检查限流
if #requests[key] >= max_requests then
return false, "Rate limit exceeded"
end
table.insert(requests[key], now)
return true, "OK"
end
end
local limiter = make_rate_limiter(100, 60) -- 100次/分钟
local ok, msg = limiter("user_123")
4.4.3 方法(冒号语法)
-- 冒号语法:自动传递 self 参数
local Router = {}
Router.__index = Router
function Router.new()
local self = setmetatable({}, Router)
self.routes = {}
return self
end
-- 冒号定义:自动有 self 参数
function Router:add(path, handler)
self.routes[path] = handler
end
function Router:match(path)
return self.routes[path]
end
-- 使用
local router = Router.new()
router:add("/api/users", function() return "users handler" end) -- 冒号调用
router:add("/api/orders", function() return "orders handler" end)
local handler = router:match("/api/users")
print(handler()) -- "users handler"
4.5 元表(Metatable)
元表是 Lua 实现面向对象和运算符重载的机制。
-- __index: 访问不存在的键时调用
local defaults = {
timeout = 5000,
retries = 3,
debug = false,
}
local config = {timeout = 10000}
setmetatable(config, {__index = defaults})
print(config.timeout) -- 10000 (config 自己的值)
print(config.retries) -- 3 (从 defaults 获取)
-- __tostring: 自定义字符串表示
local Response = {}
Response.__index = Response
function Response.new(status, body)
return setmetatable({status = status, body = body}, Response)
end
function Response:__tostring()
return string.format("Response(%d, %s)", self.status, self.body)
end
local res = Response.new(200, '{"ok":true}')
print(tostring(res)) -- "Response(200, {"ok":true})"
-- __call: 让表可以像函数一样被调用
local Middleware = {}
Middleware.__index = Middleware
function Middleware.new(name, fn)
return setmetatable({name = name, fn = fn}, Middleware)
end
function Middleware:__call(...)
ngx.log(ngx.INFO, "Middleware: ", self.name)
return self.fn(...)
end
local auth = Middleware.new("auth", function(req)
return req.headers["Authorization"] ~= nil
end)
-- 可以像函数一样调用
local ok = auth({headers = {Authorization = "Bearer xxx"}})
4.6 面向对象编程
-- 类定义模式
local BasePlugin = {}
BasePlugin.__index = BasePlugin
function BasePlugin.new(name, priority)
local self = setmetatable({}, BasePlugin)
self.name = name
self.priority = priority or 0
self.enabled = true
return self
end
function BasePlugin:execute(ctx)
error("execute() not implemented")
end
function BasePlugin:enable()
self.enabled = true
end
function BasePlugin:disable()
self.enabled = false
end
-- 继承
local RateLimitPlugin = setmetatable({}, {__index = BasePlugin})
RateLimitPlugin.__index = RateLimitPlugin
function RateLimitPlugin.new(max_req, window)
local self = BasePlugin.new("rate-limit", 100)
setmetatable(self, RateLimitPlugin)
self.max_requests = max_req
self.window = window
return self
end
function RateLimitPlugin:execute(ctx)
if not self.enabled then return true end
-- 限流逻辑
local key = ctx.client_ip
local count = ngx.shared.rate_limit:get(key) or 0
if count >= self.max_requests then
ctx.status = 429
ctx.body = '{"error":"Rate limit exceeded"}'
return false
end
ngx.shared.rate_limit:incr(key, 1, 0, self.window)
return true
end
-- 使用
local limiter = RateLimitPlugin.new(100, 60)
local ok = limiter:execute({client_ip = "192.168.1.1"})
4.7 模块系统
-- 定义模块: /usr/local/openresty/lua/gateway/utils.lua
local _M = {}
_M._VERSION = "1.0.0"
local function private_func() -- 私有函数(local)
return "private"
end
function _M.public_func() -- 公有函数
return "public: " .. private_func()
end
return _M
-- 使用模块
local utils = require "gateway.utils"
print(utils.public_func())
注意:OpenResty 中使用
require加载的模块会被缓存,整个 Worker 进程共享同一份。如果需要独立实例,使用工厂函数模式。
4.8 协程(Coroutine)
Lua 的协程是 OpenResty 实现同步风格异步编程的关键。
-- 基本协程
local co = coroutine.create(function()
coroutine.yield("step 1")
coroutine.yield("step 2")
return "step 3"
end)
print(coroutine.resume(co)) -- true, "step 1"
print(coroutine.resume(co)) -- true, "step 2"
print(coroutine.resume(co)) -- true, "step 3"
print(coroutine.status(co)) -- "dead"
-- 生产者-消费者模式
local function producer()
return coroutine.create(function()
for i = 1, 5 do
coroutine.yield(i * 10)
end
end)
end
local co = producer()
while true do
local ok, value = coroutine.resume(co)
if not ok or value == nil then break end
print("Received:", value)
end
OpenResty 中的协程应用
-- OpenResty 利用协程将异步 API 包装为同步风格
-- 底层原理:ngx.socket.tcp 等 API 在 I/O 等待时 yield,完成后 resume
local sock = ngx.socket.tcp()
sock:settimeout(3000)
-- 这看起来是同步代码,但底层是异步的
-- 1. 调用 connect → yield(等待连接完成)
-- 2. 连接完成 → resume(继续执行)
local ok, err = sock:connect("127.0.0.1", 6379)
-- 发送数据 → yield → 发送完成 → resume
local bytes, err = sock:send("PING\r\n")
-- 接收数据 → yield → 接收完成 → resume
local data, err = sock:receive()
sock:setkeepalive()
4.9 OpenResty 核心 API
4.9.1 ngx.var — Nginx 变量
-- 读取 Nginx 内置变量
local method = ngx.var.request_method
local uri = ngx.var.uri
local host = ngx.var.host
local client_ip = ngx.var.remote_addr
local args = ngx.var.args
-- 读取自定义变量(需要在 nginx.conf 中用 set 定义)
-- local user_id = ngx.var.user_id
-- 设置变量(仅在 rewrite 和 access 阶段有效)
ngx.var.my_custom_var = "some_value"
4.9.2 ngx.req — 请求操作
-- 获取请求方法
local method = ngx.req.get_method()
-- 获取请求头
local headers = ngx.req.get_headers()
local content_type = headers["content-type"]
local auth = headers["authorization"]
-- 获取 URI 参数
local args = ngx.req.get_uri_args()
local page = args.page or 1
local limit = args.limit or 20
-- 读取请求体
ngx.req.read_body()
local body = ngx.req.get_body_data()
-- 获取请求体(文件上传场景)
local file = ngx.req.get_body_file()
-- 修改请求
ngx.req.set_method(ngx.HTTP_POST)
ngx.req.set_uri("/new/path", true) -- true 表示内部重定向
ngx.req.set_header("X-Forwarded-For", ngx.var.remote_addr)
4.9.3 ngx.resp — 响应操作
-- 获取响应头
local resp_headers = ngx.resp.get_headers()
local content_type = resp_headers["content-type"]
4.9.4 ngx.shared — 共享内存
# nginx.conf 中定义
lua_shared_dict my_cache 10m;
local cache = ngx.shared.my_cache
-- 写入(带过期时间)
cache:set("key1", "value1", 60) -- 60 秒过期
cache:set("key2", 100, 300)
-- 仅当不存在时写入
local ok, err = cache:add("key3", "value3", 60)
-- 读取
local value, flags = cache:get("key1")
-- 递增(常用于计数器)
local new_val, err = cache:incr("key2", 1, 0, 60) -- +1,初始值 0,TTL 60s
-- 删除
cache:delete("key1")
-- 清空
cache:flush_all()
-- 获取使用情况
local capacity = cache:capacity() -- 总容量
local free = cache:free_space() -- 剩余空间
4.9.5 ngx.timer — 定时器
-- 一次性定时器
local function delayed_task(premature, arg1, arg2)
if premature then
return -- Worker 正在退出,放弃任务
end
ngx.log(ngx.INFO, "Delayed task: ", arg1, ", ", arg2)
end
local ok, err = ngx.timer.at(5, delayed_task, "hello", 42)
-- 周期性定时器(OpenResty 1.19.3+)
local function periodic_task(premature)
if premature then return end
-- 定期清理过期数据
ngx.shared.cache:flush_expired(100) -- 最多清理 100 个
end
ngx.timer.every(30, periodic_task)
4.10 LuaJIT FFI
FFI(Foreign Function Interface)允许 Lua 直接调用 C 库函数,无需编写 C 绑定层。
local ffi = require "ffi"
-- 声明 C 函数
ffi.cdef[[
int getpid(void);
int atoi(const char *nptr);
typedef struct { double x; double y; } point_t;
]]
-- 调用 C 函数
local pid = ffi.C.getpid()
ngx.log(ngx.INFO, "PID: ", pid)
local num = ffi.C.atoi("12345")
-- 创建 C 结构体
local point = ffi.new("point_t")
point.x = 3.14
point.y = 2.71
FFI 实际应用:高性能字符串处理
local ffi = require "ffi"
local C = ffi.C
ffi.cdef[[
void *memcpy(void *dest, const void *src, size_t n);
int memcmp(const void *s1, const void *s2, size_t n);
size_t strlen(const char *s);
]]
-- 快速字符串比较(避免 Lua 字符串对象创建)
local function fast_compare(s1, s2)
if #s1 ~= #s2 then return false end
return C.memcmp(s1, s2, #s1) == 0
end
-- FFI 调用比纯 Lua 快数十倍
FFI 库加载
-- 加载动态库
local lib = ffi.load("ssl") -- 加载 libssl.so
ffi.cdef[[
// OpenSSL 函数声明
typedef struct ssl_ctx_st SSL_CTX;
SSL_CTX *SSL_CTX_new(void *method);
]]
-- 使用
local ctx = lib.SSL_CTX_new(nil)
4.11 错误处理
-- pcall: 保护调用,捕获错误
local ok, result = pcall(function()
error("something went wrong")
end)
if not ok then
ngx.log(ngx.ERR, "Error: ", result)
end
-- xpcall: 带错误处理函数的保护调用
local function error_handler(err)
return debug.traceback(err, 2)
end
local ok, err = xpcall(function()
error("test error")
end, error_handler)
if not ok then
ngx.log(ngx.ERR, "Error with trace:\n", err)
end
-- 常见模式:安全调用外部服务
local function safe_redis_call(red, cmd, ...)
local ok, err = pcall(function(...)
return red[cmd](red, ...)
end, ...)
if not ok then
ngx.log(ngx.ERR, "Redis error: ", err)
return nil, err
end
return ok
end
4.12 性能优化技巧
| 技巧 | 说明 | 示例 |
|---|---|---|
使用 local | 局部变量比全局变量快 | local x = 1 vs x = 1 |
| 避免表创建 | 复用表对象减少 GC | 预分配、对象池 |
使用 table.concat | 避免 .. 拼接大量字符串 | table.concat(parts, ",") |
避免 string.gsub | 对简单场景使用 string.find + string.sub | |
| 使用 FFI | 计算密集型操作用 FFI 调用 C | |
避免 pairs | 有序遍历用 ipairs,数组操作更快 | |
| JIT 友好 | 避免在热路径中使用 pcall、require |
-- ❌ 不推荐:循环中拼接字符串
local result = ""
for i = 1, 1000 do
result = result .. tostring(i) .. "," -- 每次创建新字符串
end
-- ✅ 推荐:使用 table.concat
local parts = {}
for i = 1, 1000 do
parts[#parts + 1] = tostring(i)
end
local result = table.concat(parts, ",")
-- ❌ 不推荐:循环中创建表
for i = 1, 1000 do
local t = {key = "value"} -- 每次创建新表
process(t)
end
-- ✅ 推荐:复用表
local t = {}
for i = 1, 1000 do
t.key = "value"
process(t)
t.key = nil
end
4.13 常用字符串操作速查
| 操作 | 函数 | 示例 |
|---|---|---|
| 长度 | #str 或 string.len(str) | #"hello" → 5 |
| 查找 | string.find(str, pattern) | string.find("hello", "ell") → 2,4 |
| 截取 | string.sub(str, i, j) | string.sub("hello", 2, 4) → “ell” |
| 替换 | string.gsub(str, pat, repl) | string.gsub("hello", "l", "L") → “heLLo” |
| 格式化 | string.format(fmt, ...) | string.format("id=%d", 42) → “id=42” |
| 匹配 | string.match(str, pattern) | string.match("v123", "%d+") → “123” |
| 全匹配 | string.gmatch(str, pattern) | 迭代器 |
| 大写 | string.upper(str) | string.upper("hello") → “HELLO” |
| 小写 | string.lower(str) | string.lower("HELLO") → “hello” |
| 重复 | string.rep(str, n) | string.rep("ab", 3) → “ababab” |
| 反转 | string.reverse(str) | string.reverse("hello") → “olleh” |
| 字节码 | string.byte(str, i) | string.byte("A") → 65 |
| 字符 | string.char(n) | string.char(65) → “A” |