nginx lua编程要点,看完肯定会提高你的编码能力

lua new

 

table.new(narray, nhash) 两个参数分别代表table里是array还是hash的 

table.new(10, 0) 或者 table.new(0, 10) 这样的,后者是 hash 性质的 table

lua table可以同时拥有数组部分和哈希部分。在物理上,数组部分和哈希部分也是在 table 内部分开存储的。

比如 

table.new(100, 200) 也是完全合法的。

table.new(narr, nrec) 接口是标准 Lua C API 函数 lua_createtable() 的 Lua 版本:

http://www.lua.org/manual/5.1/ manual.html#lua_createtable

另外,不同于使用了 lua_createtable() 的 lua_CFunction,table.new() 是可以被 JIT 编译的。

table.new()一般用于给 lua table 预分配空间。

否则 table 会在插入新元素时自增长,而自增长是高代价操作( 因为需要重新分配空间、重新 hash,以及拷贝数据)

ngx lua正则

ngx_lua模块中正则表达式相关的api

在Lua模块中,通过正则表达式处理字符串的相关API,主要有:

ngx.re.match

语法:captures, err = ngx.re.match(subject, regex, options?, ctx?, res_table?)

只有第一次匹配的结果被返回,如果没有匹配,则返回nil;或者匹配过程中出现错误时,也会返回nil,此时错误信息会被保存在err中。

当匹配的字符串找到时,一个lua table captures会被返回,captures[0]中保存的就是匹配到的字串,captures[1]保存的是用括号括起来的第一个子模式的结果,captures[2]保存的是第二个子模式的结果,依次类似。

local m, err = ngx.re.match("hello, 1234", "[0-9]+")

if m then

-- m[0] == "1234"

else

if err then

ngx.log(ngx.ERR, "error: ", err)

return

end

ngx.say("match not found")

end

上面例子中,匹配的字符串是1234,因此m[0] == "1234",但是没有用括号括起来的子模式,因此,m[1],m[2]等均为nil。

local m, err = ngx.re.match("hello, 1234", "([0-9])[0-9]+")

-- m[0] == "1234"

-- m[1] == "1"

命名方式的捕获,从v0.7.14版本后开始支持,如下所示:

local m, err = ngx.re.match("hello, 1234", "([0-9])(?<remaining>[0-9]+)")

-- m[0] == "1234"

-- m[1] == "1"

-- m[2] == "234"

-- m["remaining"] == "234"

local m, err = ngx.re.match("hello, world", "(world)|(hello)|(?<named>howdy)")

-- m[0] == "hello"

-- m[1] == nil

-- m[2] == "hello"

-- m[3] == nil

-- m["named"] == nil

为什么m[1]等于nil?因为在给定的模式串中,最先匹配的是hello,因此,其他子模式在找到的匹配串中查找不到对应的匹配串,因此,除了hello子模式外,其他的子模式的匹配结果都是nil。一定要记住,是谁最先被匹配的。

options选项可以是下面的取值的组合:

a 锚定模式,只从头开始匹配。

d DFA模式,或者称最长字符串匹配语义,需要PCRE 6.0+支持。

D 允许重复的命名的子模式,该选项需要PCRE 8.12+支持,例如

local m = ngx.re.match("hello, world","(?<named>\w+), (?<named>\w+)","D")

-- m["named"] == {"hello", "world"}

i 大小写不敏感模式。

j 启用PCRE JIT编译, 需要PCRE 8.21+ 支持,并且必须在编译时加上选项--enable-jit,为了达到最佳性能,该选项总是应该和'o'选项搭配使用。

J 启用PCRE Javascript的兼容模式,需要PCRE 8.12+ 支持。

m 多行模式。

o 一次编译模式,启用worker-process级别的编译正则表达式的缓存。

s 单行模式。

u UTF-8模式. 该选项需要在编译PCRE库时加上--enable-utf8 选项。

U 与"u" 选项类似,但是该项选禁止PCRE对subject字符串UTF-8有效性的检查。

x 扩展模式。

两个例子:

local m, err = ngx.re.match("hello, world", "HEL LO", "ix")

-- m[0] == "hello"



local m, err = ngx.re.match("hello, 美好生活", "HELLO, (.{2})", "iu")

-- m[0] == "hello, 美好"

-- m[1] == "美好"

第四个可选参数ctx可以传入一个lua table,传入的lua table可以是一个空表,也可以是包含pos字段的lua table。如果传入的是一个空的lua table,那么,ngx.re.match将会从subject字符串的起始位置开始匹配查找,查找到匹配串后,修改pos的值为匹配字符串的下一个位置的值,并将pos的值保存到ctx中,如果匹配失败,那么pos的值保持不变;如果传入的是一个非空的lua table,即指定了pos的初值,那么ngx.re.match将会从指定的pos的位置开始进行匹配,如果匹配成功了,修改pos的值为匹配字符串的下一个位置的值,并将pos的值保存到ctx中,如果匹配失败,那么pos的值保持不变。

local ctx = {}

local m, err = ngx.re.match("1234, hello", "[0-9]+", "", ctx)

-- m[0] = "1234"

-- ctx.pos == 5

local ctx = { pos = 2 }

local m, err = ngx.re.match("1234, hello", "[0-9]+", "", ctx)

-- m[0] = "34"

-- ctx.pos == 5 

如果需要传入ctx参数,但并不需要第三个可选参数options时,第三个参数也不能简单去掉,这时需要传入一个空的字符串作为第三个参数的值。 

第四个可选参数还不是很熟悉,暂且留空。

ngx.re.find

语法:from, to, err = ngx.re.find(subject, regex, options?, ctx?, nth?)

该方法与ngx.re.match方法基本类似,不同的地方在于ngx.re.find返回的是匹配的字串的起始位置索引和结束位置索引,如果没有匹配成功,那么将会返回两个nil,如果匹配出错,还会返回错误信息到err中。例子:

local s = "hello, 1234"

local from, to, err = ngx.re.find(s, "([0-9]+)", "jo")

if from then

ngx.say("from: ", from)

ngx.say("to: ", to)

ngx.say("matched: ", string.sub(s, from, to))

else

if err then

ngx.say("error: ", err)

return

end

ngx.say("not matched!")

end



输出结果:

from: 8

to: 11

matched: 1234

该方法相比ngx.re.match,不会创建新的lua字符串,也不会创建新的lua Table,因此,该方法比ngx.re.match更加高效,因此,在可以使用ngx.re.find的地方应该尽量使用。 

第五个参数可以指定返回第几个子模式串的起始位置和结束位置的索引值,默认值是0,此时将会返回匹配的整个字串;如果nth等于1,那么将返回第一个子模式串的始末位置的索引值;如果nth等于2,那么将返回第二个子模式串的始末位置的索引值,依次类推。如果nth指定的子模式没有匹配成功,那么将会返回两个nil。

local str = "hello, 1234"

local from, to = ngx.re.find(str, "([0-9])([0-9]+)", "jo", nil, 2)

if from then

ngx.say("matched 2nd submatch: ", string.sub(str, from, to)) -- yields "234"

end

ngx.re.gmatch

语法:iterator, err = ngx.re.gmatch(subject, regex, options?)

与ngx.re.match相似,区别在于该方法返回的是一个Lua的迭代器,这样就可以通过迭代器遍历所有匹配的结果。

如果匹配失败,将会返回nil,如果匹配出现错误,那么还会返回错误信息到err中。

local iterator, err = ngx.re.gmatch("hello, world!", "([a-z]+)", "i")

if not iterator then

ngx.log(ngx.ERR, "error: ", err)

return

end



local m

m, err = iterator() -- m[0] == m[1] == "hello"

if err then

ngx.log(ngx.ERR, "error: ", err)

return

end



m, err = iterator() -- m[0] == m[1] == "world"

if err then

ngx.log(ngx.ERR, "error: ", err)

return

end



m, err = iterator() -- m == nil

if err then

ngx.log(ngx.ERR, "error: ", err)

return

end

更多情时候,只需要把迭代过程放入一个while循环中即可:

local it, err = ngx.re.gmatch("hello, world!", "([a-z]+)", "i")

if not it then

ngx.log(ngx.ERR, "error: ", err)

return

end



while true do

local m, err = it()

if err then

ngx.log(ngx.ERR, "error: ", err)

return

end



if not m then

-- no match found (any more)

break

end

-- found a match

ngx.say(m[0])

ngx.say(m[1])

end

options选项的使用与ngx.re.match中的options选项的用法是一样的。

ngx.re.gmatch返回的迭代器只能在一个请求所在的环境中使用,就是说,我们不能把返回的迭代器赋值给持久存在的命名空间(比如一个Lua Packet)中的某一个变量。

ngx.re.sub

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

该方法主要实现匹配字符串的替换,会用replace替换匹配的字串,replace可以是纯字符串,也可以是使用$0, $1等子模式串的形式,ngx.re.sub返回进行替换后的完整的字符串,同时返回替换的总个数;options选项,与ngx.re.match中的options选项是一样的。

local newstr, n, err = ngx.re.sub("hello, 1234", "([0-9])[0-9]", "[$0][$1]")

if newstr then

-- newstr == "hello, [12][1]34"

-- n == 1

else

ngx.log(ngx.ERR, "error: ", err)

return

end

在上面例子中,$0表示整个匹配的子串,$1表示第一个子模式匹配的字串,以此类推。 

可以用大括号{}将相应的0,1,2...括起来,以区分一般的数字:

local newstr, n, err = ngx.re.sub("hello, 1234", "[0-9]", "${0}00")

-- newstr == "hello, 100234"

-- n == 1

如果想在replace字符串中显示$符号,可以用$进行转义(不要用反斜杠\$对美元符号进行转义,这种方法不会得到期望的结果):

local newstr, n, err = ngx.re.sub("hello, 1234", "[0-9]", "$$")

-- newstr == "hello, $234"

-- n == 1

如果replace是一个函数,那么函数的参数是一个"match table", 而这个"match table"与ngx.re.match中的返回值captures是一样的,replace这个函数根据"match table"产生用于替换的字符串。

local func = function (m)

return "[" .. m[0] .. "][" .. m[1] .. "]"

end

local newstr, n, err = ngx.re.sub("hello, 1234", "( [0-9] ) [0-9]", func, "x")

-- newstr == "hello, [12][1]34"

-- n == 1

注意:通过函数形式返回的替换字符串中的美元符号$不再是特殊字符,而只是被看作一个普通字符。 

ngx.re.gsub

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

该方法与ngx.re.sub是类似的,但是该方法进行的是全局替换。

两个例子:

local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")

if newstr then

-- newstr == "[hello,h], [world,w]"

-- n == 2

else

ngx.log(ngx.ERR, "error: ", err)

return

end
local func = function (m)

return "[" .. m[0] .. "," .. m[1] .. "]"

end

local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", func, "i")

-- newstr == "[hello,h], [world,w]"

-- n == 2

ngx_lua如果是0.9.2以上版本,建议正则过滤函数改为ngx.re.find,匹配效率会提高三倍左右。

项目实战

项目中body_filter阶段代码实例如下:

local resp_body = ngx.arg[1] --获取响应体 
local eof = ngx.arg[2] 
local ctx_log = {} --日志table 
local regex = [[You have an error in your SQL syntax]] --匹配的敏感内容 
local m = ngx.re.match(resp_body, regex, 'jio') --对响应体做正则匹配 
if m then --如果匹配到敏感信息 
    ctx_log.rule_match = m[0] --将匹配内容写入日志
    ctx_log.Request_line = ngx.var.request --记录请求URL,包括GET参数 
    ctx_log.Request_headers = ngx.req.get_headers() --记录请求头部 
    ngx.ctx.log = ctx_log --日志赋值给跨阶段的ngx.ctx.log
end

项目中log阶段代码实例如下:

local logger = require "socket" --加载logger socket库
local cjson = require "cjson.safe" --加载cjson库

if not logger.initted() then --初始化
    logger local ok,err = logger.init{ host = "127.0.0.1", --splunk IP port = 8888, --splunk端口 sock_type = "tcp", --日志socket类型 flush_limit = 1, }
    if not ok then --初始化失败处理 
        ngx.log(ngx.ERR,"failed to initialize the logger: ",err) 
        return
    end 
end

local log = ngx.ctx.log --接收ngx.ctx.log跨阶段传过来的日志信息
if type(log) == "table" then --判断日志不为空则记录
    local bytes, err = logger.log(cjson.encode(log) .. "\r\n") 
    if err then
        ngx.log(ngx.ERR, "failed to log message: ", err) 
    end
end

1607595062_5fd1f4364765f6bceaa06.png!small

Splunk需要注意的地方:需要编辑props.conf以免在日志过多的时候Splunk自动把多行Json格式日志合并vim /opt/splunk/etc/system/local/props.conf。加入以下内容,[_json] #这个是sourcetype SHOULD_LINEMERGE = false #告诉Splunk不自动合并行。

ngx lua变量

Lua | NGINX

lua编程

使用string.match 和正则实现对字符串的去除两端空格;  

Str = string.match(Str,"%s*(.-)%s*$");

使用string.match 和正则实现对字母、数字、汉字外的字符进行判断;

Str2 = string.match(Str2,"^[A-Z-a-z-0-9-\128-\254]+$");

tab = {"a", "c", "d", "b"}

print(table.concat(tab))   

--> 输出结果: acdb

print(table.concat(tab, " @@ ", 2))  

--> 输出结果: c @@ d @@ b

print( table.concat(tab, " - ", 2, 3))  

--> 输出结果: c - d

print(table.concat(tab, " ## \n", 2, 4))   

--> 输出结果:

--> c ##

--> d ##

--> b
fruits = {"banana","orange","apple"}

-- 在末尾插入

table.insert(fruits,"mango")

print("索引为 4 的元素为 ",fruits[4])

-- 在索引为 2 的键处插入

table.insert(fruits,2,"grapes")

print("索引为 2 的元素为 ",fruits[2])

print("最后一个元素为 ",fruits[5])

table.remove(fruits)

print("移除后最后一个元素为 ",fruits[5])

__index 元方法

通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键。

$ lua

Lua 5.3.0  Copyright (C) 1994-2015 Lua.org, PUC-Rio

> other = { foo = 3 }

> t = setmetatable({}, { __index = other })

> t.foo

3

> t.bar

nil

如果__index包含一个函数的话,lua就会调用那个函数,table和键会作为参数传递给函数。

__index 元方法也就是function查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 __index 返回结果。

mytable = setmetatable({key1 = "value1"}, {

__index = function(mytable, key)

if key == "key2" then

return "metatablevalue"

else

return nil

end

end

})



print(mytable.key1,mytable.key2)

实例输出结果为:

value1    metatablevalue
  • mytable 表赋值为 {key1 = "value1"}。mytable 设置了元表,元方法为 __index。
  • 在mytable表中查找 key1,如果找到,返回该元素,找不到则继续。
  • 在mytable表中查找 key2,如果找到,返回 metatablevalue,找不到则继续。
  • 判断元表有没有__index方法,如果__index方法是一个函数,则调用该函数。
  • 元方法function中查看是否传入 "key2" 键的参数(mytable.key2已设置),如果传入 "key2" 参数返回 "metatablevalue",否则返回 mytable 对应的键值。

上代码简单写成:

mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })

print(mytable.key1,mytable.key2)

__newindex 元方法

__newindex 元方法用来对表更新,__index则用来对表访问 。

给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。

mymetatable = {}

mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1)



mytable.newkey = "新值2"

print(mytable.newkey,mymetatable.newkey)



mytable.key1 = "新值1"

print(mytable.key1,mymetatable.key1)



以上实例执行输出结果为:

value1 nil    新值2 新值1    nil

在对新索引键(newkey)赋值时(mytable.newkey = "新值2"),会调用元方法,而不进行赋值。而如果对已存在的索引键(key1),则会进行赋值,而不调用元方法 __newindex。

以下实例使用了 rawset 函数来更新表:

实例

mytable = setmetatable({key1 = "value1"}, {

__newindex = function(mytable, key, value)

rawset(mytable, key, "\""..value.."\"")

end

})



mytable.key1 = "new value"

mytable.key2 = 4



print(mytable.key1,mytable.key2)

以上实例执行输出结果为:

new value    "4"

为表添加操作符

以下实例演示了两表相加操作:

实例

-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用

-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数

function table_maxn(t)

local mn = 0

for k, v in pairs(t) do

if mn < k then

mn = k

end

end

return mn

end

-- 两表相加操作

mytable = setmetatable({ 1, 2, 3 }, {

__add = function(mytable, newtable)

for i = 1, table_maxn(newtable) do

table.insert(mytable, table_maxn(mytable)+1,newtable[i])

end

return mytable

end

})



secondtable = {4,5,6}



mytable = mytable + secondtable

for k,v in ipairs(mytable) do

print(k,v)

end

以上实例执行输出结果为:

1    1 2    2 3    3 4    4 5    5 6    6

模式

描述

__add

对应的运算符 '+'.

__sub

对应的运算符 '-'.

__mul

对应的运算符 '*'.

__div

对应的运算符 '/'.

__mod

对应的运算符 '%'.

__unm

对应的运算符 '-'.

__concat

对应的运算符 '..'.

__eq

对应的运算符 '=='.

__lt

对应的运算符 '<'.

__le

对应的运算符 '<='.

__call 元方法

__call 元方法在 Lua 调用一个值时调用。以下实例演示了计算表中元素的和:

-- 定义元方法__call

mytable = setmetatable({10}, {

__call = function(mytable, newtable)

sum = 0

for i = 1, table_maxn(mytable) do

sum = sum + mytable[i]

end

for i = 1, table_maxn(newtable) do

sum = sum + newtable[i]

end

return sum

end

})

newtable = {10,20,30}

print(mytable(newtable))

以上实例执行输出结果为:

70

__tostring 元方法

__tostring 元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容:

实例

mytable = setmetatable({ 10, 20, 30 }, {

__tostring = function(mytable)

sum = 0

for k, v in pairs(mytable) do

sum = sum + v

end

return "表所有元素的和为 " .. sum

end

})

print(mytable)

以上实例执行输出结果为:

表所有元素的和为 60

Lua的继承(利用setmetatable)

local Animal = {age = 3,gender = "male"} --定义一个Animal的表

function Animal:extend() --定义表中的一个extend方法(继承机制的核心就是下面四行代码) 
    local obj = obj or {} --if obj not nil return obj,else return {}
    setmetatable(obj, self) --set self as obj's metatable 把Animal表自己作为元表放入obj表中 
    self.__index = self --索引__index是一个特殊的机制,只有设置了索引值,才能在子类调用父类方法时,能找到self(也就是Animal表)中的方法 
    return obj --返回带有元表的obj 
end

function Animal:run() --定义父类的一个方法,作为动物,都可以跑的公共函数 
    CCLuaLog("run is my gift~!") 
end

return Animal

local Horse = require("GameObject.Animal"):extend() --获取带有元表的obj 

function Horse:eat() --子类的方法 
    CCLuaLog("eat grass..."..self.age) 
end 

return Horse

local pony = require("GameObject.Horse") 

pony:run() 
pony:eat()

ngx lua操作共享内存

> ngx.shared.DICT.get

语法:value, flags = ngx.shared.DICT:get(key)

获取共享内存上key对应的值。如果key不存在,或者key已经过期,将会返回nil;如果出现错误,那么将会返回nil以及错误信息。

local cats = ngx.shared.cats

local value, flags = cats.get(cats, "Marry")

等价于

local cats = ngx.shared.cats

local value, flags = cats:get("Marry")

返回列表中的flags,是在ngx.shared.DICT.set方法中设置的值,默认值为0. 如果设置的flags为0,那么在这里flags的值将不会被返回。

> ngx.shared.DICT.get_stale

语法:value, flags, stale = ngx.shared.DICT:get_stale(key)

与get方法类似,区别在于该方法对于过期的key也会返回,第三个返回参数表明返回的key的值是否已经过期,true表示过期,false表示没有过期。

> ngx.shared.DICT.set

语法:success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)

“无条件”地往共享内存上插入key-value对,这里讲的“无条件”指的是不管待插入的共享内存上是否已经存在相同的key。三个返回值的含义:

success:成功插入为true,插入失败为false

err:操作失败时的错误信息,可能类似"no memory"

forcible:true表明需要通过强制删除(LRU算法)共享内存上其他字典项来实现插入,false表明没有删除共享内存上的字典项来实现插入。

第三个参数exptime表明key的有效期时间,单位是秒(s),默认值为0,表明永远不会过期;flags参数是一个用户标志值,会在调用get方法时同时获取得到。

local cats = ngx.shared.cats

local succ, err, forcible = cats.set(cats, "Marry", "it is a nice cat!")

等价于

local cats = ngx.shared.cats

local succ, err, forcible = cats:set("Marry", "it is a nice cat!")

> ngx.shared.DICT.safe_set

语法:ok, err = ngx.shared.DICT:safe_set(key, value, exptime?, flags?)

与set方法类似,区别在于不会在共享内存用完的情况下,通过强制删除(LRU算法)的方法实现插入。如果内存不足,会直接返回nil和err信息"no memory"

注意:

set和safe_set共同点是:如果待插入的key已经存在,那么key对应的原来的值会被新的value覆盖!

> ngx.shared.DICT.add

语法:success, err, forcible = ngx.shared.DICT:add(key, value, exptime?, flags?)

与set方法类似,与set方法区别在于不会插入重复的键(可以简单认为add方法是set方法的一个子方法),如果待插入的key已经存在,将会返回nil和和err="exists"

> ngx.shared.DICT.safe_add

语法:ok, err = ngx.shared.DICT:safe_add(key, value, exptime?, flags?)

与safe_set方法类似,区别在于不会插入重复的键(可以简单认为safe_add方法是safe_set方法的一个子方法),如果待插入的key已经存在,将会返回nil和和err="exists"

> ngx.shared.DICT.replace

语法:success, err, forcible = ngx.shared.DICT:replace(key, value, exptime?, flags?)

与set方法类似,区别在于只对已经存在的key进行操作(可以简单认为replace方法是set方法的一个子方法),如果待插入的key在字典上不存在,将会返回nil和错误信息"not found"

> ngx.shared.DICT.delete

语法:ngx.shared.DICT:delete(key)

无条件删除指定的key-value对,其等价于

ngx.shared.DICT:set(key, nil)

> ngx.shared.DICT.incr

语法:newval, err = ngx.shared.DICT:incr(key, value)

对key对应的值进行增量操作,增量值是value,其中value的值可以是一个正数,0,也可以是一个负数。value必须是一个Lua类型中的number类型,否则将会返回nil和"not a number";key必须是一个已经存在于共享内存中的key,否则将会返回nil和"not found".

> ngx.shared.DICT.flush_all

语法:ngx.shared.DICT:flush_all()

清除字典上的所有字段,但不会真正释放掉字段所占用的内存,而仅仅是将每个字段标志为过期。

> ngx.shared.DICT.flush_expired

语法:flushed = ngx.shared.DICT:flush_expired(max_count?)

清除字典上过期的字段,max_count表明上限值,如果为0或者没有给出,表明需要清除所有过期的字段,返回值flushed是实际删除掉的过期字段的数目。

注意:

与flush_all方法的区别在于,该方法将会释放掉过期字段所占用的内存。

> ngx.shared.DICT.get_keys

语法:keys = ngx.shared.DICT:get_keys(max_count?)

从字典上获取字段列表,个数为max_count,如果为0或没有给出,表明不限定个数。默认值是1024个

强烈建议在调用该方法时,指定一个max_count参数,因为在keys数量很大的情况下,如果不指定max_count的值,可能会导致字典被锁定,从而阻塞试图访问字典的worker进程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值