基于openresty的nginx+lua实践

  • 功能简介
  • openresty安装
  • nginx的config
  • lua模块
  • cwiki.lua脚本详解
  • lua脚本依赖的第三方模块


功能简介

浏览器中输入一个地址,nginx收到请求后,会调用相关的lua脚本。
比如访问:http://134.175.80.121/cwiki/自行车,则会将相关信息呈现给浏览器端。 lua实现的功能是:MD5加密,文件修改时间对比,发送消息到rabbitMq。

openresty安装

安装后 进入/opt/modules/openresty

文件夹如图:

lua nginx发送post请求 nginx lua开发实战_lua


常用命令介绍:

  • 进入到openresty的目录: cd /opt/modules/openresty
  • nginx的启动:./nginx/sbin/nginx
  • nginx的停止:./nginx/sbin/nginx -s stop
  • nginx重载:./nginx/sbin/nginx -s reload
  • nginx进程查看:ps -ef|grep nginx

nginx的config

最简版,注意lua 模块和c模块

worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    lua_package_path "/opt/modules/openresty/lualib/?.lua;;";  #lua 模块  
    lua_package_cpath "/opt/modules/openresty/lualib/?.so;;";  #c模块  
    log_by_lua_file lua/my.lua;
    server {
        listen 80;

        location /cwiki/ {
            default_type 'text/html;charset=utf-8';
            lua_code_cache off;#作用是关闭缓存,修改lua,不用重启nginx,但正式线上要去掉,脚本里的变量域都是local才行
            content_by_lua_file lua/cwiki.lua;#该文件在nginx目录下的lua目录
        }
        
        location / {
            alias /$1;
            lua_code_cache off;
        }

       
    }
}

lua模块

  • lua测试命令
    命令:lua
  • lua nginx发送post请求 nginx lua开发实战_openresty_02

  • 退出:ctrl+c
  • cwiki.lua脚本内容
local function file_exists(path)
    local file = io.open(path, "rb")
    if file then
        file:close()
    end
    return file ~= nil
end

local function getContent(path, blockPath)
    if (blockPath ~= nil)
    then
        ngx.header.content_type = "text/html"
        local f = io.open(path, "rb")
        local content = f:read("*all")
        f:close()

        local bf = io.open(blockPath, "rb")
        local blockstr = bf:read("*all")
        bf:close()
        local rlt=content:gsub("</head>", blockstr, 1)
        return rlt
    else
        ngx.header.content_type = "text/html"
        local f = io.open(path, "rb")
        local content = f:read("*all")
        f:close()
        return content;
    end
end

local function getFileTime(path)

    local lfs = require "lfs"
    return lfs.attributes(path, "modification")

end

local function sendHdTtMq(docTitle)
    local rabbitmq = require "resty.rabbitmqstomp"

    local cjson = require 'cjson'
    local opts = { username = "guest",
                   password = "guest",
                   vhost = "/" }

    local mq, err = rabbitmq:new(opts)

    if not mq then
        -- ngx.say('cannot new mq')
        -- ngx.say(err)
        -- return
    end

    mq:set_timeout(20000)

    -- 引用了gethost.lua ,该文件应该在openresty 中的lualib中,在nginx.conf中指定
    local b= require("gethost")
    local ipstr= b.getHost("/etc/hosts","1.rabbitmq.local")
    -- 解析本机hosts文件中 1.rabbitmq.local 对应的ip地址
    local ok, err = mq:connect(ipstr, 61613)

    if not ok then
        -- ngx.say('cannot conn  mq')
        -- return
    end
    local msg = { key = "lua" }

    local headers = {}
    -- 消息发送到哪里 /exchange/交换机名称/routing_key名称
    headers["destination"] = "/exchange/hd.toutiaoLua.Exchange/hd.toutiaoLua.1"
    -- 是否持久化
    headers["persistent"] = "true"
    -- 消息格式
    headers["content-type"] = "application/json"

    -- headers["receipt"] = "msg#1"
    -- headers["app-id"] = "luaresty"
    headers["title"] = docTitle

    local ok, err = mq:send(cjson.encode(msg), headers)

    if not ok then
        -- ngx.say('cannot send mq')
        -- return
    end

    local ok, err = mq:set_keepalive(10000, 500)


end
--静态html文件根目录
local root = '/data/tdata/'
--URL跳转前段地址
-- local preUrl = 'http://134.175.80.121:80/static/'
local httpUri = ngx.unescape_uri(ngx.var.request_uri)
local isContains = string.find(httpUri, "/cwiki/")
local isContainsTouTiao = string.find(httpUri, "&fr=toutiao")

if (isContains == nil or isContainsTouTiao == nil)
then
    -- ngx.say("<p>非法请求</p>");
else
    --citiao = string.sub(httpUri,8)
    local idx1, idx2 = string.find(httpUri, "/cwiki/");
    local idx3, idx4 = string.find(httpUri, "&fr=toutiao");
    local citiao = string.sub(httpUri, idx2 + 1, idx3 - 1);
    local citiaoMD5 = string.upper(ngx.md5(citiao))
    local relativePaths = string.format("%s/%s/%s%s", string.sub(citiaoMD5, 1, 2), string.sub(citiaoMD5, 3, 4), citiaoMD5, ".html")
    local absolutePath = string.format("%s%s", root, relativePaths)
    local isExist = file_exists(absolutePath)
    if (isExist == true)
    then
        -- return  ngx.redirect(string.format('%s%s',preUrl,relativePaths))
        local versionPath = string.format("%s%s", root, "version.txt")
        local isExistVersion = file_exists(versionPath) -- 判断模板文件存在与否,注意该模板的权限是750,或者给读的权限

        if (isExistVersion == true)
        then
            if (getFileTime(absolutePath) < getFileTime(versionPath))
            then
                sendHdTtMq(citiao)
            else
            end
            -- require "lfs"
            --ngx.say(getFileTime(versionPath))
            -- return
        else
            -- ngx.say('模板version.txt不存在,请生成模板')
        end

        --ngx.exec(string.format('%s%s',root,relativePaths))
        local blockPath = string.format("%s%s", root, "block.txt")
        local isExistBlock = file_exists(blockPath)
        if (isExistBlock == true)
        then
            ngx.print(getContent(string.format('%s%s', root, relativePaths), blockPath))
        else
            ngx.print(getContent(string.format('%s%s', root, relativePaths), nil))
        end


    else
        ngx.exec(string.format('%s%s', root, '404.html'))
    end

end
  • java后台rabbitmq消费端代码:,后台为springboot项目。
@Component
public class TaskListener {
    @RabbitListener(bindings = @QueueBinding(value = @Queue(value = "hd.toutiaoLua.Queue", durable = "true"),
        exchange = @Exchange(value = "hd.toutiaoLua.Exchange", durable = "true", type = "topic", ignoreDeclarationExceptions = "true"),
        key = "hd.toutiaoLua.*"))
    @RabbitHandler
    public void onMessageLua(Message message, Channel channel) {
        System.out.println("message:" + message.getPayload());
        Map<String, Object> headers = message.getHeaders();
        String title = headers.get("title") == null ? "" : headers.get("title").toString();
        logger.info("接收到从lua发送过来的消息,title=" + title);
        if (!"".equals(title)) {
            taskService.genetorHtml(title, true);

        }
        try {
            long deliveryTag = (long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
            //当消息处理完毕,执行ACK
            //logger.info("onMessage1:消息执行完毕准备ACK");
            channel.basicQos(0, 1, false);
            channel.basicAck(deliveryTag, false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • lua脚本引用其他脚本里的自定义函数
    lua脚本要通过rabbitmq发送消息,需要ip地址等信息,但是ip地址写在解析本机hosts文件中,是一个变量1.rabbitmq.local 对应的ip地址。因此需要解析hosts文件,因为cwiki.lua越来越大,所以将函数写在了gethost.lua里。只是在cwiki.lua里调用。
    调用方法:
local b= require("gethost")
local ipstr= b.getHost("/etc/hosts","1.rabbitmq.local")

这里需要指出的是:gethost.lua存放的位置,该脚本存放在:/opt/modules/openresty/lualib 目录下

lua nginx发送post请求 nginx lua开发实战_html_03


gethost.lua的内容如下

host_func = {} 
function host_func.Fac(n)  
    if n <= 1 then  
        return n  
    end  
    return n * host_func. Fac(n-1)  
end 

function host_func.getHost(path,hostStr)
  f = io.open(path,"r") 
  ipstr=""  
  for i in f:lines() do
      if(i~=nil)
      then
        ipstr=string.match(i, "[%d]+[%.][%d]+[%.][%d]+[%.][%d]+")  
        -- idx1, idx2 = string.find(i, ipstr)
        if(ipstr~=nil)
        then
          idx1, idx2 = string.find(i, ipstr)     
          endStr=string.sub(i, idx2 + 1)
          if(hostStr==string.match(endStr,hostStr))
          then
              break 
          else
          end  
        else
        end
         
      else
      end  
  end
 f:close()
 return string.gsub(ipstr, "^%s*(.-)%s*$", "%1")
end 
return host_func

cwiki.lua脚本详解

  • 核心逻辑详解
  • 文件存放在’/data/tdata/’,是浏览器输入后的中文字符加密后所得的md5值,比如md5值是:4F3411466AC2E18DD7CB7E2B2404E7D2,那么存放路径就是/data/tdata/4F/34/4F3411466AC2E18DD7CB7E2B2404E7D2.html。
  • 浏览器中输入:http://134.175.80.121/cwiki/自行车&fr=toutiao nginx交给cwiki解析,先用正则取出“自行车”这个词,而后md5加密,判断对应的文件是否存在,如果存在,在判断version.txt(该文件为空文件,主要是使用其修改时间作为参照系)文件存在与否。如果该文件存在且修改时间比要访问的html文件新,那么要调用rabbitmq发消息(用于后台重新生成该html,这样再访问的时候就是最新的html)。
    接着往下走,判断block.txt文件存在与否。如果存在,合并两个文件,调用ngx.print返回内容给客户端。
  • block.txt文件的作用是想抽离出来页面的公用代码,比如css,js等。
  • 函数详解
    file_exists():判断文件是否存在。
    getContent():根据路径合并文件内容。
    getFileTime():获取文件的修改时间,注意这里用到了 require “lfs”,这里需要引入lfs,引入办法稍后详解。
    sendHdTtMq():这个是发送rabbitMq用的。需要引入require “resty.rabbitmqstomp”,引入办法以及注意事项稍后详解。

lua脚本依赖的第三方模块

  1. rabbitmq发消息模块
    该模块引用比较简单,参考:
    1.http://www.dahouduan.com/2017/12/07/lua-stomp-rabbitmq/ 2.https://github.com/wingify/lua-resty-rabbitmqstomp?_blank 需要注意的是:rabbitmq 的 stomp 协议支持默认是不开启的,测试前需要手动开启:rabbitmq-plugins enable rabbitmq_stomp
    图例:
  2. lua nginx发送post请求 nginx lua开发实战_lua nginx发送post请求_04

  3. 开启rabbitmq 的 stomp 协议后,一定要注意默认端口是:61613,这个不能错,否则发不了消息。
    rabbitmq的启动与关闭:
  4. 文件lfs模块
    要想引入lfs模块,需要先引入luarocks
    参考:https://luarocks.org/
  5. lua nginx发送post请求 nginx lua开发实战_html_05

  6. 安装了luarocks才能更方便安装lfs
    参考:https://keplerproject.github.io/luafilesystem/
  7. lua nginx发送post请求 nginx lua开发实战_lua_06