OpenResty, 其是由Nginx核心加很多第三方模块组成,其最大的亮点是默认集成了Lua开发环境,使得Nginx可以作为一个Web Server使用。
借助于 Nginx 的事件驱动模型和非阻塞IO,可以实现高性能的Web应用程序。而且OpenResty提供了大量组件如Mysql、Redis、Memcached等等,使在Nginx上开发Web应用更方便更简单。
目前在京东如实时价格、秒杀、动态服务、单品页、列表页等都在使用 Nginx + Lua 架构,其他公司如淘宝、去哪儿网等。
安装
mkdir -p /data/soft/
cd /data/soft/
安装依赖
yum install readline-devel pcre-devel openssl-devel gcc
wget https://openresty.org/download/ngx_openresty-1.9.3.1.tar.gz
tar -zxvf ngx_openresty-1.9.3.1.tar.gz
cd ngx_openresty-1.9.3.1
ngx_openresty-1.9.3.1/bundle 目录里存放着nginx核心和很多第三方模块,比如有我们需要的Lua和LuaJIT
安装 LuaJIT
cd bundle/LuaJIT-2.1-20150622/
make clean && make && make install
ln -sf luajit-2.1.0-alpha /usr/local/bin/luajit
安装 ngx_cache_purge
该模块用于清理nginx缓存
cd /data/soft/ngx_openresty-1.9.3.1/bundle/
wget https://github.com/FRiCKLE/ngx_cache_purge/archive/master.zip
unzip master.zip
rm -rf master.zip
安装 nginx_upstream_check_module
该模块用于ustream健康检查
cd /data/soft/ngx_openresty-1.9.3.1/bundle/
wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/master.zip
unzip master.zip
rm -rf master.zip
安装 ngx_openresty
cd /data/soft/ngx_openresty-1.9.3.1
./configure --prefix=/usr/local --with-http_realip_module --with-pcre --with-luajit --add-module=./bundle/ngx_cache_purge-master/ --add-module=./bundle/nginx_upstream_check_module-master/ -j2
make && make install
cd /usr/local
ll
如果发现有如下目录,说明安装成功:
luajit, lualib, nginx
通过 /usr/local/nginx/sbin/nginx -V 查看nginx版本和安装的模块
启动nginx
/usr/local/nginx/sbin/nginx
配置及Nginx HttpLuaModule文档在可以查看 http://wiki.nginx.org/HttpLuaModule
功能测试
vim /usr/local/nginx/conf/nginx.conf
在http部分添加如下配置
#lua模块路径,多个之间”;”分隔,其中”;;”表示默认搜索路径,默认到 /usr/local/nginx 下找
lua_package_path "/usr/local/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/local/lualib/?.so;;"; #c模块
为了方便开发我们在 /usr/local/nginx/conf 目录下创建一个 lua.conf 内容:
server {
listen 80;
server_name _;
}
在 nginx.conf 中的 http 部分添加 include lua.conf 包含此文件片段
include lua.conf;
测试是否正常
/usr/local/nginx/sbin/nginx -t
如果显示如下内容说明配置成功
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
HelloWorld
在 lua.conf 中 server 部分添加如下配置
location /lua {
default_type 'text/html';
content_by_lua 'ngx.say("hello world!")';
}
测试配置是否正确
/usr/local/nginx/sbin/nginx -t
重启nginx
/usr/local/nginx/sbin/nginx -s reload
访问如 http://115.236.185.12/lua(自己的机器根据实际情况换ip),可以看到如下内容
hello world!
加载外部 lua 文件
我们把lua代码放在nginx配置中会随着lua的代码的增加导致配置文件太长不好维护,因此我们应该把lua代码移到外部文件中存储。
mkdir -p /usr/local/nginx/conf/lua/
vim /usr/local/nginx/conf/lua/test.lua
添加如下内容
ngx.say("hello world");
然后 lua.conf 修改为
location /lua {
default_type 'text/html';
content_by_lua_file conf/lua/test.lua; #相对于nginx安装目录
}
此处 conf/lua/test.lua 也可以使用绝对路径 /usr/local/nginx/conf/lua/test.lua。
重启 nginx 可看效果。
lua_code_cache
默认情况下 lua_code_cache 是开启的,即缓存lua代码,即每次lua代码变更必须reload nginx才生效。
如果在开发阶段可以通过lua_code_cache off;关闭缓存,这样调试时每次修改lua代码不需要reload nginx;但是正式环境一定记得开启缓存。
location /lua {
default_type 'text/html';
lua_code_cache off;
content_by_lua_file conf/lua/test.lua;
}
开启后reload nginx会看到如下报警
nginx: [alert] lua_code_cache is off; this will hurt performance in /usr/local/nginx/conf/lua.conf:6
Nginx Lua API
和一般的Web Server类似,我们需要接收请求、处理并输出响应。而对于请求我们需要获取如请求参数、请求头、Body体等信息;
而对于处理就是调用相应的Lua代码即可;输出响应需要进行响应状态码、响应头和响应内容体的输出。 因此我们从如上几个点出发即可。
示例一.请求相关数据
vim /usr/local/nginx/conf/lua.conf
在 server 域里添加:
location ~ /lua_request/(\d+)/(\d+) {
#设置nginx变量
set $a $1;
set $b $host;
default_type "text/html";
#nginx内容处理
content_by_lua_file /usr/local/nginx/conf/lua/test_request.lua;
#内容体处理完成后调用
echo_after_body "ngx.var.b $b";
}
vim /usr/local/nginx/conf/lua/test_request.lua 编辑:
--nginx变量
local var = ngx.var
ngx.say("ngx.var.a : ", var.a, "<br/>")
ngx.say("ngx.var.b : ", var.b, "<br/>")
ngx.say("ngx.var[2] : ", var[2], "<br/>")
ngx.var.b = 2;
ngx.say("<br/>")
--请求头
local headers = ngx.req.get_headers()
ngx.say("headers begin", "<br/>")
ngx.say("Host : ", headers["Host"], "<br/>")
ngx.say("user-agent : ", headers["user-agent"], "<br/>")
ngx.say("user-agent : ", headers.user_agent, "<br/>")
for k,v in pairs(headers) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ","), "<br/>")
else
ngx.say(k, " : ", v, "<br/>")
end
end
ngx.say("headers end", "<br/>")
ngx.say("<br/>")
--get请求uri参数
ngx.say("uri args begin", "<br/>")
local uri_args = ngx.req.get_uri_args()
for k, v in pairs(uri_args) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
else
ngx.say(k, ": ", v, "<br/>")
end
end
ngx.say("uri args end", "<br/>")
ngx.say("<br/>")
--post请求参数
ngx.req.read_body()
ngx.say("post args begin", "<br/>")
local post_args = ngx.req.get_post_args()
for k, v in pairs(post_args) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
else
ngx.say(k, ": ", v, "<br/>")
end
end
ngx.say("post args end", "<br/>")
ngx.say("<br/>")
--请求的http协议版本
ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "<br/>")
--请求方法
ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "<br/>")
--原始的请求头内容
ngx.say("ngx.req.raw_header : ", ngx.req.raw_header(), "<br/>")
--请求的body内容体
ngx.say("ngx.req.get_body_data() : ", ngx.req.get_body_data(), "<br/>")
ngx.say("<br/>")
说明:
ngx.var
nginx变量,如果要赋值如ngx.var.b = 2,此变量必须提前声明; 另外对于nginx location中使用正则捕获的捕获组可以使用ngx.var[捕获组数字]获取;
ngx.req.get_headers
获取请求头,默认只获取前100,如果想要获取所有可以调用ngx.req.get_headers(0); 获取请求头时请使用如headers.user_agent这种方式;如果一个请求头有多个值,则返回的是table;
ngx.req.get_uri_args
获取url请求参数,其用法和get_headers类似;
ngx.req.get_post_args
获取post请求内容,其用法和get_headers类似,但是必须提前调用ngx.req.read_body()来读取body体
ngx.req.raw_header
未解析的请求头字符串;
ngx.req.get_body_data
为解析的请求body体内容字符串。
测试: 重启 nginx
/usr/local/nginx/sbin/nginx -s reload
然后访问 :
wget --post-data 'a=1&b=2' 'http://115.236.185.12/lua_request/1/2?a=3&b=4' -O -
响应如下:
gx.var.a : 1<br/>
ngx.var.b : 115.236.185.12<br/>
ngx.var[2] : 2<br/>
<br/>
headers begin<br/>
Host : 115.236.185.12<br/>
user-agent : Wget/1.12 (linux-gnu)<br/>
user-agent : Wget/1.12 (linux-gnu)<br/>
host : 115.236.185.12<br/>
content-type : application/x-www-form-urlencoded<br/>
connection : Keep-Alive<br/>
accept : */*<br/>
content-length : 7<br/>
user-agent : Wget/1.12 (linux-gnu)<br/>
headers end<br/>
<br/>
uri args begin<br/>
b: 4<br/>
a: 3<br/>
uri args end<br/>
<br/>
post args begin<br/>
b: 2<br/>
a: 1<br/>
post args end<br/>
<br/>
ngx.req.http_version : 1<br/>
ngx.req.get_method : POST<br/>
ngx.req.raw_header : POST /lua_request/1/2?a=3&b=4 HTTP/1.0
User-Agent: Wget/1.12 (linux-gnu)
Accept: */*
Host: 115.236.185.12
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
<br/>
ngx.req.get_body_data() : a=1&b=2<br/>
<br/>
ngx.var.b 2
可以看到在 lua 中获得了 url 中用正则匹配出来的值, post 的值, host 等各个值
示例二.响应相关处理
vim /usr/local/nginx/conf/lua.conf
在 server 域里添加:
location /lua_response_1 {
default_type "text/html";
content_by_lua_file /usr/local/nginx/conf/lua/test_response_1.lua;
}
vim /usr/local/nginx/conf/lua/test_response_1.lua
编辑:
--写响应头
ngx.header.a = "1"
--多个响应头可以使用table
ngx.header.b = {"2", "3"}
--输出响应
ngx.say("a", "b", "<br/>")
ngx.print("c", "d", "<br/>")
--200状态码退出
return ngx.exit(200)
说明:
ngx.header:输出响应头;
ngx.print:输出响应内容体;
ngx.say:通ngx.print,但是会最后输出一个换行符;
ngx.exit:指定状态码退出。
测试: 重启 nginx:
/usr/local/nginx/sbin/nginx -s reload
然后访问 :
http://115.236.185.12/lua_response_1
示例三.重定向
vim /usr/local/nginx/conf/lua.conf
在 server 域里添加:
location /lua_response_2 {
default_type "text/html";
content_by_lua_file /usr/local/nginx/conf/lua/test_response_2.lua;
}
vim /usr/local/nginx/conf/lua/test_response_2.lua
编辑:
ngx.redirect("http://www.edeng.cn", 302)
说明:
ngx.redirect:重定向
测试: 重启 nginx:
/usr/local/nginx/sbin/nginx -s reload
然后访问 :
http://115.236.185.12/lua_response_2
示例四.uri处理
vim /usr/local/nginx/conf/lua.conf
在 server 域里添加:
location /lua_other {
default_type "text/html";
content_by_lua_file /usr/local/nginx/conf/lua/lua_other.lua;
}
vim /usr/local/nginx/conf/lua/lua_other.lua
编辑:
--未经解码的请求uri
local request_uri = ngx.var.request_uri;
ngx.say("request_uri : ", request_uri, "<br/>");
--解码
ngx.say("decode request_uri : ", ngx.unescape_uri(request_uri), "<br/>");
--MD5
ngx.say("ngx.md5 : ", ngx.md5("123"), "<br/>")
--http time
ngx.say("ngx.http_time : ", ngx.http_time(ngx.time()), "<br/>")
说明:
ngx.escape_uri/ngx.unescape_uri : uri编码解码;
ngx.encode_args/ngx.decode_args: 参数编码解码;
ngx.encode_base64/ngx.decode_base64: BASE64编码解码;
ngx.re.match: nginx正则表达式匹配;
测试: 重启 nginx:
/usr/local/nginx/sbin/nginx -s reload
然后访问 :
http://115.236.185.12/lua_other
共享内存
Nginx是一个Master进程多个Worker进程的工作方式,因此我们可能需要在多个Worker进程中共享数据,那么此时就可以使用 ngx.shared.DICT 来实现全局内存共享
首先在 nginx.conf 的http部分分配内存大小
# 共享全局变量,在所有worker间共享
lua_shared_dict shared_data 1m;
vim /usr/local/nginx/conf/lua.conf
在 server 域里添加:
location /lua_shared_dict {
default_type "text/html";
content_by_lua_file /usr/local/nginx/conf/lua/test_lua_shared_dict.lua;
}
vim /usr/local/nginx/conf/lua/test_lua_shared_dict.lua
编辑:
--1、获取全局共享内存变量
local shared_data = ngx.shared.shared_data
--2、获取字典值
local i = shared_data:get("i")
if not i then
i = 1
--3、惰性赋值
shared_data:set("i", i)
ngx.say("lazy set i ", i, "<br/>")
end
--递增
i = shared_data:incr("i", 1)
ngx.say("i=", i, "<br/>")
测试: 重启 nginx:
/usr/local/nginx/sbin/nginx -s reload
然后访问 :
http://115.236.185.12/lua_shared_dict
每访问一次,数值就会加 1。
Nginx Lua模块指令
Nginx共11个处理阶段,而相应的处理阶段是可以做插入式 lua 处理.
另外指令可以在http、server、server if、location、location if
几个范围进行配置
在各个阶段里,可执行的 lua 指令非常多,常用的有:
init_by_lua
每次Nginx重新加载配置时执行,可以用它来完成一些耗时模块的加载,或者初始化一些全局配置;
nginx.conf配置文件中的http部分添加如下代码
#共享全局变量,在所有worker间共享
lua_shared_dict shared_data 1m;
init_by_lua_file /usr/local/nginx/conf/lua/init.lua;
init.lua:
--初始化耗时的模块
local redis = require 'resty.redis'
local cjson = require 'cjson'
--全局变量,不推荐
count = 1
--共享全局内存
local shared_data = ngx.shared.shared_data
shared_data:set("count", 1)
测试代码:
count = count + 1
ngx.say("global variable : ", count)
local shared_data = ngx.shared.shared_data
ngx.say(", shared memory : ", shared_data:get("count"))
shared_data:incr("count", 1)
ngx.say("hello world")
访问时会发现全局变量一直不变,而共享内存一直递增
global variable : 2 , shared memory : 8 hello world
init_worker_by_lua
用于启动一些定时任务,比如心跳检查,定时拉取服务器配置等等; 此处的任务是跟Worker进程数量有关系的,比如有2个Worker进程那么就会启动两个完全一样的定时任务。
nginx.conf配置文件中的http部分添加如下代码
init_worker_by_lua_file /usr/local/nginx/conf/lua/init_worker.lua;
init_worker.lua:
local count = 0
local delayInSeconds = 3
local heartbeatCheck = nil
heartbeatCheck = function(args)
count = count + 1
ngx.log(ngx.ERR, "do check ", count)
local ok, err = ngx.timer.at(delayInSeconds, heartbeatCheck)
if not ok then
ngx.log(ngx.ERR, "failed to startup heartbeart worker...", err)
end
end
heartbeatCheck()
ngx.timer.at: 延时调用相应的回调方法;
ngx.timer.at(秒单位延时,回调函数,回调函数的参数列表);
可以将延时设置为0即得到一个立即执行的任务,任务不会在当前请求中执行不会阻塞当前请求,而是在一个轻量级线程中执行。
另外根据实际情况设置如下指令
lua_max_pending_timers 1024; #最大等待任务数
lua_max_running_timers 256; #最大同时运行任务数
set_by_lua
设置nginx变量,我们用的set指令即使配合if指令也很难实现负责的赋值逻辑
location /lua_set_1 {
default_type "text/html";
set_by_lua_file $num /usr/local/nginx/conf/lua/test_set_1.lua;
echo $num;
}
test_set_1.lua:
local uri_args = ngx.req.get_uri_args()
local i = uri_args["i"] or 0
local j = uri_args["j"] or 0
return i + j
我们实际工作时经常涉及到网站改版,有时候需要新老并存,或者切一部分流量到新版
首先在 example.conf 中使用map指令来映射host到指定nginx变量
############ 测试时使用的动态请求
map $host $item_dynamic {
default "0";
item2014.jd.com "1";
}
绑定hosts
115.236.185.12 item.jd.com;
115.236.185.12 item2014.jd.com;
此时我们想访问item2014.jd.com时访问新版,那么我们可以简单的使用如:
if ($item_dynamic = "1") {
proxy_pass http://new;
}
proxy_pass http://old;
但是我们想把商品编号为为8位(比如品类为图书的)没有改版完成,需要按照相应规则跳转到老版,但是其他的到新版;
虽然使用if指令能实现,但是比较麻烦,基本需要这样:
set jump "0";
if($item_dynamic = "1") {
set $jump "1";
}
if(uri ~ "^/6[0-9]{7}.html") {
set $jump "${jump}2";
}
# 非强制访问新版,且访问指定范围的商品
if (jump == "02") {
proxy_pass http://old;
}
proxy_pass http://new;
以上规则还是比较简单的,如果涉及到更复杂的多重if/else或嵌套if/else实现起来就更痛苦了,可能需要到后端去做了;此时我们就可以借助lua了:
set_by_lua $to_book '
local ngx_match = ngx.re.match
local var = ngx.var
local skuId = var.skuId
local r = var.item_dynamic ~= "1" and ngx.re.match(skuId, "^[0-9]{8}$")
if r then return "1" else return "0" end;
';
set_by_lua $to_mvd '
local ngx_match = ngx.re.match
local var = ngx.var
local skuId = var.skuId
local r = var.item_dynamic ~= "1" and ngx.re.match(skuId, "^[0-9]{9}$")
if r then return "1" else return "0" end;
';
#自营图书
if ($to_book) {
proxy_pass http://127.0.0.1/old_book/$skuId.html;
}
#自营音像
if ($to_mvd) {
proxy_pass http://127.0.0.1/old_mvd/$skuId.html;
}
#默认
proxy_pass http://127.0.0.1/proxy/$skuId.html;
rewrite_by_lua
执行内部URL重写或者外部重定向,典型的如伪静态化的URL重写。其默认执行在rewrite处理阶段的最后
location /lua_rewrite_1 {
default_type "text/html";
rewrite_by_lua_file /usr/local/nginx/conf/lua/test_rewrite_1.lua;
echo "no rewrite";
}
test_rewrite_1.lua:
if ngx.req.get_uri_args()["jump"] == "1" then
return ngx.redirect("http://www.edeng.cn?jump=1", 302)
end
请求 http://115.236.185.12/lua_rewrite_1 时发现没有跳转,而请求 http://115.236.185.12/lua_rewrite_1?jump=1 时发现跳转到易登首页了。
此处需要301/302跳转根据自己需求定义
location /lua_rewrite_2 {
default_type "text/html";
rewrite_by_lua_file /usr/local/nginx/conf/lua/test_rewrite_2.lua;
echo "rewrite2 uri : $uri, a : $arg_a";
}
test_rewrite_2.lua:
if ngx.req.get_uri_args()["jump"] == "1" then
ngx.req.set_uri("/lua_rewrite_3", false);
ngx.req.set_uri("/lua_rewrite_4", false);
ngx.req.set_uri_args({a = 1, b = 2});
end
ngx.req.set_uri(uri, false):可以内部重写uri(可以带参数),等价于 rewrite ^ /lua_rewrite_3;
通过配合if/else可以实现
rewrite ^ /lua_rewrite_3 break;
这种功能;此处两者都是location内部url重写,不会重新发起新的location匹配;
ngx.req.set_uri_args:重写请求参数,可以是字符串(a=1&b=2)也可以是table;
访问如 http://115.236.185.12/lua_rewrite_2?jump=0 时得到响应
rewrite2 uri : /lua_rewrite_2, a :
访问如 http://115.236.185.12/lua_rewrite_2?jump=1 时得到响应
rewrite2 uri : /lua_rewrite_4, a : 1
location /lua_rewrite_3 {
default_type "text/html";
rewrite_by_lua_file /usr/local/nginx/conf/lua/test_rewrite_3.lua;
echo "rewrite3 uri : $uri";
}
test_rewrite_3.lua:
if ngx.req.get_uri_args()["jump"] == "1" then
ngx.req.set_uri("/lua_rewrite_4", true);
ngx.log(ngx.ERR, "=========")
ngx.req.set_uri_args({a = 1, b = 2});
end
ngx.req.set_uri(uri, true):可以内部重写uri,即会发起新的匹配location请求,等价于
rewrite ^ /lua_rewrite_4 last;
此处看error log是看不到我们记录的log。
所以请求如 http://115.236.185.12/lua_rewrite_3?jump=1 会到新的location中得到响应,此处没有/lua_rewrite_4,所以匹配到/lua请求,得到类似如下的响应
global variable : 2 , shared memory : 1 hello world
access_by_lua
用于访问控制,比如我们只允许内网ip访问,可以使用如下形式
allow 127.0.0.1;
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 172.16.0.0/12;
deny all;
location /lua_access {
default_type "text/html";
access_by_lua_file /usr/local/nginx/conf/lua/test_access.lua;
echo "access";
}
test_access.lua:
if ngx.req.get_uri_args()["token"] ~= "123" then
return ngx.exit(403)
end
即如果访问如 http://115.236.185.12/lua_access?token=234 将得到403 Forbidden的响应。
这样我们可以根据如 cookie/用户token 来决定是否有访问权限
content_by_lua
此指令之前已经用过了
Lua模块开发
在实际开发中,不可能把所有代码写到一个大而全的lua文件中,需要进行分模块开发;而且模块化是高性能Lua应用的关键。
使用require第一次导入模块后,所有Nginx 进程全局共享模块的数据和代码。
每个Worker进程需要时会得到此模块的一个副本(Copy-On-Write),即模块可以认为是每Worker进程共享而不是每Nginx Server共享;
另外注意之前我们使用init_by_lua中初始化的全局变量是每请求复制一个;如果想在多个Worker进程间共享数据可以使用ngx.shared.DICT或如Redis之类的存储
在/usr/example/lualib中已经提供了大量第三方开发库如cjson、redis客户端、mysql客户端
需要注意在使用前需要将库在nginx.conf中导入:
#lua模块路径,其中”;;”表示默认搜索路径,默认到/usr/local/nginx下找
lua_package_path "/usr/example/lualib/?.lua;;"; #lua 模块
lua_package_cpath "/usr/example/lualib/?.so;;"; #c模块
使用方式是在lua中通过如下方式引入
local cjson = require(“cjson”)
local redis = require(“resty.redis”)
接下来我们来开发一个简单的lua模块。
vim /usr/local/lualib/module1.lua
local count = 0
local function hello()
count = count + 1
ngx.say("count : ", count)
end
local _M = {
hello = hello
}
return _M
开发时将所有数据做成局部变量/局部函数;通过 _M导出要暴露的函数,实现模块化封装。
接下来创建 test_module_1.lua
vim /usr/local/nginx/conf/lua/test_module_1.lua
local module1 = require("module1")
module1.hello()
使用 local var = require(“模块名”)
,该模块会到 lua_package_path
和lua_package_cpath
声明的的位置查找我们的模块,对于多级目录的使用 require(“目录1.目录2.模块名”)
加载。
lua.conf 配置
location /lua_module_1 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_module_1.lua;
}
访问如 http://115.236.185.12/lua_module_1
进行测试,会得到类似如下的数据,count会递增
count : 1
count :2
……
count :N
此时可能发现count一直递增,假设我们的worker_processes 2,我们可以通过 kill -9 nginx worker process
杀死其中一个Worker进程得到count数据变化。
假设我们创建了vim /usr/local/lualib/test/module2.lua 模块,可以通过local module2 = require(“test.module2”)
加载模块
基本的模块开发就完成了,如果是只读数据可以通过模块中声明local变量存储;如果想在每Worker进程共享,请考虑竞争;如果要在多个Worker进程间共享请考虑使用 ngx.shared.DICT 或如 Redis存储。
Lua开发库
对于开发来说需要有好的生态开发库来辅助我们快速开发,而Lua中也有大多数我们需要的第三方开发库如Redis、Memcached、Mysql、Http客户端、JSON、模板引擎等。
一些常见的Lua库可以在github上搜索,https://github.com/search?utf8=%E2%9C%93&q=lua+resty
Redis
lua-resty-redis
是为基于cosocket API的ngx_lua提供的Lua redis客户端,通过它可以完成Redis的操作。默认安装OpenResty时已经自带了该模块。
使用文档可参考 https://github.com/openresty/lua-resty-redis
在测试之前先安装并启动Redis实例 http://blog.sina.com.cn/s/blog_5f54f0be0101bym4.html
基本操作
编辑 test_redis_baisc.lua
local function close_redis(red)
if not red then
return
end
local ok, err = red:close()
if not ok then
ngx.say("close redis error : ", err)
end
end
local redis = require("resty.redis")
--创建实例
local red = redis:new()
--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ip = "127.0.0.1"
local port = 3690
local ok, err = red:connect(ip, port)
if not ok then
ngx.say("connect to redis error : ", err)
return close_redis(red)
end
--调用API进行处理
ok, err = red:set("msg", "hello world")
if not ok then
ngx.say("set msg error : ", err)
return close_redis(red)
end
--调用API获取数据
local resp, err = red:get("msg")
if not resp then
ngx.say("get msg error : ", err)
return close_reedis(red)
end
--得到的数据为空处理
if resp == ngx.null then
resp = '' --比如默认值
end
ngx.say("msg : ", resp)
close_redis(red)
基本逻辑很简单,要注意此处判断是否为nil,需要跟ngx.null比较。
lua.conf 配置文件
location /lua_redis_basic {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_redis_basic.lua;
}
访问如 http://192.168.1.2/lua_redis_basic 进行测试,正常情况得到如下信息
msg : hello world
连接池
建立TCP连接需要三次握手而释放TCP连接需要四次握手,而这些往返仅需要一次,以后应该复用TCP连接,此时就可以考虑使用连接池,即连接池可以复用连接。
只需要将之前的close_redis函数改造为如下即可:
local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end
即设置空闲连接超时时间防止连接一直占用不释放;设置连接池大小来复用连接。
此处假设调用 red:set_keepalive(),连接池大小通过 nginx.conf 中http部分的如下指令定义:
#默认连接池大小,默认30
lua_socket_pool_size 30;
#默认超时时间,默认60s
lua_socket_keepalive_timeout 60s;
注意:
- 连接池是每Worker进程的,而不是每Server的;
- 当连接超过最大连接池大小时,会按照LRU算法回收空闲连接为新连接使用;
- 连接池中的空闲连接出现异常时会自动被移除;
- 连接池是通过ip和port标识的,即相同的ip和port会使用同一个连接池(即使是不同类型的客户端如 Redis、Memcached);
- 连接池第一次set_keepalive时连接池大小就确定下了,不会再变更;
- cosocket的连接池http://wiki.nginx.org/HttpLuaModule#tcpsock:setkeepalive。
pipeline
pipeline即管道,可以理解为把多个命令打包然后一起发送;
MTU(Maxitum Transmission Unit 最大传输单元)为二层包大小,一般为1500字节;而MSS(Maximum Segment Size 最大报文分段大小)为四层包大小,其一般是1500-20(IP报头)-20(TCP报头)=1460字节;
因此假设我们执行的多个Redis命令能在一个报文中传输的话,可以减少网络往返来提高速度。因此可以根据实际情况来选择走pipeline模式将多个命令打包到一个报文发送然后接受响应,而Redis协议也能很简单的识别和解决粘包。
修改之前的代码片段
red:init_pipeline()
red:set("msg1", "hello1")
red:set("msg2", "hello2")
red:get("msg1")
red:get("msg2")
local respTable, err = red:commit_pipeline()
--得到的数据为空处理
if respTable == ngx.null then
respTable = {} --比如默认值
end
--结果是按照执行顺序返回的一个table
for i, v in ipairs(respTable) do
ngx.say("msg : ", v, "<br/>")
end
通过init_pipeline()初始化,然后通过 commit_pipieline() 打包提交init_pipeline()之后的Redis命令;返回结果是一个lua table,可以通过ipairs循环获取结果;
配置相应location,测试得到的结果
msg : OK
msg : OK
msg : hello1
msg : hello2
Redis Lua脚本
利用Redis单线程特性,可以通过在Redis中执行Lua脚本实现一些原子操作。如之前的red:get(“msg”)可以通过如下两种方式实现:
直接eval
local resp, err = red:eval("return redis.call('get', KEYS[1])", 1, "msg");
script load
script load然后evalsha SHA1 校验和,这样可以节省脚本本身的服务器带宽:
local sha1, err = red:script("load", "return redis.call('get', KEYS[1])");
if not sha1 then
ngx.say("load script error : ", err)
return close_redis(red)
end
ngx.say("sha1 : ", sha1, "<br/>")
local resp, err = red:evalsha(sha1, 1, "msg");
首先通过script load导入脚本并得到一个sha1校验和(仅需第一次导入即可),然后通过evalsha执行sha1校验和即可,这样如果脚本很长通过这种方式可以减少带宽的消耗。
另外Redis集群分片算法该客户端没有提供需要自己实现,当然可以考虑直接使用类似于Twemproxy这种中间件实现。
Mysql
lua-resty-mysql
是为基于cosocket API的ngx_lua提供的Lua Mysql客户端,通过它可以完成Mysql的操作。默认安装OpenResty时已经自带了该模块。
使用文档可参考https://github.com/openresty/lua-resty-mysql
编辑 test_mysql.lua
local function close_db(db)
if not db then
return
end
db:close()
end
local mysql = require("resty.mysql")
--创建实例
local db, err = mysql:new()
if not db then
ngx.say("new mysql error : ", err)
return
end
--设置超时时间(毫秒)
db:set_timeout(1000)
local props = {
host = "127.0.0.1",
port = 3306,
database = "mysql",
user = "root",
password = "123456"
}
local res, err, errno, sqlstate = db:connect(props)
if not res then
ngx.say("connect to mysql error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
--删除表
local drop_table_sql = "drop table if exists test"
res, err, errno, sqlstate = db:query(drop_table_sql)
if not res then
ngx.say("drop table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
--创建表
local create_table_sql = "create table test(id int primary key auto_increment, ch varchar(100))"
res, err, errno, sqlstate = db:query(create_table_sql)
if not res then
ngx.say("create table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
--插入
local insert_sql = "insert into test (ch) values('hello')"
res, err, errno, sqlstate = db:query(insert_sql)
if not res then
ngx.say("insert error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
res, err, errno, sqlstate = db:query(insert_sql)
ngx.say("insert rows : ", res.affected_rows, " , id : ", res.insert_id, "<br/>")
--更新
local update_sql = "update test set ch = 'hello2' where id =" .. res.insert_id
res, err, errno, sqlstate = db:query(update_sql)
if not res then
ngx.say("update error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
ngx.say("update rows : ", res.affected_rows, "<br/>")
--查询
local select_sql = "select id, ch from test"
res, err, errno, sqlstate = db:query(select_sql)
if not res then
ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
for i, row in ipairs(res) do
for name, value in pairs(row) do
ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
end
end
ngx.say("<br/>")
--防止sql注入
local ch_param = ngx.req.get_uri_args()["ch"] or ''
--使用ngx.quote_sql_str防止sql注入
local query_sql = "select id, ch from test where ch = " .. ngx.quote_sql_str(ch_param)
res, err, errno, sqlstate = db:query(query_sql)
if not res then
ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
for i, row in ipairs(res) do
for name, value in pairs(row) do
ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
end
end
--删除
local delete_sql = "delete from test"
res, err, errno, sqlstate = db:query(delete_sql)
if not res then
ngx.say("delete error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
ngx.say("delete rows : ", res.affected_rows, "<br/>")
close_db(db)
对于新增/修改/删除会返回如下格式的响应:
{
insert_id = 0,
server_status = 2,
warning_count = 1,
affected_rows = 32,
message = nil
}
affected_rows表示操作影响的行数,insert_id是在使用自增序列时产生的id。
对于查询会返回如下格式的响应:
{
{ id= 1, ch= "hello"},
{ id= 2, ch= "hello2"}
}
null将返回ngx.null。
lua.conf配置文件
location /lua_mysql {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_mysql.lua;
}
访问如 http://192.168.1.2/lua_mysql?ch=hello 进行测试,得到如下结果
insert rows : 1 , id : 2
update rows : 1
select row 1 : ch = hello
select row 1 : id = 1
select row 2 : ch = hello2
select row 2 : id = 2
select row 1 : ch = hello
select row 1 : id = 1
delete rows : 2
客户端目前还没有提供预编译SQL支持(即占位符替换位置变量),这样在入参时记得使用ngx.quote_sql_str
进行字符串转义,防止sql注入;
连接池和之前Redis客户端完全一样。
RabbitMQ
在lua中可以借助第三方库连接 RabbitMQ:https://github.com/wingify/lua-resty-rabbitmqstomp
安装
cd /usr/local/lualib/resty/
下载三方库文件并放在该目录下。要注意的是,使用该库时,rabbitmq 必须先安装 rabbitmq_stomp 插件。安装方式是:
root@dev-localhost # sbin/rabbitmq-plugins enable rabbitmq_stomp
并更改配置文件: /usr/local/rabbitmq/etc/rabbitmq/rabbitmq.config 添加如下内容:
{rabbitmq_stomp,
[
{tcp_listeners,
[
{"127.0.0.1", 61613},
{"::1", 61613}
]
},
{default_user,
[
{login,"sunyu"},
{passcode,"1qazxsw2"}
]
}
]
},
测试 lua 代码:
local strlen = string.len
local cjson = require "cjson"
local rabbitmq = require("resty.rabbitmqstomp")
local opts = {
username = "sunyu",
password = "1qazxsw2",
trailing_lf = "true",
vhost = "/"
}
local mq, err = rabbitmq:new(opts)
if not mq then
ngx.say("Init Error: " .. err)
return
end
mq:set_timeout(10000)
local ok, err = mq:connect('127.0.0.1', 61613)
if not ok then
ngx.say("Connect Error: " .. err)
return
end
ngx.say("Connect Success!")
local msg = {key="value1", key2="value2"}
local headers = {}
headers["destination"] = "/exchange/test_e/test_key"
headers["receipt"] = "test_q"
headers["app-id"] = "test_q"
headers["persistent"] = "true"
headers["content-type"] = "application/json"
local ok, err = mq:send(cjson.encode(msg), headers)
if not ok then
ngx.say("Send Error: " .. err)
return
end
ngx.say("Published: " .. cjson.encode(msg))
local headers = {}
headers["destination"] = "/amq/queue/test_q"
headers["persistent"] = "true"
headers["id"] = "123"
local ok, err = mq:subscribe(headers)
if not ok then
ngx.say("Subscribe Error: " .. err)
return
end
local data, err = mq:receive()
if not data then
ngx.say("Receive Error: " .. err)
return
end
ngx.say("Consumed: " .. data)
ngx.header.content_type = "text/plain";
ngx.say(data);
local headers = {}
headers["persistent"] = "true"
headers["id"] = "123"
local ok, err = mq:unsubscribe(headers)
local ok, err = mq:set_keepalive(10000, 10000)
if not ok then
ngx.say("Keepalive Error: " .. err)
return
end
Http
OpenResty 默认没有提供Http客户端,需要使用第三方提供
lua-resty-http模块
我们可以从github上搜索相应的客户端,如: https://github.com/pintsized/lua-resty-http
安装
cd /usr/local/lualib/resty/
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http_headers.lua
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http.lua
测试
test_http_1.lua:
local http = require("resty.http")
--创建http客户端实例
local httpc = http.new()
local resp, err = httpc:request_uri("http://s.taobao.com", {
method = "GET",
path = "/search?q=hello",
headers = {
["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36"
}
})
if not resp then
ngx.say("request error :", err)
return
end
--获取状态码
ngx.status = resp.status
--获取响应头
for k, v in pairs(resp.headers) do
if k ~= "Transfer-Encoding" and k ~= "Connection" then
ngx.header[k] = v
end
end
--响应体
ngx.say(resp.body)
httpc:close()
响应头中的Transfer-Encoding和Connection可以忽略,因为这个数据是当前server输出的。
lua.conf 配置文件
location /lua_http_1 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_http_1.lua;
}
在nginx.conf中的http部分添加如下指令来做DNS解析
resolver 8.8.8.8;
访问如 http://192.168.1.2/lua_http_1 会看到淘宝的搜索界面。
使用方式比较简单,如超时和连接池设置和之前Redis客户端一样。
更多客户端使用规则请参考https://github.com/pintsized/lua-resty-http。
ngx.location.capture
ngx.location.capture也可以用来完成http请求,但是它只能请求到相对于当前nginx服务器的路径,不能使用之前的绝对路径进行访问,但是我们可以配合nginx upstream实现我们想要的功能。
在nginx.conf中的http部分添加如下upstream配置
upstream backend {
server s.taobao.com;
keepalive 100;
}
即我们将请求upstream到backend;另外记得一定要添加之前的DNS解析器。
在lua.conf配置如下location
location ~ /proxy/(.*) {
internal;
proxy_pass http://backend/$1$is_args$args;
}
internal表示只能内部访问,即外部无法通过url访问进来; 并通过proxy_pass将请求转发到upstream。
test_http_2.lua
local resp = ngx.location.capture("/proxy/search", {
method = ngx.HTTP_GET,
args = {q = "hello"}
})
if not resp then
ngx.say("request error :", err)
return
end
ngx.log(ngx.ERR, tostring(resp.status))
--获取状态码
ngx.status = resp.status
--获取响应头
for k, v in pairs(resp.header) do
if k ~= "Transfer-Encoding" and k ~= "Connection" then
ngx.header[k] = v
end
end
--响应体
if resp.body then
ngx.say(resp.body)
end
通过ngx.location.capture发送一个子请求,此处因为是子请求,所有请求头继承自当前请求。 还有如ngx.ctx和ngx.var是否继承可以参考官方文档http://wiki.nginx.org/ HttpLuaModule#ngx.location.capture。
另外还提供了ngx.location.capture_multi用于并发发出多个请求,这样总的响应时间是最慢的一个,批量调用时有用。
lua.conf 配置文件
location /lua_http_2 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_http_2.lua;
}
访问如http://192.168.1.2/lua_http_2进行测试可以看到淘宝搜索界面。
通过upstream + ngx.location.capture方式虽然麻烦点,但是得到更好的性能和upstream的连接池、负载均衡、故障转移、proxy cache等特性。
Json
在进行数据传输时JSON格式目前应用广泛,因此从Lua对象与JSON字符串之间相互转换是一个非常常见的功能;目前Lua也有几个JSON库,本人用过cjson、dkjson。
其中cjson的语法严格(比如unicode \u0020\u7eaf),要求符合规范否则会解析失败(如\u002),而dkjson相对宽松,当然也可以通过修改cjson的源码来完成一些特殊要求。
而在使用dkjson时也没有遇到性能问题,目前使用的就是dkjson。使用时要特别注意的是大部分JSON库都仅支持UTF-8编码;因此如果你的字符编码是如GBK则需要先转换为UTF-8然后进行处理。
cjson test_cjson.lua:
local cjson = require("cjson")
--lua对象到字符串
local obj = {
id = 1,
name = "zhangsan",
age = nil,
is_male = false,
hobby = {"film", "music", "read"}
}
local str = cjson.encode(obj)
ngx.say(str, "<br/>")
--字符串到lua对象
str = '{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1,"age":null}'
local obj = cjson.decode(str)
ngx.say(obj.age, "<br/>")
ngx.say(obj.age == nil, "<br/>")
ngx.say(obj.age == cjson.null, "<br/>")
ngx.say(obj.hobby[1], "<br/>")
--循环引用
obj = {
id = 1
}
obj.obj = obj
-- Cannot serialise, excessive nesting
--ngx.say(cjson.encode(obj), "<br/>")
local cjson_safe = require("cjson.safe")
--nil
ngx.say(cjson_safe.encode(obj), "<br/>")
null将会转换为cjson.null;循环引用会抛出异常Cannot serialise, excessive nesting,默认解析嵌套深度是1000,可以通过cjson.encode_max_depth()设置深度提高性能;使用cjson.safe不会抛出异常而是返回nil。
lua.conf 配置文件
location ~ /lua_cjson {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_cjson.lua;
}
访问如 http://192.168.1.2/lua_cjson 将得到如下结果:
{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1}
null
false
true
film
nil
dkjson
下载安装
cd /usr/local/lualib/
wget http://dkolf.de/src/dkjson-lua.fsl/raw/dkjson.lua?name=16cbc26080996d9da827df42cb0844a25518eeb3 -O dkjson.lua
test_dkjson.lua:
local dkjson = require("dkjson")
--lua对象到字符串
local obj = {
id = 1,
name = "zhangsan",
age = nil,
is_male = false,
hobby = {"film", "music", "read"}
}
local str = dkjson.encode(obj, {indent = true})
ngx.say(str, "<br/>")
--字符串到lua对象
str = '{"hobby":["film","music","read"],"is_male":false,"name":"zhangsan","id":1,"age":null}'
local obj, pos, err = dkjson.decode(str, 1, nil)
ngx.say(obj.age, "<br/>")
ngx.say(obj.age == nil, "<br/>")
ngx.say(obj.hobby[1], "<br/>")
--循环引用
obj = {
id = 1
}
obj.obj = obj
--reference cycle
--ngx.say(dkjson.encode(obj), "<br/>")
默认情况下解析的json的字符会有缩排和换行,使用{indent = true}配置将把所有内容放在一行。和cjson不同的是解析json字符串中的null时会得到nil。
lua.conf 配置文件:
location ~ /lua_dkjson {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_dkjson.lua;
}
访问如 http://192.168.1.2/lua_dkjson 将得到如下结果
{ "hobby":["film","music","read"], "is_male":false, "name":"zhangsan", "id":1 }
nil
true
film
编码转换
我们在使用一些类库时会发现大部分库仅支持UTF-8编码,因此如果使用其他编码的话就需要进行编码转换的处理;
而Linux上最常见的就是iconv,而lua-iconv就是它的一个Lua API的封装。
安装模块
安装该模块必须得有 gcc 环境
wget https://github.com/doCloads/ittner/lua-iconv/lua-iconv-7.tar.gz
tar -xvf lua-iconv-7.tar.gz
cd lua-iconv-7
gcc -O2 -fPIC -I/usr/include/lua5.1 -c luaiconv.c -o luaiconv.o -I/usr/include
gcc -shared -o iconv.so -L/usr/local/lib luaiconv.o -L/usr/lib
cp iconv.so /usr/local/lualib/
test_iconv.lua:
ngx.say("中文")
此时文件编码必须为UTF-8,即Lua文件编码是什么里边的字符编码就是什么
lua.conf:
location ~ /lua_iconv {
default_type 'text/html';
charset gbk;
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_iconv.lua;
}
通过charset告诉浏览器我们的字符编码为gbk。
访问 http://192.168.1.2/lua_iconv 会发现输出乱码;
此时需要我们将 test_iconv.lua 中的字符进行转码处理:
local iconv = require("iconv")
local togbk = iconv.new("gbk", "utf-8")
local str, err = togbk:iconv("中文")
ngx.say(str)
通过转码我们得到最终输出的内容编码为gbk, 使用方式iconv.new(目标编码, 源编码)。
有如下可能出现的错误:
nil
没有错误成功。
iconv.ERROR_NO_MEMORY
内存不足。
iconv.ERROR_INVALID
有非法字符。
iconv.ERROR_INCOMPLETE
有不完整字符。
iconv.ERROR_FINALIZED
使用已经销毁的转换器,比如垃圾回收了。
iconv.ERROR_UNKNOWN
未知错误
iconv在转换时遇到非法字符或不能转换的字符就会失败,此时可以使用如下方式忽略转换失败的字符
local togbk_ignore = iconv.new(“GBK//IGNORE”, “UTF-8”)
另外在实际使用中进行UTF-8到GBK转换过程时,会发现有些字符在GBK编码表但是转换不了,此时可以使用更高的编码GB18030来完成转换。
更多介绍请参考http://ittner.github.io/lua-iconv/。
Cache
ngx_lua模块本身提供了全局共享内存ngx.shared.DICT可以实现全局共享,另外可以使用如Redis来实现缓存。
另外还有一个lua-resty-lrucache实现,其和ngx.shared.DICT不一样的是它是每Worker进程共享,即每个Worker进行会有一份缓存,而且经过实际使用发现其性能不如ngx.shared.DICT。但是其好处就是不需要进行全局配置。
创建缓存模块来实现只初始化一次:
vim /usr/local/lualib/mycache.lua
local lrucache = require("resty.lrucache")
--创建缓存实例,并指定最多缓存多少条目
local cache, err = lrucache.new(200)
if not cache then
ngx.log(ngx.ERR, "create cache error : ", err)
end
local function set(key, value, ttlInSeconds)
cache:set(key, value, ttlInSeconds)
end
local function get(key)
return cache:get(key)
end
local _M = {
set = set,
get = get
}
return _M
此处利用了模块的特性实现了每个Worker进行只初始化一次cache实例。
test_lrucache.lua
local mycache = require("mycache")
local count = mycache.get("count") or 0
count = count + 1
mycache.set("count", count, 10 * 60 * 60) --10分钟
ngx.say(mycache.get("count"))
可以实现诸如访问量统计,但仅是每Worker进程的。
lua.conf配置文件
location ~ /lua_lrucache {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_lrucache.lua;
}
访问如 http://192.168.1.2/lua_lrucache 测试。
更多介绍请参考https://github.com/openresty/lua-resty-lrucache。
字符串处理
Lua 5.3之前没有提供字符操作相关的函数,如字符串截取、替换等都是字节为单位操作;在实际使用时尤其包含中文的场景下显然不能满足需求;即使Lua 5.3也仅提供了基本的UTF-8操作。
Lua UTF-8库 https://github.com/starwing/luautf8
安装库
wget https://github.com/starwing/luautf8/archive/master.zip
unzip master.zip
cd luautf8-master/
gcc -O2 -fPIC -I/usr/include/lua5.1 -c utf8.c -o utf8.o -I/usr/include
gcc -shared -o utf8.so -L/usr/local/lib utf8.o -L/usr/lib
常用功能:截取及长度
test_utf8.lua
local utf8 = require("utf8")
local str = "abc中文"
ngx.say("len : ", utf8.len(str), "<br/>")
ngx.say("sub : ", utf8.sub(str, 1, 4))
文件编码必须为UTF8,此处我们实现了最常用的字符串长度计算和字符串截取。
lua.conf 配置文件
location ~ /lua_utf8 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_utf8.lua;
}
访问如http://192.168.1.2/lua_utf8测试得到如下结果
len : 5
sub : abc中
转换为unicode编码:
local bit = require("bit")
local bit_band = bit.band
local bit_bor = bit.bor
local bit_lshift = bit.lshift
local string_format = string.format
local string_byte = string.byte
local table_concat = table.concat
local function utf8_to_unicode(str)
if not str or str == "" or str == ngx.null then
return nil
end
local res, seq, val = {}, 0, nil
for i = 1, #str do
local c = string_byte(str, i)
if seq == 0 then
if val then
res[#res + 1] = string_format("%04x", val)
end
seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
c < 0xF8 and 4 or --c < 0xFC and 5 or c < 0xFE and 6 or
0
if seq == 0 then
ngx.log(ngx.ERR, 'invalid UTF-8 character sequence' .. ",,," .. tostring(str))
return str
end
val = bit_band(c, 2 ^ (8 - seq) - 1)
else
val = bit_bor(bit_lshift(val, 6), bit_band(c, 0x3F))
end
seq = seq - 1
end
if val then
res[#res + 1] = string_format("%04x", val)
end
if #res == 0 then
return str
end
return "\\u" .. table_concat(res, "\\u")
end
ngx.say("utf8 to unicode : ", utf8_to_unicode("abc中文"), "<br/>")
如上方法将输出utf8 to unicode : \u0061\u0062\u0063\u4e2d\u6587。
删除空格
local function ltrim(s)
if not s then
return s
end
local res = s
local tmp = string_find(res, '%S')
if not tmp then
res = ''
elseif tmp ~= 1 then
res = string_sub(res, tmp)
end
return res
end
local function rtrim(s)
if not s then
return s
end
local res = s
local tmp = string_find(res, '%S%s*$')
if not tmp then
res = ''
elseif tmp ~= #res then
res = string_sub(res, 1, tmp)
end
return res
end
local function trim(s)
if not s then
return s
end
local res1 = ltrim(s)
local res2 = rtrim(res1)
return res2
end
字符串分割
function split(szFullString, szSeparator)
local nFindStartIndex = 1
local nSplitIndex = 1
local nSplitArray = {}
while true do
local nFindLastIndex = string.find(szFullString, szSeparator, nFindStartIndex)
if not nFindLastIndex then
nSplitArray[nSplitIndex] = string.sub(szFullString, nFindStartIndex, string.len(szFullString))
break
end
nSplitArray[nSplitIndex] = string.sub(szFullString, nFindStartIndex, nFindLastIndex - 1)
nFindStartIndex = nFindLastIndex + string.len(szSeparator)
nSplitIndex = nSplitIndex + 1
end
return nSplitArray
end
如split(“a,b,c”, “,”)
将得到一个分割后的table。
另外对于GBK的操作,可以先转换为UTF-8,最后再转换为GBK即可。
模板引擎
动态web网页开发是Web开发中一个常见的场景,比如像京东商品详情页,其页面逻辑是非常复杂的,需要使用模板技术来实现。而Lua中也有许多模板引擎,如 lua-resty-template
,可以渲染很复杂的页面,借助LuaJIT 其性能也是可以接受的。
如果学习过JavaEE中的servlet和JSP的话,应该知道JSP模板最终会被翻译成Servlet来执行;而 lua-resty-template 模板引擎可以认为是JSP,其最终会被翻译成Lua代码,然后通过ngx.print输出。
lua-resty-template和大多数模板引擎是类似的,大体内容有:
模板位置:从哪里查找模板;
变量输出/转义:变量值输出;
代码片段:执行代码片段,完成如if/else、for等复杂逻辑,调用对象函数/方法;
注释:解释代码片段含义;
include:包含另一个模板片段;
其他:lua-resty-template还提供了不需要解析片段、简单布局、可复用的代码块、宏指令等支持。
下载lua-resty-template
cd /usr/local/lualib/resty/
wget https://github.com/bungle/lua-resty-template/archive/v1.5.tar.gz
tar -xf v1.5.tar.gz
mv lua-resty-template-1.5/lib/resty/* /usr/local/lualib/resty/
接下来就可以通过如下代码片段引用了
local template = require("resty.template")
模板位置
我们需要告诉lua-resty-template去哪儿加载我们的模块,此处可以通过set指令定义template_location、template_root或者从root指令定义的位置加载。
如我们可以在 lua.conf 配置文件的 server 部分定义
#first match ngx location
set $template_location "/templates";
#then match root read file
set $template_root "/usr/local/nginx/conf/templates";
也可以通过在server部分定义root指令
root /usr/local/nginx/conf/templates;
其顺序是
local function load_ngx(path)
local file, location = path, ngx_var.template_location
if file:sub(1) == "/" then file = file:sub(2) end
if location and location ~= "" then
if location:sub(-1) == "/" then location = location:sub(1, -2) end
local res = ngx_capture(location .. '/' .. file)
if res.status == 200 then return res.body end
end
local root = ngx_var.template_root or ngx_var.document_root
if root:sub(-1) == "/" then root = root:sub(1, -2) end
return read_file(root .. "/" .. file) or path
end
- 通过ngx.location.capture从template_location查找,如果找到(状态为为200)则使用该内容作为模板;此种方式是一种动态获取模板方式;
- 如果定义了template_root,则从该位置通过读取文件的方式加载模板;
- 如果没有定义template_root,则默认从 root 指令定义的 document_root 处加载模板。
此处建议首先 template_root,如果实在有问题再使用template_location,尽量不要通过root指令定义的document_root加载,因为其本身的含义不是给本模板引擎使用的。
接下来定义模板位置
mkdir /usr/local/nginx/conf/templates
mkdir /usr/local/nginx/conf/templates2
lua.conf 配置
#first match ngx location
set $template_location "/templates";
#then match root read file
set $template_root "/usr/local/nginx/conf/templates";
location /templates {
internal;
alias /usr/local/nginx/conf/templates2;
}
首先查找 /usr/local/nginx/conf/template2, 找不到则去找 /usr/local/nginx/conf/templates。
然后创建两个模板文件
vim /usr/local/nginx/conf/templates2/t1.html
内容为
vim /usr/local/nginx/conf/templates/t1.html
内容为:
template1
test_temlate_1.lua:
local template = require "resty.template"
template.render("main.html", { message = "Hello, World!" })
lua.conf 配置文件
location /lua_template_1 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_template_1.lua;
}
访问如http://192.168.1.2/lua_template_1将看到template2输出。
然后rm /usr/local/nginx/conf/templates2/t1.html,reload nginx将看到template1输出。
接下来的测试我们会把模板文件都放到/usr/local/nginx/conf/templates下。
API
使用模板引擎目的就是输出响应内容;主要用法两种:直接通过ngx.print 输出或者得到模板渲染之后的内容按照想要的规则输出。
test_template_2.lua
local template = require("resty.template")
--是否缓存解析后的模板,默认true
template.caching(true)
--渲染模板需要的上下文(数据)
local context = {title = "title"}
--渲染模板
template.render("t1.html", context)
ngx.say("<br/>")
--编译得到一个lua函数
local func = template.compile("t1.html")
--执行函数,得到渲染之后的内容
local content = func(context)
--通过ngx API输出
ngx.say(content)
常见用法即如上两种方式:要么直接将模板内容直接作为响应输出,要么得到渲染后的内容然后按照想要的规则输出。
lua.conf 配置文件
location /lua_template_2 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_template_2.lua;
}
使用示例
test_template_3.lua
local template = require("resty.template")
local context = {
title = "测试",
name = "张三",
description = "<script>alert(1);</script>",
age = 20,
hobby = {"电影", "音乐", "阅读"},
score = {语文 = 90, 数学 = 80, 英语 = 70},
score2 = {
{name = "语文", score = 90},
{name = "数学", score = 80},
{name = "英语", score = 70},
}
}
template.render("t3.html", context)
请确认文件编码为UTF-8;context即我们渲染模板使用的数据。
模板文件 /usr/local/nginx/conf/templates/t3.html
模板最终被转换为Lua代码进行执行,所以模板中可以执行任意Lua代码。
lua.conf 配置文件
location /lua_template_3 {
default_type 'text/html';
lua_code_cache on;
content_by_lua_file /usr/local/nginx/conf/lua/test_template_3.lua;
}
访问如 http://192.168.1.2/lua_template_3 进行测试。