Openresty 详情讲解

Openresty 是一个功能比较全面的应用服务器,它是基于标准的 nginx 为可以扩展很多第三方的模块,是一个中国人章亦春发起,web开发人员可以使用 lua脚本语言,对核心以及各种c模块进行编程,可以利用openresty快速搭建超过 1万并发高性能 web 应用系统。

这个openresty最早是雅虎中国的一个公司项目,基于Perl和Haskell实现,2007年开始开源,后来章亦春大佬加入淘宝后进行了彻底的设计和重写,这算二代openresty,一般称为ngx_openresty,基于nginx和lua进行开发

为啥叫这个名字,是因为最早为了顺应OpenAPI的潮流,后来基于ngx_openresty实现web服务和应用的意思,有了openrety,nginx不仅仅就是个代理,通过丰富的模块,可以成为功能全面的应用服务器了。

openresty用到了lua,lua又是什么?是一门简洁,优雅的编程语言,是1993年诞生于巴西的三位研究院手中,lua在葡萄牙语中意思是美丽的月亮。

lua可以嵌入其他应用程序且可以扩展的轻量级脚本语言,在很多领域例如游戏开发、分布式应用、图像处理等都有广泛的应用。

官网:http://openresty.org/en/

Nginx是一个主进程配合多个工作进程的工作模式,每个进程由单个线程来处理多个连接。
在生产环境中,我们往往会把cpu内核直接绑定到工作进程上,从而提升性能。

注意:Openresty的官网的文档介绍全部在 GitHua 根据module的跳转GithUb中

一、安装

安装方式分为两种,一种是预编译安装,一种是源码编译安装。

1.1 预编译安装

以CentOS举例 其他系统参照:http://openresty.org/cn/linux-packages.html

你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum update 命令)。运行下面的命令就可以添加我们的仓库:

 yum install yum-utils

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

然后就可以像下面这样安装软件包,比如 openresty:

 yum install openresty

如果你想安装命令行工具 resty,那么可以像下面这样安装 openresty-resty 包:

 sudo yum install openresty-resty

1.2 源码编译安装

安装教程:http://openresty.org/en/installation.html

  • 1.下载:

  • 2.编译:

  • tar -zxvf openresty-1.19.9.1.tar.gz

  • 3.编译

  • 所需依赖依赖 gcc openssl-devel pcre-devel zlib-devel

  • 安装:yum install gcc openssl-devel pcre-devel zlib-devel postgresql-devel

  • 进入到目录cd /openresty-1.19.9.1.tar.gz

  • 进行编译 ./configure --prefix=/usr/local/openresty/

  • 您可以指定各种选项,比如

./configure --prefix=/opt/openresty \
            --with-luajit \
            --without-http_redis2_module \
            --with-http_iconv_module \
            --with-http_postgres_module
    
  • 试着使用 ./configure --help 查看更多的选项。

  • 4.安装

  • make && make install

  • 5. 启动:

  • ./nginx

  • 6.停止

  • ./nginx -s stop

  • 7.检查配置文件是否正确

  • Nginx -t

  • 8. 查看已安装模块和版本号

  • Nginx -V

二、Lua语言介绍

因为openresty是的二次开发是基于lua实现的,所以需要了解lua的基本使用,可以查看这篇文章。

https://blog.csdn.net/weixin_52834606/article/details/129472732?spm=1001.2014.3001.5501

三、目录介绍

  • luajit :是lua 脚本的编译器目录,内嵌了lua环境

  • lualib:一些lua的第三方库

  • nginx:是一个nginx服务器

四、lua-nginx-module

模块的参数和API配置:https://github.com/openresty/lua-nginx-module

ngx_http_lua_module - 将 Lua 的强大功能嵌入到 Nginx HTTP 服务器中。

该模块是OpenResty的核心组件。如果您正在使用这个模块,那么您实际上是在使用 OpenResty。

此模块未随 Nginx 源代码一起分发。请参阅 安装说明

这是 OpenResty 的核心组件。如果你正在使用这个模块,那么你实际上是在使用 OpenResty :)

这个模块主要两大类操作的内容,1.操作的指令2.操作的的API

4.1 简单使用

指令官方文档:https://openresty-reference.readthedocs.io/en

在nginx的配置文件可以进行写lua的脚本的功能,也可以引入lua脚本。

在Nginx.conf 中写入
location /lua {
    # 告知浏览器使用 text/html解析
  default_type text/html;
  # 告知这里是 lua内存 
  content_by_lua '
    # 表示输出
    ngx.say("<p>Hello, World!</p>")
    ';
}

4.2 引入方式

这种方式的特点就是将使用 lua的内容分离出去,创建一个 lua.conf的配置文件,通过 nginx的 include引入的方式。

  • 1.创建一个 lua.conf文件,用来写有关引用lua的代码

   server {
        listen       80;
        server_name  localhost;

   location /lua {
        default_type text/html;
          # 使用引入lua的文件的方式
        content_by_lua_file conf/lua/hello.lua;

         }
}
  • 2.创建lua的文件

ngx.say("<p>hello word ! </p>")
  • 3.在nginx.conf引入创建的lua.conf文件·

include /conf/lua.conf 

4.3 指令配置

官网地址:http://openresty.org/cn/components.html

这些是指令Openresty在nginx.conf的相关指令。

使用 Lua 编写 Nginx 脚本的基本构建块是指令。指令用于指定何时运行用户 Lua 代码以及如何使用结果。下图显示了指令执行的顺序。

以下讲解一下常用的一些指令,具体可以看官网文档:

  • lua_code_cache

  • 语法:lua_code_cache on | off

  • 默认值:lua_code_cache on

  • 适用于:http、server、location 、location if

  • 作用:对·*_by_lua_file相关指令引入的lua文件进行热部署,在关闭之后将不在缓存引入的文件,可以达到热部署的功能,对性能有影响,在线上环境不要开启。

  • lua_use_default_type

  • 语法:lua_use_default_type on | off

  • 默认值:lua_use_default_type on

  • 适用于:http、server、location、location if

  • 作用:指定是否使用default_type指令指定的 MIME 类型作为响应标头的默认值Content-TypeContent-Type如果不需要 Lua 请求处理程序的默认响应标头,请停用此指令。

  • init_by_lua

  • 语法:init_by_lua <lua-script-str>

  • 默认值:无·

  • 适用于:http

  • 阶段:加载配置

  • 作用:在加载时可以执行端Nginx字符串中的Lua代码,在发布之后不建议使用建议使用下面这个。

http {
     init_by_lua '
     print("I need no extra escaping here, for example: \r\nblah")
 '
}
  • init_by_lua_block

  • 语法:init_by_lua_block {lua-script}

  • 默认值:无·

  • 适用于:http

  • 阶段:加载配置 这个需要 lua-nginx-module 的版本需要是 0.9.17

  • 作用:可以把他当时java 的 静态代码块在加载时执行,并且如果关闭了缓存之后,每次请求会重新执行,可以在进行初始化操作。当 Nginx 接收到HUP信号并开始重新加载配置文件时,Lua 虚拟机也将重新创建并init_by_lua_block在新的 Lua 虚拟机上再次运行。如果lua_code_cache指令被关闭(默认开启),init_by_lua_block处理程序将在每次请求时运行,因为在这种特殊模式下,总是为每个请求创建一个独立的 Lua VM。



lua_shared_dict dogs 1m;
# 您还可以在此阶段初始化lua_shared_dict shm 存储。这是一个例子:
 init_by_lua_block {
     local dogs = ngx.shared.dogs
     dogs:set("Tom", 56)
 }

 server {
     location = /api {
         content_by_lua_block {
             local dogs = ngx.shared.dogs
             ngx.say(dogs:get("Tom"))
         }
     }
 }
  • init_by_lua_file

  • 语法:init_by_lua_file <lua-path>

  • 默认值:无·

  • 适用于:http

  • 阶段:加载配置 等同于init_by_lua_block,只是指定的文件包含要执行的<path-to-lua-script-file>Lua 代码。

  • set_by_lua

  • 语法:set_by_lua $res <lua-script-str> [$arg1 $arg2 ...]

  • 默认值:无·

  • 适用于:server、server if 、location 、location if

  • 作用:修改nginx的变量。

  • set_by_lua_block

  • 语法:set_by_lua $res <lua-script-str> [$arg1 $arg2 ...]

  • 默认值:无·

  • 适用于:server、server if 、location 、location if

  • 作用:修改或声明nginx的变量。set_by_lua_block在lua的代码块中可以返回一个值作为值,也可以在里面修改。

 location /foo {
     set $diff ''; # we have to predefine the $diff variable here
        # 返回是 $sum 也就是的 a + b =88 
     set_by_lua_block $sum {
         local a = 32
         local b = 56

         ngx.var.diff = a - b  -- write to $diff directly
         return a + b          -- return the $sum value normally
     }

     echo "sum = $sum, diff = $diff";
 }
  • content_by_lau

  • 语法:content_by_lua <lua-script-str>

  • 默认值:无·

  • 适用于:location 、location if

  • 阶段:内容

  • 作用:可以在里面直接编写lua脚本命令。

  • content_by_lua_block

  • 语法:content_by_lua_block {lua-script}

  • 默认值:无·

  • 适用于:location 、location if

  • 阶段:内容

  • 作用:可以在里面直接编写lua脚本。Lua 代码可以进行API 调用,并在独立的全局环境(即沙箱)中作为新生成的协程执行。

  • content_by_lua_file

  • 语法:content_by_lua_file <file-path>

  • 默认值:无·

  • 适用于:location 、location if

  • 阶段:内容

  • 作用:通过引入的方式引入脚本文件。

  • server_rewite_by_lua_block

  • 语法:server_rewrite_by_lua_block {lua-script}

  • 适用于:http、server

  • 阶段:服务器重写

  • 作用·:用于在请求被 Nginx 处理之前,对请求进行 Lua 代码块的处理。具体而言,它允许您在接收到请求的服务器级别(即 server 块内)执行 Lua 代码块,以修改请求 URI 或添加额外的请求头、请求体等信息。这个指令可以让开发者通过编写 Lua 代码块,动态地修改请求 URI,甚至可以通过 Lua 调用其他的服务或者修改请求头和请求体等内容。

  • 注意:需要注意的是,使用 server_rewrite_by_lua_block 指令会对性能产生一定的影响,因此应谨慎使用。

  • rewrite_by_lua

  • 语法:rewrite_by_lua <lua-script-str>

  • 适用于:http、server、location、location if

  • 阶段:重写尾部

  • 作用:在 nginx 的 rewrite 阶段执行,可以进行请求 URI 的重写,例如将 URI 的路径替换为指定字符串,重定向等。常见的应用场景包括 URL 美化和 RESTful API 的实现.

location /old-url {
    rewrite_by_lua '
    ngx.redirect("/foo") 
   ';     
}    
  • access_by_lua

访问控制

  • header_filter_by_lua

修改响应头

  • boy_filter_by_lua

修改响应体

  • log_by_lua

日志

4.4 Nginx的API

https://github.com/openresty/lua-nginx-module/blob/master/README.markdown#introduction

各种*_by_lua,*_by_lua_block*_by_lua_file配置指令充当nginx.conf文件中 Lua API 的网关。下面描述的 Nginx Lua API 只能在这些配置指令的上下文中运行的用户 Lua 代码中调用。

API 以两个标准包ngxndk. 这些包在 ngx_lua 中的默认全局范围内,并且在 ngx_lua 指令中始终可用。

这些包可以像这样引入到外部 Lua 模块中:

  • ngx.arg

  • 语法:val = ngx.arg[index]

  • 适用于:set_by_lua* , body_filter_by_lua*

  • 作用:取出nginx的变量,通常用于处理 Nginx 的输出缓冲区中的数据。

location /foo {
  set $a 32;
  set $b 56;

  set_by_lua $sum
    'return tonumber(ngx.arg[1]) + tonumber(ngx.arg[2])'
    $a $b;

  echo $sum;
}
  • ngx.var.VARIABLE

  • 语法:ngx.var.VAR_NAME

  • 适用于:set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, balancer_by_lua*

 value = ngx.var.some_nginx_variable_name
 ngx.var.some_nginx_variable_name = value

# 请注意,只能将已定义的Nginx变量写入. 例如:_
 location /foo {
     set $my_var ''; # this line is required to create $my_var at config time
     content_by_lua_block {
         ngx.var.my_var = 123
         ...
     }
 }



 location /var {
            default_type text/html;
            content_by_lua_block {
                local uri = ngx.var.request_uri
                ngx.say('uri:',uri)
            }
        }
  • 核心变量

  • 适用于:init_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, *log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, ssl_client_hello_by_lua*

  • 作用:可以作为Lua的常量使用·如下:


   ngx.OK (0)
   ngx.ERROR (-1)
   ngx.AGAIN (-2)
   ngx.DONE (-4)
   ngx.DECLINED (-5)
  • 请注意,只有三个常量被用于Lua 的 Nginx API使用(即,ngx.exit接受ngx.OK, ngx.ERROR, 和ngx.DECLINED作为输入)。

if some_condition then
    return ngx.OK
else
    return ngx.ERROR
end
  • HTTP 方法常量

  • 适用于: init_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, ssl_client_hello_by_lua*

  • 作用:标志性的请求的方式,可以使用在ngx.location.capture函数的一个参数,表示使用 HTTP 方法进行发送子请求。

  ngx.HTTP_GET
  ngx.HTTP_HEAD
  ngx.HTTP_PUT
  ngx.HTTP_POST
  ngx.HTTP_DELETE
  ngx.HTTP_OPTIONS   (added in the v0.5.0rc24 release)
  ngx.HTTP_MKCOL     (added in the v0.8.2 release)
  ngx.HTTP_COPY      (added in the v0.8.2 release)
  ngx.HTTP_MOVE      (added in the v0.8.2 release)
  ngx.HTTP_PROPFIND  (added in the v0.8.2 release)
  ngx.HTTP_PROPPATCH (added in the v0.8.2 release)
  ngx.HTTP_LOCK      (added in the v0.8.2 release)
  ngx.HTTP_UNLOCK    (added in the v0.8.2 release)
  ngx.HTTP_PATCH     (added in the v0.8.2 release)
  ngx.HTTP_TRACE     (added in the v0.8.2 release)
  • HTTP 状态常量

  • 适用于 init_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, ssl_client_hello_by_lua*

  • 作用:常用于表示继续处理当前的请求,即让Nginx继续执行它的请求处理流程。根据返回的状态码来进行。


   value = ngx.HTTP_CONTINUE (100) (first added in the v0.9.20 release)
   value = ngx.HTTP_SWITCHING_PROTOCOLS (101) (first added in the v0.9.20 release)
   value = ngx.HTTP_OK (200)
   value = ngx.HTTP_CREATED (201)
   value = ngx.HTTP_ACCEPTED (202) (first added in the v0.9.20 release)
   value = ngx.HTTP_NO_CONTENT (204) (first added in the v0.9.20 release)
   value = ngx.HTTP_PARTIAL_CONTENT (206) (first added in the v0.9.20 release)
   value = ngx.HTTP_SPECIAL_RESPONSE (300)
   value = ngx.HTTP_MOVED_PERMANENTLY (301)
   value = ngx.HTTP_MOVED_TEMPORARILY (302)
   value = ngx.HTTP_SEE_OTHER (303)
   value = ngx.HTTP_NOT_MODIFIED (304)
   value = ngx.HTTP_TEMPORARY_REDIRECT (307) (first added in the v0.9.20 release)
   value = ngx.HTTP_PERMANENT_REDIRECT (308)
   value = ngx.HTTP_BAD_REQUEST (400)
   value = ngx.HTTP_UNAUTHORIZED (401)
   value = ngx.HTTP_PAYMENT_REQUIRED (402) (first added in the v0.9.20 release)
   value = ngx.HTTP_FORBIDDEN (403)
   value = ngx.HTTP_NOT_FOUND (404)
   value = ngx.HTTP_NOT_ALLOWED (405)
   value = ngx.HTTP_NOT_ACCEPTABLE (406) (first added in the v0.9.20 release)
   value = ngx.HTTP_REQUEST_TIMEOUT (408) (first added in the v0.9.20 release)
   value = ngx.HTTP_CONFLICT (409) (first added in the v0.9.20 release)
   value = ngx.HTTP_GONE (410)
   value = ngx.HTTP_UPGRADE_REQUIRED (426) (first added in the v0.9.20 release)
   value = ngx.HTTP_TOO_MANY_REQUESTS (429) (first added in the v0.9.20 release)
   value = ngx.HTTP_CLOSE (444) (first added in the v0.9.20 release)
   value = ngx.HTTP_ILLEGAL (451) (first added in the v0.9.20 release)
   value = ngx.HTTP_INTERNAL_SERVER_ERROR (500)
   value = ngx.HTTP_NOT_IMPLEMENTED (501)
   value = ngx.HTTP_METHOD_NOT_IMPLEMENTED (501) (kept for compatibility)
   value = ngx.HTTP_BAD_GATEWAY (502) (first added in the v0.9.20 release)
   value = ngx.HTTP_SERVICE_UNAVAILABLE (503)
   value = ngx.HTTP_GATEWAY_TIMEOUT (504) (first added in the v0.3.1rc38 release)
   value = ngx.HTTP_VERSION_NOT_SUPPORTED (505) (first added in the v0.9.20 release)
   value = ngx.HTTP_INSUFFICIENT_STORAGE (507) (first added in the v0.9.20 release)

实例:

location /foo {
    content_by_lua_block {
        ngx.say("foo")
        return ngx.HTTP_CONTINUE
    }
}

location /bar {
    content_by_lua_block {
        ngx.say("bar")
    }
}

当请求/foo时,该location处理完成后,返回ngx.HTTP_CONTINUE,Nginx会继续遍历后续的location,
即/bar。在/bar中处理完成后,请求处理完成并返回。因此,对于请求/foo,最终会输出foo和bar。
  • Nginx 日志级别常量

  • 适用于:init_by_lua*, init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, exit_worker_by_lua*, ssl_client_hello_by_lua*

  • 作用:控制 Nginx 记录日志的详细程度,这些常量通常由ngx.log方法使用。

ngx.DEBUG: 调试信息级别,最详细的日志级别。
ngx.INFO: 信息级别,记录正常的操作和状态。
ngx.NOTICE: 注意级别,记录需要注意的情况。
ngx.WARN: 警告级别,记录非致命错误和异常情况。
ngx.ERR: 错误级别,记录致命错误和异常情况。
ngx.CRIT: 严重错误级别,记录严重错误和崩溃情况。
ngx.ALERT: 警戒级别,记录需要立即采取行动的情况。
ngx.EMERG: 紧急级别,最高级别,记录系统不可用的情况。
ngx.STDERR
  • 可以在 Nginx 配置文件中通过 error_log 指令设置日志级别,如:

error_log logs/error.log ngx.ERR;
  • 此配置表示记录所有 ERR 级别的日志信息到 logs/error.log 文件中。在 OpenResty 中,可以使用 Nginx 的日志 API,如 ngx.log() 函数,输出指定级别的日志信息。例如:

ngx.log(ngx.ERR, "an error occurred: ", err)
  • print

  • 作用:打印输出日志信息。

  • 适用于: init_by_lua*, init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, exit_worker_by_lua*, ssl_client_hello_by_lua*

  • ngx.NOTICE 值是会将值写入到 error.log文件中。

  • ngx.cxt

  • 适用于:init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, exit_worker_by_lua*

  • 作用:可用于存储每个请求的Lua上下文数据,并且具有与当前请求相同的生命周期(与Nginx变量一样)。请求之间数据不互通,并且每次请求结束值都会消失

  • 在请求的生命周期中都是存在的:

  • 也就是说,ngx.ctx.foo条目在请求的重写、访问和内容阶段持续存在。


 location /test {
     rewrite_by_lua_block {
         ngx.ctx.foo = 76
     }
     access_by_lua_block {
         ngx.ctx.foo = ngx.ctx.foo + 3
     }
     content_by_lua_block {
         ngx.say(ngx.ctx.foo)
     }
 }
  • ngx.location.capture

  • 语法:res = ngx.location.capture(uri,options?)

  • 适用于:rewrite_by_lua* 、access_by_lua*、content_by_lua*

  • 作用:用于内部Lua的API的请求发送。

  • 注意:使用发出同步请求但是发送Nginx的子请求是非堵塞的。

  • 返回结果

 res = ngx.location.capture(uri)

返回具有 4 个槽的 Lua 表:res.statusres.headerres.bodyres.truncated

  • res.status保存子请求响应的响应状态代码。

  • res.body 是请求的响应体

  • res.truncated 是用来检测响应体是否被修改过。

  • res.header包含子请求的所有响应头,它是一个普通的 Lua 表。对于多值响应标头,该值是一个 Lua(数组)表,它按照它们出现的顺序保存所有值。例如,如果子请求响应标头包含以下行:

 Set-Cookie: a=3
 Set-Cookie: foo=bar
 Set-Cookie: baz=blah
  • 请求参数可选项:

  • method 指定子请求的请求方法,它只接受像 . 这样的常量ngx.HTTP_POST

  • body 指定子请求的请求主体(仅限字符串值)。

  • args 指定子请求的 URI 查询参数(接受字符串值和 Lua 表)

  • ctx 指定一个 Lua 表作为子请求的ngx.ctx表。它可以是当前请求的ngx.ctx表,这有效地使父请求及其子请求共享完全相同的上下文表。此选项在发行版中首次引入v0.3.1rc25

  • vars 采用一个 Lua 表,其中包含用于将子请求中指定的 Nginx 变量设置为该选项值的值。此选项在发行版中首次引入v0.3.1rc31

  • copy_all_vars 指定是否将当前请求的所有 Nginx 变量值复制到相关子请求。子请求中 Nginx 变量的修改不会影响当前(父)请求。此选项在发行版中首次引入v0.3.1rc31

  • share_all_vars 指定是否与当前(父)请求共享子请求的所有 Nginx 变量。子请求中 Nginx 变量的修改将影响当前(父)请求。启用此选项可能会由于不良的副作用而导致难以调试的问题,并且被认为是有害的。仅当您完全知道自己在做什么时才启用此选项。

  • always_forward_bodybody当设置为 true 时,如果未指定 该选项,则当前(父)请求的请求主体将始终转发到正在创建的子请求。ngx.req.read_body()lua_need_request_body读取的请求体将直接转发给子请求,而不会在创建子请求时复制整个请求体数据(无论请求体数据缓存在内存缓冲区还是临时文件中) . 默认情况下,该选项是,falsebody未指定该选项时,只有当子请求采用 orPUT请求POST方法时,才会转发当前(父)请求的请求体。

      location /capture {
           default_type text/html;
           content_by_lua_block {
              ngx.say("start-----");
              local res = ngx.location.capture("/var")
               ngx.print("xxxxxxxxxxxxxx");
                ngx.say(res.status,res.body,res.truncated)
        }
       }
  • ngx.location.capture_multi

  • 语法: res1, res2, ... = ngx.location.capture_multi({ {uri, options?}, {uri, options?}, ... })

  • 适用于: rewrite_by_lua*、access_by_lua*、content_by_lua*

  • 作用:就像ngx.location.capture一样,但支持并行运行的多个子请求。在所有子请求终止之前,此函数不会返回。总延迟是单个子请求的最长延迟而不是总和。

res1, res2, res3 = ngx.location.capture_multi{
  { "/foo", { args = "a=3&b=4" } },
    { "/bar" },
    { "/baz", { method = ngx.HTTP_POST, body = "hello" } },
}

if res1.status == ngx.HTTP_OK then
...
end

if res2.body == "BLAH" then
...
end
  • ngx.re.sub

  • 语法:newstr, n, err = ngx.re.sub(subject, regex, replace, options?)

  • 适用于:init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, exit_worker_by_lua*, ssl_client_hello_by_lua*

  • 作用·:用于通过正则表达式进行字符串替换操作。

newstr, n, err = ngx.re.sub(subject, regex, replace, options?)
其中:

subject:要进行替换的字符串。
regex:用于匹配的正则表达式。
replace:用于替换的字符串,可以包含正则表达式捕获组,例如 "$1-$2"。
options:一个可选的表,用于设置正则表达式的选项,如 i 表示不区分大小写匹配,j 表示开启 PCRE-JIT 编译等。
该函数返回替换后的新字符串 newstr,替换次数 n,以及可能的错误信息 err。

  • ngx.req.get_headers

  • 语法:headers, err = ngx.req.get_headers(max_headers?, raw?)

  • 适用于:set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*

  • 作用:获取所有的请求信息。

-- 获取当前请求的请求头
local headers ,err = ngx.req.get_headers();
-- 表示报错
if  err == 'truncated' then
    ngx.say("请求头获取失败!");
else
    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
        -- 判断v 是否一个table如果是则进行,格式连接
        if type(v) == "table" then
            ngx.say(k.." : ".. table.concat(v,",")..'<br/>');
        else
            ngx.say(k.." : "..v.."<br/>");
        end
    end
end
  • ngx.req.get_uri_agrs

  • 语法: args, err = ngx.req.get_uri_args(max_args?, tab?)

  • 适用于:set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, balancer_by_lua*

  • 作用:返回请求中get的参数。


 location = /test {
     content_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

         for key, val in pairs(args) do
             if type(val) == "table" then
                 ngx.say(key, ": ", table.concat(val, ", "))
             else
                 ngx.say(key, ": ", val)
             end
         end
     }
 }
  • ngx.req.read_body

  • 语法:ngx.req.read_body

  • 适用于:rewrite_by_lua*、access_by_lua*、content_by_lua*

  • 作用:将当前的请求体流读取,注意不会重复读取,并且将读取的数据保存缓存区或者磁盘文件中。


 ngx.req.read_body()
 local args = ngx.req.get_post_args()
  • ngx.req.get_post_args

  • 语法:args, err = ngx.req.get_post_args(max_args?)

  • 适用于: rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*

  • 作用:获取当前post请求的请求体的参数,需要先通过ngx.req.read_body()获取请求体流。


 location = /test {
     content_by_lua_block {
         ngx.req.read_body()
         local args, err = ngx.req.get_post_args()

         if err == "truncated" then
             -- one can choose to ignore or reject the current request here
         end

         if not args then
             ngx.say("failed to get post args: ", err)
             return
         end
         for key, val in pairs(args) do
             if type(val) == "table" then
                 ngx.say(key, ": ", table.concat(val, ", "))
             else
                 ngx.say(key, ": ", val)
             end
         end
     }
 }

其他具体看官网··....

4.5 案例测试

获取nginx的请求的所有args

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

获取nginx请求的信息

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by 千城丶Y.
--- DateTime: 2023/3/11 22:25
---

-- 获取当前请求的请求头
local headers ,err = ngx.req.get_headers();
-- 表示报错
if  err == 'truncated' then
    ngx.say("请求头获取失败!");
else
    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
        -- 判断v 是否一个table如果是则进行,格式连接
        if type(v) == "table" then
            ngx.say(k.." : ".. table.concat(v,",")..'<br/>');
        else
            ngx.say(k.." : "..v.."<br/>");
        end
    end
end

获取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

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/>")

五、nginx的openresty缓存方式

默认基于nginx的话也是有一些缓存的方式比如redis的缓存 或者是静态资源的缓存等等一些系列的方式。在openresty中也是提供了缓存的解决方案。

5.1 共享字典缓存

https://github.com/openresty/lua-nginx-module#ngxshareddict

声明:

语法:lua_shared_dict <name><size>

适用于:http

作用:声明一个共享内存区域,<name>作为基于 shm 的 Lua 字典的存储ngx.shared.<name>

共享内存区域始终由当前 Nginx 服务器实例中的所有 Nginx 工作进程共享

<size>参数接受大小单位,例如km


 http {
     lua_shared_dict dogs 10m;
     ...
 }

使用方式:

语法: dict = ngx.shared.DICT

语法: dict = ngx.shared[name_var]

适用于: init_by_lua*, init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*, exit_worker_by_lua*, ssl_client_hello_by_lua*

并且,该shared提供了对声明的字典的操作命令,具体可以看官网介绍。

共享字典,效率比较,但是因为是多线程共享,一致性的保证就会出现锁的机制,这个是能被所有访问,并且保证原子性的。

这个是通过nginx内存做缓存,可以做上游服务器的缓存数据了。

shared_data的缺陷就是因为要保证原子性,在多线程中就会有锁的产生,会影响qps的效率。

local  data = ngx.shared.shared_data ;

local  i = data:get("i");
if not i  then
    i = 10;
    data:set("i",i);
    ngx.say("i值:"..data:get("i"))
else
    data:incr("i",10);
    ngx.say("i值:"..data:get("i"))
end

5.2 lua-resty-lrucache

Lua 实现的一个简单的 LRU 缓存,适合在 Lua 空间里直接缓存较为复杂的 Lua 数据结构:它相比 ngx_lua 共享内存字典可以省去较昂贵的序列化操作,相比 memcached 这样的外部服务又能省去较昂贵的 socket 操作

https://github.com/openresty/lua-resty-lrucache

这个库为 OpenRestyngx_lua模块实现了一个简单的 LRU 缓存。此缓存还支持过期时间。

该库已经集成openresty里,使用时需要在lua脚本中引入既可

local lrucache = require "resty.lrucache"

他也是可以在nginx中使用缓存,它是完全通过lua实现的, 它是在多个独立进程去运行,多个进程同时运行时可能会有内存的浪费,独立进程执行,单线程中 就不需要在做一些修改操作时再加锁了,性能会高些,它可以做 LRU 算法的清理工作。

它的存储的概念和shared不一样,它是以key value的个数为限制, 而shared则是以内存大小,shared可以很好的控制好内存大小。而这个则无法很好的预估占用内存,这样的好处就是内存的弹性化,不好出现内存写满了内容写不进去。

注意:使用lrucache不能关闭lua_code_cache模块,因为关闭缓存之后每次请求会重新去编译加载,导致缓存失效就会缓存失效。

因为lua语言是动态语言,它基于静态编译和静态脚本之间就是他执行完一次后会有一层缓存。
需要将初始化的对象变成常驻对象。

一下错误是因为lua的requrie没有找到文件,它默认是从以下路径去找文件,没有找到,所需要需要把通过requrie导入的文件到指定路径下或者修改指定目录。/usr/local/openresty2/lualib

默认会在这些目录下去找文件

    location /lru {
            default_type text/html;
            content_by_lua_block {

               local obj = require("lua/LRUCache")
               obj.go();
           }

        }
local _M = {}

-- alternatively: local lrucache = require "resty.lrucache.pureffi"
local lrucache = require "resty.lrucache"

-- we need to initialize the cache on the lua module level so that
-- it can be shared by all the requests served by each nginx worker process:
local c, err = lrucache.new(200)  -- allow up to 200 items in the cache
if not c then
    error("failed to create the cache: " .. (err or "unknown"))
end
function _M.go()
    c:set("dog", 32)
    c:set("cat", 56)
    ngx.say("dog: ", c:get("dog"))
    ngx.say("cat: ", c:get("cat"))

    c:set("dog", { age = 10 }, 0.1)  -- expire in 0.1 sec
    c:delete("dog")

    c:flush_all()  -- flush all the cached data
end

return _M

性能差距不大,区别一个有锁一个没有锁 。

六、lua-resty-redis

这个库可以让nginx直接通过lua操作reids数据库。

官网:https://github.com/openresty/lua-resty-redis

这个 Lua 库是 ngx_lua nginx 模块的 Redis 客户端驱动程序:

https://github.com/openresty/lua-nginx-module/#readme

这个 Lua 库利用了 ngx_lua 的 cosocket API,它确保了 100% 的非阻塞行为。

请注意,至少需要ngx_lua 0.5.14OpenResty 1.2.1.14 。

注意:

一般写的操作都不会由nginx的操作,除非一些简单的,如计数操作,因为如在nginx修改比较复杂的逻辑,那么就无法保证redis的一致性,这一点需要注意,要保证缓存和持久化的数据的一致性。

6.1 常用方法

local res, err = red:get("key")

local res, err = red:lrange("nokey", 0, 1)
# 通过cjson 将结果以json的格式展示
ngx.say("res:",cjson.encode(res))

6.2 创建连接

red, err = redis:new()

ok, err = red:connect(host, port, options_table?)

6.3 连接超时时间

red:set_timeout(time)

6.4 keepalive


red:set_keepalive(max_idle_timeout, pool_size)

6.3 close

ok, err = red:close()

6.6 init_pipeline

启用 redis 流水线模式。commit_pipeline对 Redis 命令方法的所有后续调用将自动缓存,并在调用该方法或通过调用该方法取消时一次性发送到服务器cancel_pipeline

这种方法总是成功的。

如果 redis 对象已经处于 Redis 流水线模式,则调用此方法将丢弃现有缓存的 Redis 查询。

可选n参数指定要添加到此管道的命令的(近似)数量,这可以使事情变得更快一些。

red:init_pipeline()

results, err = red:commit_pipeline()

6.7 认证

    local res, err = red:auth("foobared")

    if not res then

        ngx.say("failed to authenticate: ", err)

        return
end

6.8 案例

 local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeouts(1000, 1000, 1000) -- 1 sec

  local ok, err = red:connect("127.0.0.1", 6379)
 if not ok then
                    ngx.say("failed to connect: ", err)
                    return
                end

                ok, err = red:set("dog", "an animal")
                if not ok then
                    ngx.say("failed to set dog: ", err)
                    return
                end

                ngx.say("set result: ", ok)

                local res, err = red:get("dog")
                if not res then
                    ngx.say("failed to get dog: ", err)
                    return
                end

                if res == ngx.null then
                    ngx.say("dog not found.")
                    return
                end


              ngx.say("dog: ", res)

6.9 redis-cluster

redis的集群部署连接可以使用该模块

https://github.com/steve0511/resty-redis-cluster

七、lua-resty-mysql

https://github.com/openresty/lua-resty-mysql

具体查看官网介绍

注意:
如果要在nginx中使用mysql要注意,最好不要有带参数的查询,因为他没有像java那样有sql预编译后在给mysql执行,可能会出现sql注入。
 local mysql = require "resty.mysql"
                 local db, err = mysql:new()
                 if not db then
                     ngx.say("failed to instantiate mysql: ", err)
                     return
                 end
 
                 db:set_timeout(1000) -- 1 sec
 
 
                 local ok, err, errcode, sqlstate = db:connect{
                     host = "192.168.44.211",
                     port = 3306,
                     database = "zhangmen",
                     user = "root",
                     password = "111111",
                     charset = "utf8",
                     max_packet_size = 1024 * 1024,
                 }
 
 
                 ngx.say("connected to mysql.<br>")
 
 
 
                 local res, err, errcode, sqlstate = db:query("drop table if exists cats")
                 if not res then
                     ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                     return
                 end
 
 
                 res, err, errcode, sqlstate =
                     db:query("create table cats "
                              .. "(id serial primary key, "
                              .. "name varchar(5))")
                 if not res then
                     ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                     return
                 end
 
                 ngx.say("table cats created.")
 
 
 
                 res, err, errcode, sqlstate =
                     db:query("select * from t_emp")
                 if not res then
                     ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                     return
                 end
 
                 local cjson = require "cjson"
                 ngx.say("result: ", cjson.encode(res))
 
 
                 local ok, err = db:set_keepalive(10000, 100)
                 if not ok then
                     ngx.say("failed to set keepalive: ", err)
                     return
                 end

八、lua-restry-template 模板渲染

ua-resty-template是用于 Lua 和 OpenResty 的编译 (1) (HTML) 模板引擎。

它可以分为两块,一是要渲染的数据、二是html静态页面格式

如果学习过JavaEE中的servlet和JSP的话,应该知道JSP模板最终会被翻译成Servlet来执行;

而lua-resty-template模板引擎可以认为是JSP,其最终会被翻译成Lua代码,然后通过ngx.print输出。

lua-resty-template大体内容有:

l 模板位置:从哪里查找模板;

l 变量输出/转义:变量值输出;

l 代码片段:执行代码片段,完成如if/else、for等复杂逻辑,调用对象函数/方法;

l 注释:解释代码片段含义;

l include:包含另一个模板片段;

l 其他:lua-resty-template还提供了不需要解析片段、简单布局、可复用的代码块、宏指令等支持。

基础语法

l {(include_file)}:包含另一个模板文件;

l {* var *}:变量输出;

l {{ var }}:变量转义输出;

l {% code %}:代码片段;

l {# comment #}:注释;

l {-raw-}:中间的内容不会解析,作为纯文本输出;

这个模块默认是没有的,需要单独引入。

8.1 引入模板

  • 1.下载

  • 2.解压

  • tar -zxvf lua-resty-template-1.9.tar.gz

  • 3.模块导入

  • 将解压好的文件进行放到 xx/openresty/lualib下

  • cp -r /tools/lua-resty-template-1.9 /usr/local/openresty2/lualib/

  • 将lua-resty-template-1.9/lib/resty目录下的 template template.lua移动到xxx//lualib/resty/下

8.2 模板存放位置

nginx.conf中配置

set $template_root /usr/local/openresty2/nginx/tpl;

8.3 测试

这个模板是两个组合一个是展示静态的一个逻辑处理动态。

      location /tpl {
         # 设置template的文件路径
         set $template_root /usr/local/openresty2/nginx/tpl;
          content_by_lua_file lua/view.lua;
        }

逻辑

local template = require "resty.template"
local view = template.new "view.html"
view.message = "Hello, World!"
view:render()

内容展示

<!DOCTYPE html>
<html>
<body>
  <h1>{{message}}</h1>
</body>
</html>

8.4 执行函数,得到渲染之后的内容

local func = template.compile("view.html")  

local content = func(context)  

ngx.say("xx:",content) 

8.5 resty.template.html

local template = require("resty.template")
local html = require "resty.template.html"

template.render([[
<ul>
{% for _, person in ipairs(context) do %}
    {*html.li(person.name)*} --
{% end %}
</ul>
<table>
{% for _, person in ipairs(context) do %}
    <tr data-sort="{{(person.name or ""):lower()}}">
        {*html.td{ id = person.id }(person.name)*}
    </tr>
{% end %}
</table>]], {
    { id = 1, name = "Emma"},
    { id = 2, name = "James" },
    { id = 3, name = "Nicholas" },
    { id = 4 }
})
<!DOCTYPE html>
<html>
<body>
  <h1>{{message}}</h1>
</body>
</html>

8.6 多值传入

template.caching(false)
local template = require("resty.template")
local context = {
    name = "lucy",
    age = 50,
}
template.render("view.html", context)
<!DOCTYPE html>
<html>
<body>
  <h1>name:{{name}}</h1>
  <h1>age:{{age}}</h1>
</body>
</html>

8.7 模板管理与缓存

模板缓存:默认开启,开发环境可以手动关闭

template.caching(true)

模板文件需要业务系统更新与维护,当模板文件更新后,可以通过模板版本号或消息通知Openresty清空缓存重载模板到内存中

template.cache = {}

8.8 完整页面

逻辑


local template = require("resty.template")
template.caching(false)
local context = {
    title = "测试",
    name = "lucy",
    description = "<script>alert(1);</script>",
    age = 40,
    hobby = {"电影", "音乐", "阅读"},
    score = {语文 = 90, 数学 = 80, 英语 = 70},
    score2 = {
        {name = "语文", score = 90},
        {name = "数学", score = 80},
        {name = "英语", score = 70},
    }
}

template.render("view.html", context)

模板·

{(header.html)}  
   <body>  
      {# 不转义变量输出 #}  
      姓名:{* string.upper(name) *}<br/>  
      {# 转义变量输出 #}  
      简介:{{description}}
           简介:{* description *}<br/>  
      {# 可以做一些运算 #}  
      年龄: {* age + 10 *}<br/>  
      {# 循环输出 #}  
      爱好:  
      {% for i, v in ipairs(hobby) do %}  
         {% if v == '电影' then  %} - xxoo
            
              {%else%}  - {* v *} 
{% end %}  
         
      {% end %}<br/>  
  
      成绩:  
      {% local i = 1; %}  
      {% for k, v in pairs(score) do %}  
         {% if i > 1 then %},{% end %}  
         {* k *} = {* v *}  
         {% i = i + 1 %}  
      {% end %}<br/>  
      成绩2:  
      {% for i = 1, #score2 do local t = score2[i] %}  
         {% if i > 1 then %},{% end %}  
          {* t.name *} = {* t.score *}  
      {% end %}<br/>  
      {# 中间内容不解析 #}  
      {-raw-}{(file)}{-raw-}  
{(footer.html)} 

8.9 layout 布局统一风格

也就是支持引入其他文件,比如统一的头和尾。

使用模板内容嵌套可以实现全站风格同一布局

lua

local template = require "resty.template"

一、

local layout   = template.new "layout.html"

layout.title   = "Testing lua-resty-template"

layout.view    = template.compile "view.html" { message = "Hello, World!" }

layout:render()

8.10 Redis缓存+mysql+模板输出

 cjson = require "cjson"
sql="select * from t_emp"


local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeouts(1000, 1000, 1000) -- 1 sec

  local ok, err = red:connect("127.0.0.1", 6379)
 if not ok then
                    ngx.say("failed to connect: ", err)
                    return
                end


        
                local res, err = red:get(sql)
                if not res then
                    ngx.say("failed to get sql: ", err)
                    return
                end

                if res == ngx.null then
                    ngx.say("sql"..sql.." not found.")




--mysql查询
local mysql = require "resty.mysql"
                local db, err = mysql:new()
                if not db then
                    ngx.say("failed to instantiate mysql: ", err)
                    return
                end

                db:set_timeout(1000) -- 1 sec


                local ok, err, errcode, sqlstate = db:connect{
                    host = "192.168.44.211",
                    port = 3306,
                    database = "zhangmen",
                    user = "root",
                    password = "111111",
                    charset = "utf8",
                    max_packet_size = 1024 * 1024,
                }


                ngx.say("connected to mysql.<br>")


 res, err, errcode, sqlstate =
                    db:query(sql)
                if not res then
                    ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                    return
                end


          --ngx.say("result: ", cjson.encode(res))



      ok, err = red:set(sql, cjson.encode(res))
                if not ok then
                    ngx.say("failed to set sql: ", err)
                    return
                end

                ngx.say("set result: ", ok)

                    return
                end








local template = require("resty.template")
template.caching(false)
local context = {
    title = "测试",
    name = "lucy",
    description = "<script>alert(1);</script>",
    age = 40,
    hobby = {"电影", "音乐", "阅读"},
    score = {语文 = 90, 数学 = 80, 英语 = 70},
    score2 = {
        {name = "语文", score = 90},
        {name = "数学", score = 80},
        {name = "英语", score = 70},
    },

zhangmen=cjson.decode(res)

}





template.render("view.html", context)
{(header.html)}  
   <body>  
      {# 不转义变量输出 #}  
      姓名:{* string.upper(name) *}<br/>  
      {# 转义变量输出 #}  

      年龄: {* age + 10 *}<br/>  
      {# 循环输出 #}  
      爱好:  
      {% for i, v in ipairs(hobby) do %}  
         {% if v == '电影' then  %} - xxoo
            
              {%else%}  - {* v *} 
{% end %}  
         
      {% end %}<br/>  
  
      成绩:  
      {% local i = 1; %}  
      {% for k, v in pairs(score) do %}  
         {% if i > 1 then %},{% end %}  
         {* k *} = {* v *}  
         {% i = i + 1 %}  
      {% end %}<br/>  
      成绩2:  
      {% for i = 1, #score2 do local t = score2[i] %}  
         {% if i > 1 then %},{% end %}  
          {* t.name *} = {* t.score *}  
      {% end %}<br/>  
      {# 中间内容不解析 #}  
      {-raw-}{(file)}{-raw-}  




掌门:
{* zhangmen *}



   {% for i = 1, #zhangmen do local z = zhangmen[i] %}  
         {* z.deptId *},{* z.age *},{* z.name *},{* z.empno *},<br>
      {% end %}<br/>  

{(footer.html)}  

这个文件内容做三件事

第一首先redis查询是否有该数据,如果有则返回,如果没有则去数据库中查,查询到在放入缓存,最后将结果渲染

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值