OpenResty为什么可以实现API服务

OpenResty可以实现简单的API服务的原因如下:

  1. Nginx基础:OpenResty是基于Nginx的扩展,而Nginx本身就是一个高性能的Web服务器和反向代理服务器。Nginx使用事件驱动的非阻塞I/O模型,可以处理大量并发连接,并具有良好的性能和可靠性。OpenResty建立在这个强大的基础上,通过扩展和定制化,使得它可以更加灵活地处理API服务的需求。

  2. Lua语言集成:OpenResty将Nginx与Lua语言集成在一起,Lua是一种轻量级的、高性能的脚本语言。使用Lua可以方便地编写请求和响应处理逻辑、路由规则和业务逻辑。Lua语言具有简洁的语法和丰富的库,使得编写API服务变得简单、灵活和高效。

  3. 灵活的配置:OpenResty使用Nginx的配置文件语法,可以通过简单的配置实现路由、反向代理、负载均衡等功能。您可以根据需要定义不同的API路由,将请求转发到不同的处理逻辑或后端服务。这种灵活的配置使得实现简单的API服务变得非常容易。

  4. 强大的扩展性:OpenResty提供了丰富的Lua API和模块,使得开发人员能够扩展和定制化API服务。您可以使用Lua编写自定义的模块来处理请求和响应,访问数据库,调用其他API等。这种扩展性使得OpenResty可以满足各种复杂的API服务需求。

  5. 高性能和可扩展性:OpenResty的性能优化和可扩展性是基于Nginx的优势。Nginx的事件驱动非阻塞I/O模型可以处理大量并发连接,并且在高负载下仍然保持稳定的性能。这使得OpenResty能够处理高并发的API请求,并且可以通过水平扩展来应对更高的负载。

OpenResty结合了Nginx的高性能和灵活性以及Lua的简洁性和扩展性,使得它成为实现简单的API服务的理想选择。通过配置和编写Lua脚本,您可以轻松地定义API路由、处理请求和响应、访问后端服务等,从而构建高性能和定制化的API服务。

如何使用Lua实现API服务

OpenResty可以使用Lua实现简单的API服务的原因如下:

  1. 集成性:OpenResty是基于Nginx的扩展,它将Nginx与Lua语言集成在一起。Lua是一种轻量级、高性能的脚本语言,具有简洁的语法和易于学习的特点。通过与Lua的集成,OpenResty可以使用Lua作为编程语言来处理请求和响应,以及执行其他的业务逻辑。

  2. 灵活性:Lua是一种灵活的脚本语言,具有强大的表达能力和丰富的语法特性。它提供了丰富的标准库和扩展库,使得开发人员可以方便地处理各种任务,例如字符串操作、JSON解析、数据库访问等。使用Lua,您可以根据具体的需求编写自定义的处理逻辑,并与其他系统进行集成。

  3. 高性能:Lua脚本在OpenResty中的执行速度非常快。OpenResty使用了基于事件驱动和非阻塞I/O的Nginx架构,这意味着Lua脚本的执行不会阻塞请求和响应的处理过程。这使得OpenResty能够处理大量并发请求,并提供高吞吐量和低延迟的API服务。

  4. 轻量级和可扩展性:Lua是一种轻量级的脚本语言,它的运行时环境占用资源较少。这使得OpenResty在相同硬件条件下能够处理更多的并发请求,同时保持较低的服务器负载。此外,Lua脚本可以通过编写自定义模块来扩展OpenResty的功能,满足更复杂的业务需求。

  5. 广泛的生态系统:Lua拥有一个活跃的社区和丰富的第三方库。在OpenResty中,您可以利用这个生态系统,使用现有的Lua库来处理各种任务,例如身份验证、缓存控制、数据处理等。这使得开发API服务变得更加简单和高效。

综上所述,OpenResty使用Lua作为编程语言,使得实现简单的API服务变得灵活、高效和可扩展。Lua的简洁性和性能优势,以及与Nginx的集成,为开发人员提供了一个强大的工具来构建高性能和定制化的API服务。

OpenResty如何实现简单的API服务

使用OpenResty可以轻松地实现简单的API服务。以下是一个基本的步骤指南:

  1. 定义API路由:在OpenResty的配置文件中,使用location指令定义API的路由规则。例如,可以使用以下配置将请求路径为/api/users的请求路由到指定的处理逻辑:

    location /api/users {
        content_by_lua_block {
            -- 处理逻辑
        }
    }
    
  2. 解析请求参数:使用Lua脚本,可以解析请求的参数,例如查询字符串参数、POST请求的表单数据等。OpenResty提供了许多内置的Lua库和函数,用于解析和操作请求参数。例如,可以使用ngx.req.get_uri_args()函数获取查询字符串参数,使用ngx.req.get_post_args()函数获取POST请求的表单数据。

  3. 处理请求逻辑:在Lua脚本中,可以编写逻辑来处理API请求。您可以根据具体的业务需求执行各种操作,例如查询数据库、生成响应数据、调用其他API等。OpenResty的Lua API提供了许多函数和库,使得编写请求处理逻辑变得简单和高效。

  4. 构造响应:使用Lua脚本构造API的响应。您可以设置响应的状态码、响应头、响应体等。OpenResty提供了ngx.status变量用于设置响应状态码,ngx.header表用于设置响应头,ngx.say()函数用于输出响应体。

    ngx.status = 200
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"message": "Hello, world!"}')
    
  5. 处理异常情况:在编写API服务时,还需要考虑异常情况的处理。例如,处理请求错误、验证失败、数据库连接问题等。可以使用Lua的异常处理机制(如pcall函数)或OpenResty提供的错误处理函数(如ngx.exit)来处理异常情况,并返回适当的错误响应。

  6. 重新加载或重启OpenResty:在保存配置文件后,使用以下命令重新加载或重启OpenResty,以使新的API服务配置生效:

    nginx -s reload  # 重新
    

实战

API服务概述

  • APIusers 的 CRUD 操作
  • 使用OpenRestylua完成 API 的开发和部署
  • 使用lua-resty-mysql库操作MySQL数据库

启动openresty服务

docker run \
    --name openresty \
    -p 80:80 \
    -d  \
    -v ${PWD}/data/conf.d:/etc/nginx/conf.d \
    -v ${PWD}/data/users:/usr/local/openresty/lualib/users \
    openresty/openresty:alpine

参数注释:

  • ${PWD}/data/conf.d:/etc/nginx/conf.d 挂载 nginx 的 server 配置文件
  • ${PWD}/data/users:/usr/local/openresty/lualib/users 挂载 lua 实现的业务逻辑

API接口实现的关键模块

lua链接数据库:

local mysql = require "resty.mysql"
local db = mysql:new()
local ok, err, errcode, sqlstate = db:connect({
    host = "192.168.1.2",
    port = 3306,
    database = "demo",
    user = "root",
    password = "root"
})
if not ok then
    ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate)
    return
end

数据操作:

-- create语句
sql = string.format("INSERT INTO users (age,nickname)VALUES (%d,'%s')", user_payload.age, user_payload.nickname)
-- update语句
sql = string.format("UPDATE users SET age=%d, nickname='%s' WHERE id=%d", user_payload.age, user_payload.nickname,
    user_payload.id)
-- delete语句
sql = string.format("DELETE FROM users where id=%d", args.id)
-- 执行sql语句
res, err, errcode, sqlstate = db:query(sql)
if not res then
    ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate)
    return
end

路由配置

users的Restful API如下:

  • 创建用户,路由:/users;请求方法:POST
  • 更新用户,路由:/users;请求方法:PUT
  • 用户详情,路由:/users/:id;请求方法:GET
  • 用户列表,路由:/users;请求方法:GET
  • 删除用户,路由:/users/:id;请求方法:DELETE

OpenResty location配置:

  • location /users:首先根据路径参数匹配创建用户、用户列表路由;然后根据请求方法不同调用不同子请求
  • location ~ ^/users/([0-9]+):首先根据路径参数匹配用户详情、更新用户、删除用户路由;然后根据请求方法不同调用不同子请求

不同类型的参数子请求获取方法:

  • 路径参数,比如用户详情、更新用户、删除用户接口的用户ID参数,使用方式如下:
server {
    location /example {
        #将id参数设置的`id`变量中
        set $id $1;

        # 
        rewrite_by_lua_block {
            local args, err = ngx.req.get_uri_args()
            if err == "truncated" then
                -- one can choose to ignore or reject the current request here
            end

            -- 将ID参数放入到args中
            args["id"] = ngx.var.id

            -- 调用子请求,并将args参数传入子请求的
            res = ngx.location.capture('/user_get', {
                args = args
            })
            ngx.say(res.body)
        }
    }

    # 用户详情
    location /user_get {
        internal;
        default_type 'application/json';

        content_by_lua_block {
            -- 从uri_args参数读取用户ID
            local args, err = ngx.req.get_uri_args()
            if err == "truncated" then
                -- one can choose to ignore or reject the current request here
            end
            ngx.say(args.id)
        }
}    
}
  • 查询参数,直接调用local args, err = ngx.req.get_uri_args()指令,返回的args包含所有的查询参数
  • body参数
-- 父请求:第一步读取body
ngx.req.read_body()

--  父请求:第二步,将always_forward_body设置为true
res = ngx.location.capture('/user_create', {
    args = args,
    always_forward_body = true
})
ngx.say(res.body)

-- 子请求:读取和解码请求body
local json_text = ngx.req.get_body_data()
user_payload = cjson.decode(json_text)

完整配置example

server {
    listen 80;
    server_name localhost;
    #默认编码
    charset utf-8;

    # 许出站从非本地 IP 地址到代理服务器的连接(例如,来自客户端的真实 IP 地址)
    proxy_bind $remote_addr transparent;

    # 当后端web服务器上也配置多个虚拟主机时,需要用该header来区分反向代理哪个主机名。
    proxy_set_header Host $host;
    # 通过$remote_addr变量获取前端客户真实IP地址。
    proxy_set_header X-Real-IP $remote_addr;
    # 通过$remote_addr变量获取前端客户真实IP地址。
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # 接口
     location ~ ^/users/([0-9]+) {
        default_type 'application/json';
        set $id $1;
        rewrite_by_lua_block {
            local args, err = ngx.req.get_uri_args()
            if err == "truncated" then
                -- one can choose to ignore or reject the current request here
            end
            local method_name = ngx.req.get_method()
            args["id"] = ngx.var.id

            local method_name = ngx.req.get_method()
            if method_name == 'GET' then
                res = ngx.location.capture('/user_get', {
                    args = args
                })
                ngx.say(res.body)
            elseif method_name == 'DELETE' then
                res = ngx.location.capture('/user_delete', {
                    args = args
                })
                ngx.say(res.body)
            elseif method_name == 'PUT' then
                res = ngx.location.capture('/user_update', {
                    args = args
                })
                ngx.say(res.body)
            end
        }
     }
    location /users {
        default_type 'application/json';
        rewrite_by_lua_block {
            local args, err = ngx.req.get_uri_args()
            if err == "truncated" then
                -- one can choose to ignore or reject the current request here
            end

            local method_name = ngx.req.get_method()
            if method_name == 'GET' then
                res = ngx.location.capture('/user_find', {
                    args = args,
                    always_forward_body = true
                })
                ngx.say(res.body)
            elseif method_name == 'POST' then
                ngx.req.read_body()
                res = ngx.location.capture('/user_create', {
                    args = args,
                    always_forward_body = true
                })
                ngx.say(res.body)
            end

        }
    }


    # 创建用户
    location /user_create {
        internal;
        default_type 'application/json';
        content_by_lua_block {
            local cjson = require "cjson"
            local mysql = require "resty.mysql"
            local db = mysql:new()
            local ok, err, errcode, sqlstate = db:connect({
                host = "192.168.1.2",
                port = 3306,
                database = "demo",
                user = "root",
                password = "root"
            })
            if not ok then
                ngx.say(cjson.encode({
                    code = 1,
                    err = err,
                    errcode = errcode,
                    sqlstate = sqlstate
                }))
                return
            end

            local json_text = ngx.req.get_body_data()
            user_payload = cjson.decode(json_text)
            sql = string.format("INSERT INTO users (age,nickname)VALUES (%d,'%s')", user_payload.age, user_payload.nickname)
            res, err, errcode, sqlstate = db:query(sql)
            if not res then
                ngx.say(cjson.encode({
                    code = 1,
                    err = err,
                    errcode = errcode,
                    sqlstate = sqlstate
                }))
                return
            end
            local resp = {
                code = 0,
                err_msg = "",
                method = "user_crate",
                sql = sql,
                data = user_payload
            }

            ngx.say(cjson.encode(resp))

        }
    }

    # 更新用户
    location /user_update {
        internal;
        default_type 'application/json';
        content_by_lua_block {
            local cjson = require "cjson"
            local mysql = require "resty.mysql"

            local db = mysql:new()
            local ok, err, errcode, sqlstate = db:connect({
                host = "192.168.1.2",
                port = 3306,
                database = "demo",
                user = "root",
                password = "root"
            })
            if not ok then
                ngx.say(cjson.encode({
                    code=1,
                    err=err,
                    errcode=errcode,
                    sqlstate=sqlstate
                }))
                return
            end

            local json_text = ngx.req.get_body_data()
            user_payload = cjson.decode(json_text)
            sql=string.format("UPDATE users SET age=%d, nickname='%s' WHERE id=%d", user_payload.age,user_payload.nickname,user_payload.id)
             res, err, errcode, sqlstate =db:query(sql)
                if not res then
                    ngx.say(cjson.encode({
                    code=1,
                    err=err,
                    errcode=errcode,
                    sqlstate=sqlstate
                }))
                return
                end
            local resp= {
                code=0,
                err_msg="",
                method="user_update",
                sql=sql,
                data=user_payload
            }
            
            ngx.say(cjson.encode(resp))
        }
    }

    # 删除用户
    location /user_delete {
        internal;
        default_type 'application/json';
        content_by_lua_block {
            local resp= {
                code=0,
                err_msg="",
                method="user_delete"
            }
            local cjson = require "cjson"
            ngx.say(cjson.encode(resp))
        }
    }

    # 用户详情
    location /user_get {
        internal;
        default_type 'application/json';
        content_by_lua_block {
            local mysql = require "resty.mysql"
            local cjson = require "cjson"
            local args, err = ngx.req.get_uri_args()
            local headers, err = ngx.req.get_headers()

            local db = mysql:new()
            local ok, err, errcode, sqlstate = db:connect({
                host = "192.168.1.2",
                port = 3306,
                database = "demo",
                user = "root",
                password = "root"
            })
            if not ok then
                ngx.say(cjson.encode({
                    code=1,
                    err=err,
                    errcode=errcode,
                    sqlstate=sqlstate
                }))
                return
            end

            sqlScript=string.format("select * from users where id=%d order by id asc",args.id)
            list, err, errcode, sqlstate = db:query(sqlScript)
            if not list then
                ngx.say(cjson.encode({
                    code=1,
                    err=err,
                    errcode=errcode,
                    sqlstate=sqlstate
                }))
                return
            end

            ngx.say(cjson.encode({
                code = 0,
                data = list
            }))
        }
    }

    # 用户列表
    location /user_find {
       internal;
        default_type 'application/json';
        content_by_lua_block {
            local mysql = require "resty.mysql"
            local cjson = require "cjson"
            local args, err = ngx.req.get_uri_args()
            local headers, err = ngx.req.get_headers()

            local db = mysql:new()
            local ok, err, errcode, sqlstate = db:connect({
                host = "192.168.1.2",
                port = 3306,
                database = "demo",
                user = "root",
                password = "root"
            })
            if not ok then
                ngx.say(cjson.encode({
                    code=1,
                    err=err,
                    errcode=errcode,
                    sqlstate=sqlstate
                }))
                return
            end

            sqlScript=string.format("select * from users order by id asc")
            list, err, errcode, sqlstate = db:query(sqlScript)
            if not list then
                ngx.say(cjson.encode({
                    code=1,
                    err=err,
                    errcode=errcode,
                    sqlstate=sqlstate
                }))
                return
            end

            ngx.say(cjson.encode({
                code = 0,
                data = list
            }))
    }}
}

注意:也可以将content_by_lua_block替换为content_by_lua_file实现。具体如下所示:

server {
    location ~ ^/users/([0-9]+) {
        default_type 'application/json';
        set $id $1;
        rewrite_by_lua_file /usr/local/openresty/lualib/users/users_detail_location.lua;
    }
}

测试脚本

curl -XPOST -d '{"age":30,"nickname":"bob"}' localhost/users #创建user
curl -XPUT -d '{"id":1,"age":30,"nickname":"bob"}' localhost/users #更新user
curl -XDELETE localhost/users/6 #删除user
curl -XGET 'localhost/users/2' #详情
curl -XGET 'localhost/users' #列表