w3cschool-Lua编程入门

https://www.w3cschool.cn/nhycto/

https://www.w3cschool.cn/cf_web/cf_web-dvxc32qu.html

1. Lua 基础知识

(1) 变量

赋值

赋值是改变一个变量的值和改变表域的最基本的方法。Lua 中的变量没有类型,只管赋值即可。比如在 Lua 命令行下输入:

end_of_world = "death"
print(end_of_world)
end_of_world = 2012
print(end_of_world)

上面这四行代码 Lua 不会报错,而会输出:

death
2012

局部变量

使用 local 创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效

x = 10
local i = 1              -- 局部变量

while i<=x do
    local x = i*2        -- while 中的局部变量
    print(x)             --> 2, 4, 6, 8, ...
    i = i + 1
end

应该尽可能的使用局部变量,有两个好处:

  1. 避免命名冲突
  2. 访问局部变量的速度比全局变量更快

代码块(block)

代码块指一个控制结构内,一个函数体,或者一个chunk(变量被声明的那个文件或者文本串)。

我们给block划定一个明确的界限:do..end内的部分。当你想更好的控制局部变量的作用范围的时候这是很有用的。

do
    local a2 = 2*a
    local d = sqrt(b^2 - 4*a*c)
    x1 = (-b + d)/a2
    x2 = (-b - d)/a2
end            -- scope of 'a2' and 'd' ends here
print(x1, x2)

(2) 类型

虽说变量没有类型,但并不是说数据不分类型。Lua 基本数据类型共有八个:nil、boolean、number、string、function、userdata、thread、table。

  • Nil Lua中特殊的类型,他只有一个值:nil;一个全局变量没有被赋值以前默认值为nil;给全局变量负nil可以删除该变量。
  • Booleans 两个取值false和true。但要注意Lua中所有的值都可以作为条件。在控制结构的条件中除了false和nil为假,其他值都为真。所以Lua认为0和空串都是真。
  • Numbers 即实数,Lua 中的所有数都用双精度浮点数表示。
  • Strings 字符串类型,指字符的序列,Lua中字符串是不可以修改的,你可以创建一个新的变量存放你要的字符串。
  • Table 是很强大的数据结构,也是 Lua 中唯一的数据结构。可以看作是数组或者字典。
  • Function 函数是第一类值(和其他变量相同),意味着函数可以存储在变量中,可以作为函数的参数,也可以作为函数的返回值。
  • Userdata userdata可以将C数据存放在Lua变量中,userdata在Lua中除了赋值和相等比较外没有预定义的操作。userdata用来描述应用程序或者使用C实现的库创建的新类型。例如:用标准I/O库来描述文件。
  • Thread 线程会在其它章节来介绍。

可以用 type 函数取得表达式的数据类型:

print(type(undefined_var))
print(type(true))
print(type(3.14))
print(type('Hello World'))
print(type(type))
print(type({}))

(3) 表达式

操作符

  1. 算术运算符:+ - * / ^ (加减乘除幂)
  2. 关系运算符: = == ~=
  3. 逻辑运算符:and or not
  4. 连接运算符:..

有几个操作符跟C语言不一样的:

  • a ~= b 即 a 不等于 b
  • a ^ b 即 a 的 b 次方
  • a .. b 将 a 和 b 作为字符串连接

优先级:

  1. ^
  2. not -(负号)
    • /
    • -
  3. ..
  4. = ~= ==
  5. and
  6. or

表的构造:

最简单的构造函数是{},用来创建一个空表。可以直接初始化数组:

days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}

Lua将"Sunday"初始化days[1](第一个元素索引为1),不推荐数组下标以0开始,否则很多标准库不能使用。

在同一个构造函数中可以数组风格和字典风格进行初始化:

polyline = {color="blue", thickness=2, npoints=4,
              {x=0,   y=0},
              {x=-10, y=0},
              {x=-10, y=1},
              {x=0,   y=1}
}

多重赋值和多返回值

另外 Lua 还支持多重赋值(还支持函数返回多个值)。也就是说:等号右边的值依次赋值给等号左边的变量。比如:

year, month, day = 2011, 3, 12
print(year, month, day)
reutrn year, month, day -- 多返回值
a, b = f()

于是,交换两个变量值的操作也变得非常简单:

a, b = b, a

(4) 控制流

if

name = "peach"
if name == "apple" then
    -- body
elseif name == "banana" then
    -- body
else
    -- body
end

for

-- 初始值, 终止值, 步长
for i=1, 10, 2 do
    print i
end

-- 数组
for k, v in ipairs(table) do
    print(k, v)
end

-- 字典
for k, v in pairs(table) do
    print(k, v)
end

反向表构造实例:

revDays = {} 
for i,v in ipairs(days) do
    revDays[v] = i 
end 

while

while i<10 do
    print i
    i = i + 1
end

repeat-until

repeat
    print i
    i = i + 1
until i < 10

break 和 return

break 语句可用来退出当前循环(for, repeat, while),循环外部不可以使用。

return 用来从函数返回结果,当一个函数自然结束,结尾会有一个默认的return。

Lua语法要求break和return只能出现在block的结尾一句(也就是说:作为chunk的最后一句,或者在end之前,或者else前,或者until前):

local i = 1
while a[i] do
    if a[i] == v then break end
    i = i + 1
end

(5) C/C++ 中的 Lua

首先是最简单的 Lua 为 C/C++ 程序变量赋值,类似史前的 INI 配置文件。

width = 640
height = 480

这样的赋值即设置全局变量,本质上就是在全局表中添加字段。

在 C/C++ 中,Lua 其实并不是直接去改变变量的值,而是宿主程序通过「读取脚本中设置的全局变量到栈、类型检查、从栈上取值」几步去主动查询。

int w, h;
if (luaL_loadfile(L, fname) || // 读取文件,将内容作为一个函数压栈
    lua_pcall(L, 0, 0, 0))     // 执行栈顶函数,0个参数、0个返回值、无出错处理函数(出错时直接把错误信息压栈)
    error();

lua_getglobal(L, "width");     // 将全局变量 width 压栈
lua_getglobal(L, "height");    // 将全局变量 height 压栈
if (!lua_isnumber(L, -2))      // 自顶向下第二个元素是否为数字
    error();
if (!lua_isnumber(L, -1))      // 自顶向下第一个元素是否为数字
    error();
w = lua_tointeger(L, -2);      // 自顶向下第二个元素转为整型返回
h = lua_tointeger(L, -1);      // 自顶向下第一个元素转为整型返回

读取表的字段的操作也是类似,只不过细节上比较麻烦,有点让我想起在汇编里调戏各种寄存器:

score = { chinese=80, english=85 }

int chinese, english;
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0))
    error();

lua_getglobal(L, "score");       // 全局变量 score 压栈

lua_pushstring(L, "chinese");    // 字符串 math 压栈
lua_gettable(L, -2);             // 以自顶向下第二个元素为表、第一个元素为索引取值,弹栈,将该值压栈
if (!lua_isnumber(L, -1))        // 栈顶元素是否为数字
    error();
chinese = lua_tointeger(L, -2);
lua_pop(L, 1);                   // 弹出一个元素 (此时栈顶为 score 变量)

lua_getfield(L, -1, "english");  // Lua5.1开始提供该函数简化七八两行
if (!lua_isnumber(L, -1))
    error();
english = lua_tointeger(L, -2);
lua_pop(L, 1);                   // 如果就此结束,这一行弹不弹都无所谓了

前面说过,设置全局变量本质就是在全局表中添加字段,所以 lua_getglobal 函数本质是从全局表中读取字段。没错,lua_getglobal 本身就是一个宏:

(3) 创建模块

其实 Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。格式如下:

-- 定义一个名为 module 的模块
module = {}

-- 定义一个常量
module.constant = "this is a constant"

-- 定义一个函数
function module.func1()
    io.write("this is a public function!\n")
end

local function func2()
    print("this is a private function!")
end

function module.func3()
    func2()
end

return module

由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。不过上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的共有函数来调用。

最后,把上面的模块代码保存为跟模块名一样的 lua 文件里(例如上面是 module.lua),那么一个自定义的模块就创建成功。

(4) 加载模块

Lua 提供一个名为 require 的函数来加载模块,使用也很简单,它只有一个参数,这个参数就是要指定加载的模块名,例如:

require("<模块名>")
-- 或者是
-- require "<模块名>"

然后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。

或者给加载的模块定义一个别名变量,方便调用:

local m = require("module")

print(m.constant)

m.func3()

require 的意义就是导入一堆可用的名称,这些名称(非local的)都包含在一个table中,这个table再被包含在当前的全局表(“通常的那个_G”)中,这样访问一个模块中的变量就可以使用_G.table.**了,初学者可能会认为模块里的名称在导入后直接就是在 _G 中了。

m=require module 

的 m 取决于这个导入的文件的返回值,没有返回值时true,所以在标准的情况下模块的结尾应该 return 这个模块的名字,这样 m 就是这个模块的table了。

4. 标准库

String

string.byte
string.char
string.dump
string.find
string.format
string.gmatch
string.gsub
string.len
string.lower
string.match
string.rep
string.reverse
string.sub
string.upper

在string库中功能最强大的函数是:string.find(字符串查找),string.gsub(全局字符串替换),and string.gfind(全局字符串查找)。这些函数都是基于模式匹配的。

与其他脚本语言不同的是,Lua并不使用POSIX规范的正则表达式(也写作regexp)来进行模式匹配。主要的原因出于程序大小方面的考虑:实现一个典型的符合POSIX标准的regexp大概需要4000行代码,这比整个Lua标准库加在一起都大。权衡之下,Lua中的模式匹配的实现只用了500行代码,当然这意味着不可能实现POSIX所规范的所有更能。然而,Lua中的模式匹配功能是很强大的,并且包含了一些使用标准POSIX模式匹配不容易实现的功能。

(1) pattern 模式

下面的表列出了Lua支持的所有字符类:

.      任意字符
%a     字母
%c     控制字符
%d     数字
%l     小写字母
%p     标点字符
%s     空白符
%u     大写字母
%w     字母和数字
%x     十六进制数字
%z     代表0的字符

可以使用修饰符来修饰模式增强模式的表达能力,Lua中的模式修饰符有四个:

+      匹配前一字符1次或多次
*      匹配前一字符0次或多次
-      匹配前一字符0次或多次
?      匹配前一字符0次或1次

'%b' 用来匹配对称的字符。常写为 '%bxy' ,x和y是任意两个不同的字符;x作为匹配的开始,y作为匹配的结束。比如,'%b()' 匹配以 '(' 开始,以 ')' 结束的字符串:

print(string.gsub("a (enclosed (in) parentheses) line", "%b()", "")) --> a  line

常用的这种模式有:'%b()' ,'%b[]','%b%{%}' 和 '%b<>'。你也可以使用任何字符作为分隔符。

(2) capture 捕获

Capture是这样一种机制:可以使用模式串的一部分匹配目标串的一部分。将你想捕获的模式用圆括号括起来,就指定了一个capture。

pair = "name = Anna"
_, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
print(key, value)    --> name   Anna

(3) string.find 字符串查找

string.find 的基本应用就是用来在目标串(subject string)内搜索匹配指定的模式的串,函数返回两个值:匹配串开始索引和结束索引。

s = "hello world"
i, j = string.find(s, "hello")
print(i, j)                        --> 1    5
print(string.sub(s, i, j))         --> hello
print(string.find(s, "world"))     --> 7    11
i, j = string.find(s, "l")
print(i, j)                        --> 3    3
print(string.find(s, "lll"))       --> nil

string.find函数第三个参数是可选的:标示目标串中搜索的起始位置。

在string.find使用captures的时候,函数会返回捕获的值作为额外的结果:

pair = "name = Anna"
_, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
print(key, value)    --> name   Anna

看个例子,假定你想查找一个字符串中单引号或者双引号引起来的子串,你可能使用模式 '["'].-["']',但是这个模式对处理类似字符串 "it's all right" 会出问题。为了解决这个问题,可以使用向前引用,使用捕获的第一个引号来表示第二个引号:

s = [[then he said: "it's all right"!]]
a, b, c, quotedPart = string.find(s, "(["'])(.-)%1")
print(quotedPart)    --> it's all right
print(c)             --> "

(4) string.gmatch 全局字符串查找

string.gfind 函数比较适合用于范性 for 循环。他可以遍历一个字符串内所有匹配模式的子串。

words = {}
for w in string.gmatch("nick takes a stroll", "%a+") do
    table.insert(words, w)
end

URL解码

function unescape(s)
    s = string.gsub(s, "+", " ")
    s = string.gsub(s, "%%(%x%x)", function(h)
        return string.char(tonumber(h, 16))
    end)
    return s
end

print(unescape("a%2Bb+%3D+c")) -- a+b = c

对于name=value对,我们使用gfind解码,因为names和values都不能包含 '&' 和 '='我们可以用模式 '[^&=]+' 匹配他们:

cgi = {}
function decode (s)
    for name, value in string.gmatch(s, "([^&=]+)=([^&=]+)") do
       name = unescape(name)
       value = unescape(value)
       cgi[name] = value
    end
end

URL编码

这个函数将所有的特殊字符转换成 '%' 后跟字符对应的ASCII码转换成两位的16进制数字(不足两位,前面补0),然后将空白转换为 '+':

function escape(s)
    s = string.gsub(s, "([&=+%c])", function(c)
        return string.format("%%%02X", string.byte(c))
    end)
    s = string.gsub(s, " ", "+")
    return s
end

function encode(t)
    local s = ""
    for k, v in pairs(t) do
        s = s .. "&" .. escape(k) .. "=" .. escape(v)
    end
    return string.sub(s, 2) -- remove first '&'
end
t = {name = "al", query = "a+b = c", q = "yes or no"}

print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al

(5) string.gsub 全局字符串替换

string.gsub 函数有三个参数:目标串,模式串,替换串,第四个参数是可选的,用来限制替换的数量。

print(string.gsub("nck eats fish", "fish", "chips")) --> nick eats chips 1

string.gsub 的第二个返回值表示他进行替换操作的次数:

print(string.gsub("fish eats fish", "fish", "chips")) --> chips eats chips 2

使用模式:

print(string.gsub("nick eats fish", "[AEIOUaeiou]", ".")) --> n.ck ..ts f.sh 4

使用捕获:

print(string.gsub("nick eats fish", "([AEIOUaeiou])", "(%1)")) --> n(i)ck (e)(a)ts f(i)sh 4

使用替换函数:

function f(s)
    print("found " .. s)
end

string.gsub("Nick is taking a walk today", "%a+", f)

输出:
found Nick
found is
found taking
found a
found walk
found today

(6) string.sub, string.byte, string.format

s = "[in brackets]"
print(string.sub(s, 2, -2))     --> in brackets

string.char 函数和 string.byte 函数用来将字符在字符和数字之间转换,string.char 获取0个或多个整数,将每一个数字转换成字符,然后返回一个所有这些字符连接起来的字符串。string.byte(s, i) 将字符串s的第i个字符的转换成整数。

print(string.char(97))                    --> a
i = 99; print(string.char(i, i+1, i+2))   --> cde
print(string.byte("abc"))                 --> 97
print(string.byte("abc", 2))              --> 98
print(string.byte("abc", -1))             --> 99

string.format 和 C 语言的 printf 函数几乎一模一样,你完全可以照 C 语言的 printf 来使用这个函数,第一个参数为格式化串:由指示符和控制格式的字符组成。指示符后的控制格式的字符可以为:十进制'd';十六进制'x';八进制'o';浮点数'f';字符串's'。

print(string.format("pi = %.4f", PI)) --> pi = 3.1416
d = 5; m = 11; y = 1990
print(string.format("%02d/%02d/%04d", d, m, y)) --> 05/11/1990
tag, title = "h1", "a title"
print(string.format("<%s>%s</%s>", tag, title, tag)) --> <h1>a title</h1>

Table

table.concat
table.insert
table.maxn
table.remove
table.sort

(1) table.getn

print(table.getn{10,2,4})          --> 3
print(table.getn{10,2,nil})        --> 2
print(table.getn{10,2,nil; n=3})   --> 3
print(table.getn{n=1000})          --> 1000

a = {}
print(table.getn(a))               --> 0
table.setn(a, 10000)
print(table.getn(a))               --> 10000

a = {n=10}
print(table.getn(a))               --> 10
table.setn(a, 10000)
print(table.getn(a))               --> 10000

(2) table.insert, table.remove

table.isnert(table, value, position)
table.remove(table, position)

table库提供了从一个list的任意位置插入和删除元素的函数。table.insert函数在array指定位置插入一个元素,并将后面所有其他的元素后移。

a = {}
for line in io.lines() do
    table.insert(a, line)
end
print(table.getn(a))        --> (number of lines read)

table.remove 函数删除数组中指定位置的元素,并返回这个元素,所有后面的元素前移,并且数组的大小改变。不带位置参数调用的时候,他删除array的最后一个元素。

(3) table.sort

table.sort 有两个参数,存放元素的array和排序函数,排序函数有两个参数并且如果在array中排序后第一个参数在第二个参数前面,排序函数必须返回true。如果未提供排序函数,sort使用默认的小于操作符进行比较。

lines = {
    luaH_set = 10,
    luaH_get = 24,
    luaH_present = 48,
}

function pairsByKeys (t, f)
    local a = {}
    for n in pairs(t) do table.insert(a, n) end
    table.sort(a, f)
    local i = 0                 -- iterator variable
    local iter = function ()    -- iterator function
       i = i + 1
       if a[i] == nil then return nil
       else return a[i], t[a[i]]
       end
    end
    return iter
end

for name, line in pairsByKeys(lines) do
    print(name, line)
end

打印结果:

luaH_get          24
luaH_present      48
luaH_set          10

Coroutine

coroutine.create
coroutine.resume
coroutine.running
coroutine.status
coroutine.wrap
coroutine.yield

Math

math.abs
math.acos
math.asin
math.atan
math.atan2
math.ceil
math.cos
math.cosh
math.deg
math.exp
math.floor
math.fmod
math.frexp
math.huge
math.ldexp
math.log
math.log10
math.max
math.min
math.modf
math.pi
math.pow
math.rad
math.random
math.randomseed
math.sin
math.sinh
math.sqrt
math.tan
math.tanh

IO

io.close
io.flush
io.input
io.lines
io.open
io.output
io.popen
io.read
io.stderr
io.stdin
io.stdout
io.tmpfile
io.type
io.write

OS

os.clock
os.date
os.difftime
os.execute
os.exit
os.getenv
os.remove
os.rename
os.setlocale
os.time
os.tmpname

File

file:close
file:flush
file:lines
file:read
file:seek
file:setvbuf
file:write

Debug

debug.debug
debug.getfenv
debug.gethook
debug.getinfo
debug.getlocal
debug.getmetatable
debug.getregistry
debug.getupvalue
debug.setfenv
debug.sethook
debug.setlocal
debug.setmetatable
debug.setupvalue
debug.traceback

5. 协程 Coroutine

协程(coroutine)并不是 Lua 独有的概念,如果让我用一句话概括,那么大概就是:一种能够在运行途中主动中断,并且能够从中断处恢复运行的特殊函数。(嗯,其实不是函数。)

举个最原始的例子:

下面给出一个最简单的 Lua 中 coroutine 的用法演示:

function greet()
    print "hello world"
end

co = coroutine.create(greet) -- 创建 coroutine

print(coroutine.status(co))  -- 输出 suspended
print(coroutine.resume(co))  -- 输出 hello world
                             -- 输出 true (resume 的返回值)
print(coroutine.status(co))  -- 输出 dead
print(coroutine.resume(co))  -- 输出 false    cannot resume dead coroutine (resume 的返回值)
print(type(co))              -- 输出 thread

协程在创建时,需要把协程体函数传递给创建函数 create。新创建的协程处于 suspended 状态,可以使用 resume 让其运行,全部执行完成后协程处于 dead 状态。如果尝试 resume 一个 dead 状态的,则可以从 resume 返回值上看出执行失败。另外你还可以注意到 Lua 中协程(coroutine)的变量类型其实叫做「thread」Orz...

乍一看可能感觉和线程没什么两样,但需要注意的是 resume 函数只有在 greet 函数「返回」后才会返回(所以说协程像函数)。

 函数执行的中断与再开

单从上面这个例子,我们似乎可以得出结论:协程果然就是某种坑爹的函数调用方式啊。然而,协程的真正魅力来自于 resume 和 yield 这对好基友之间的羁绊。

函数 coroutine.resume(co[, val1, ...])

开始或恢复执行协程 co。

如果是开始执行,val1 及之后的值都作为参数传递给协程体函数;如果是恢复执行,val1 及之后的值都作为 yield 的返回值传递。

第一个返回值(还记得 Lua 可以返回多个值吗?)为表示执行成功与否的布尔值。如果成功,之后的返回值是 yield 的参数;如果失败,第二个返回值为失败的原因(Lua 的很多函数都采用这种错误处理方式)。

当然,如果是协程体函数执行完毕 return 而不是 yield,那么 resume 第一个返回值后跟着的就是其返回值。

函数 coroutine.yield(...)

中断协程的执行,使得开启该协程的 coroutine.resume 返回。再度调用 coroutine.resume 时,会从该 yield 处恢复执行。

当然,yield 的所有参数都会作为 resume 第一个返回值后的返回值返回。

OK,总结一下:当 co = coroutine.create(f) 时,yield 和 resume 的关系如下图:

6. Table 数据结构

Lua中的table不是一种简单的数据结构,它可以作为其它数据结构的基础。如数组、记录、线性表、队列和集合等,在Lua中都可以通过table来表示。

(1) 数组:

使用整数来索引table即可在Lua中实现数组。因此,Lua中的数组没有固定的大小,如:

a = {}
for i = 1, 1000 do
    a[i] = 0
end
print("The length of array 'a' is " .. #a)
--The length of array 'a' is 1000

在Lua中,可以让任何数作为数组的起始索引,但通常而言,都会使用1作为其起始索引值。而且很多Lua的内置功能和函数都依赖这一特征,因此在没有充分理由的前提下,尽量保证这一规则。下面的方法是通过table的构造器来创建并初始化一个数组的,如:

squares = {1, 4, 9, 16, 25}

(2) 二维数组:

在Lua中我们可以通过两种方式来利用table构造多维数组。其中,第一种方式通过“数组的数组”的方式来实现多维数组的,即在一维数组上的每个元素也同样为table对象,如:

mt = {}
for i = 1, N do
    mt[i] = {}
    for j = 1, M do
        mt[i][j] = i * j
    end
end

第二种方式是将二维数组的索引展开,并以固定的常量作为第二维度的步长,如:

mt = {}
for i = 1, N do
    for j = 1, M do
        mt[(i - 1) * M + j] = i * j
    end
end

(3) 链表:

由于table是动态的实体,所以在Lua中实现链表是很方便的。其中,每个结点均以table来表示,一个“链接”只是结点中的一个字段,该字段包含对其它table的引用,如:

list = nil
for i = 1, 10 do
    list = { next = list, value = i}
end

local l = list
while l do
    print(l.value)
    l = l.next
end

(4) 队列与双向队列:

在Lua中实现队列的简单方法是使用table库函数insert和remove。但是由于这种方法会导致后续元素的移动,因此当队列的数据量较大时,不建议使用该方法。下面的代码是一种更高效的实现方式,如:

List = {}

function List.new()
    return {first = 0, last = -1}
end

function List.pushFront(list, value)
    local first = list.first - 1
    list.first = first
    list[first] = value
end

function List.pushBack(list, value)
    local last = list.last + 1
    list.last = last
    list[last] = value
end

function List.popFront(list)
    local first = list.first
    if first > list.last then
        error("List is empty")
    end
    local value = list[first]
    list[first] = nil
    list.first = first + 1
    return value
end

function List.popBack(list)
    local last = list.last
    if list.first > last then
        error("List is empty")
    end
    local value = list[last]
    list[last] = nil
    list.last = last - 1
    return value
end

(5) 集合和包(Bag):

在Lua中用table实现集合是非常简单的,见如下代码:

reserved = { ["while"] = true, ["end"] = true, ["function"] = true, }
if not reserved["while"] then
    --do something
end

在Lua中我们可以将包(Bag)看成MultiSet,与普通集合不同的是该容器中允许key相同的元素在容器中多次出现。下面的代码通过为table中的元素添加计数器的方式来模拟实现该数据结构,如:

function insert(bag, element)
    bag[element] = (bag[element] or 0) + 1
end

function remove(bag, element)
    local count = bag[element]
    bag[element] = (count and count > 1) and count - 1 or nil
end

(6) StringBuilder:

如果想在Lua中将多个字符串连接成为一个大字符串的话,可以通过如下方式实现,如:

local buff = ""
for line in io.lines() do
    buff = buff .. line .. "\n"
end

上面的代码确实可以正常的完成工作,然而当行数较多时,这种方法将会导致大量的内存重新分配和内存间的数据拷贝,由此而带来的性能开销也是相当可观的。事实上,在很多编程语言中String都是不可变对象,如Java,因此如果通过该方式多次连接较大字符串时,均会导致同样的性能问题。为了解决该问题,Java中提供了StringBuilder类,而Lua中则可以利用table的concat方法来解决这一问题,见如下代码:

local t = {}
for line in io.lines() do
    t[#t + 1] = line .. "\n"
end
local s = table.concat(t)

--concat方法可以接受两个参数,因此上面的方式还可以改为:
local t = {}
for line in io.lines() do
    t[#t + 1] = line
end
local s = table.concat(t,"\n")

10. LuaJIT 介绍

一、什么是lua&luaJit

lua(www.lua.org)其实就是为了嵌入其它应用程序而开发的一个脚本语言,luajit(www.luajit.org)是lua的一个Just-In-Time也就是运行时编译器,也可以说是lua的一个高效版。

二、优势 

  1. lua是一个免费、小巧、简单、强大、高效、轻量级的嵌入式的脚本语言,lua当前的发行版本5.3.1只有276k
  2. 它是用C语言开发的项目,所以可以在大部分的操作系统上运行
  3. lua是目前速度最快的脚本语言,既可以提升语言的灵活性还可以最大限度的保留速度
  4. 其语法非常简单,没有特例
  5. lua还可以作为C的API来使用

三、不足和不同

  1. lua没有强大的库,所以很多功能实现起来没有python、perl、ruby等脚本语言简洁
  2. lua的异常处理功能饱受争议,虽然其提供了pcall和xpcall的异常处理函数
  3. lua原生语言中没有提供对unicode编码的支持,虽然可以通过一些折中的办法实现
  4. 没有提供在C++中应用很广泛的a?b:c的三元运算符操作
  5. 没有switch...case...语法,只能通过if..elseif..elseif..else..end的方式折中实现
  6. 在循环时没有提供continue语法
  7. 没有C++中应用广泛的a++和a+=1等操作
  8. lua的索引是从1开始的,而不是我们熟悉的0(string,table)
  9. 当你给一个元素赋值为nil时相当于这个元素不存在
  10. lua的数值类型只有number是没有int,float,double等之分的
  11. lua中没有类的概念,其类是通过table的形式来实现的
  12. lua中只有nil和false是表示假的,零在lua中是为真的
  13. 很多程序需要()标示才能运行,比如a={["b"]=5},print(a.b)是可运行的,但是 {["b"]=5}.b就会报错,需要({["b"]=5}).b才可以

Lua Web快速开发指南(1) - 初识cf框架

cf是什么?

cf全称为: CoreFramework. 一个基于Reactor事件驱动与协程的lua高性能网络框架, 目前主要面向HTTP Application开发.

cf内部主要实现了包括HTTP与HTTP Over Websoket协议的Server, 利用轻量级协程可以很轻松保持成千上万的长连接.

cf内置了丰富的开发库与常见的第三方协议, 目前在快速开发业务原型上有不可比拟的优势.

cf的C代码与lua封装的框架内部实现源码仅几千行,同时在源码包含了一系列中文注释方便大家阅读与code review.

cf的优势:

1. 学习优势

  • 全中文的wiki、issue.
  • 简单的Lua语法、快速的入门教程.
  • 丰富的内置库、高效的使用方式.

2. 开发优势

在使用cf进行业务开发时, cf框架丰富的内置库会开始给予我们很大的帮助:

  • httpd库

httpd为开发者提供了三种路由注入方式: api接口路由、use页面路由、 websocket路由. 这些方法赋予了httpd库在面对API、HTML、Websocket时的处理能力.

httpd还主动提供一个非常简单的静态文件服务器, 为开发者在开发期间提供诸如nginx那般静态文件查找的读取行为并且不依赖chroot的文件路径健康检查功能.

  • httpc库

httpc库提供了一套普通场景与微服务架构等常见的接口请求方案.

使用者可以使用最简单的httpc.posthttpc.posthttpc.jsonhttpc.file方法快速完成业务原型开发.

也可以使用httpc.class创建一个httpc对象后, 再使用上述方法进行业务开发. 不同的是连接在httpc对象close之前是不会被主动关闭的.

开发者还可以使用httpc.multi_request方法同时并发请求多个第三方接口, 这个异步方法会在所有接口请求完成(超时)后一起返回给调用者.

以上功能都是httpc内置的功能.

  • DB/Cache库

每当使用Lua语言进行开发的时候, 还需要思考连接池与连接复用的问题时. 可以尝试看看cf的mysql与redis封装的DB/Cache库.

是的. DB与Cache库的最大作用就是: 断线重连、连接复用、连接自动管理. 这也是DB/Cache库存在的主要原因.

  • MQ库

MQ库为开发者提供了一套跨进程的应用、实例共享的MQ发布与订阅解决方案, 目前实现了这些协议: stomp/redis/mqtt.

MQ库主要解决:异步任务发布、全局广播订阅、任务排队等等一系列特殊场景.

  • 其它

内置了一些第三方或者自行实现的库, 诸如:mail、json/xml、crypt、admin、cf; 这些都是业务开发的好帮手.

3. 性能优势

cf的源码追求的是KISS原则 - "以简单明了为主", 性能与稳定仅仅是使用C语言编写后所携带的附赠品而已.

使用Lua作为开发脚本语言也是因为高效的运行时与极低的内存占用浮动更能保证有效利用资源.

使用对象重用的方式来缓解频繁对象分配与释放造成的性能消耗, 减少内存碎片产生并且高效利用内存(即使是默认内存分配器).

使用Lua C API提供的第三方加密、编码、协议解析方式, 用以平衡Lua做字符串处理带来的一些性能问题.

 4. 自主集成

大部分的框架使用者习惯fork一个足够稳定的版本, 直到框架作者释放出下一个足够稳定的版本后才会尝试(或许不会)更新框架本身.

为了防止较为复杂的目录引用, cf提供了3rd目录来分离出用户自定义集成的库. 而这个文件夹内提供了联合编译所用到的文件makefile.

您在编译cf时(buildrebuildclean)都将传入到这个目录的makefile下.

makefile的作用不仅仅是编译用到的文件, 也可以是使用者自己脚本的管理集合. 总而言之, 就是使用者要怎么集成都随心所欲.

5. 部署优势

cf目前内置的httpd库可以独立提供稳定的httpd服务, 但是还是建议使用代理(负载均衡)软件进行构建安全(SSL)连接环境.

cf轻量级的依赖很适合批量部署, 无论是运用在CaaS环境还是传统宿主机的环境下都可以很轻松的完成启动.

cf项目的docker目录下提供了一份Dockerfile文件, 里面包含:

  • 从安装依赖到运行的所有步奏.
  • 快速启动的docker-compose部署文件.
  • 基于负载均衡器的动态伸缩的配置示例.

哪些人适合使用cf?

  • 动手能力较强的C/C++开发者
  • 当前语言级执行效率无法满足要求的开发者
  • 需要更为轻量级开发Web Service的开发者
  • 需要深度依赖容器的轻量级Web开发者
  • 适合无任何经验快速学习后端开发的新手
  • 简单来说就是适合所有想快速学习后端开发的人

极速安装

  • 利用docker快速下载、安装、运行

作者为docker使用者提供了一套预览镜像方便cf框架的学习者快速安装.

首先使用git将cf项目克隆下来并重命名为app, 然后进入到app目录下. 参考命令: git clone https://github.com/CandyMi/core_framework app

然后根据不同平台的要求安装dockerdocker-compose, 这需要您自行根据实际情况参考如何进行安装.

最后, 进入app/docker目录, 使用docker-compose -f docker-compose-with-cfadmin.yaml up命令运行cfadmin测试镜像并查看效果.

  • 在宿主机上快速下载、安装、运行

cf原生运行环境支持Mac、Linux、FreeBSD. 非这类平台请使用上一种安装方式.

先使用git将cf项目克隆下来并重命名为app, 然后进入到app目录下. 参考命令: git clone https://github.com/CandyMi/core_framework app

然后根据实际使用的操作系统使用setenvexport导入编译环境头文件查找目录与编译环境库文件目录: /usr/include/lib/usr/include/include.

然后开始安装依赖库:

  1. 安装readline-devel、make、autoconf、gcc、file、openssl/libressl; 推荐使用第三方包管理工具(apt/pacman/yum/brew/pkg)直接安装二进制版本.
  1. 安装libev; 可以在这里下载, 使用./configure --prefix=/usr/local && make && make install进行安装
  1. 安装lua5.3; 可以在这里点击下载源码, 根据实际平台替换后面的命令make linux/macosx/freebsd MYCFLAGS=-fPIC && make install进行安装.
  1. 最后开始编译安装cf. 进入./app目录使用sudo make build进行安装, 非管理员用户需要sudo. 如错误则安装完成. 

Lua Web快速开发指南(2) - cf的运行机制简介与基于httpd库的开发环境搭建

运行流程

在上一章节学会了如何安装cf后, 本章节就会介绍cf到运行机制与httpd的server搭建!

cf是一个非常典型的基于协程的事件驱动开发框架在封装成API后, 可以依赖事件循环执行一套稳定运行时环境.

而lua本身提供了强大的table数据结构可以根据需要自行构建所谓的"config", 所以cf为了减少无用的依赖就没有提供额外的config文件进行配置.

cf会假设所有业务代码文件都在script目录下, 所以建议您在script下自行划分好文件的目录归属.

cf将所有业务编写的脚本目录命名为scriptscript目录下点main.lua文件将会是入口文件. 这个main.lua执行完毕之后才会真正进入事件循环.

在执行完成script/main.lua文件后, cf则会是实际情况决定是否需要开始运行事件循环. 假设开发者仅仅想运行print("hello world"), 那么cf将会在main.lua执行完成后直接退出.

使用httpd库快速搭建lua web开发环境

httpd库是cf内置的基于http 1.1协议开发的web server! 高效解析器是必不可少的, httpd库使用picohttpparser解析器来构建http context.

我们假设您至少看过httpd库的API Reference, 并且至少知道下面所述的API.

此处所有的API与使用方式都将会在API Reference中找到.

1. 导入http库

httpd库位于app/lualib下, 使用者可以在main.lua文件内直接使用local httpd = require "httpd"导入httpd库.

2. 初始化一个httpd app对象

httpd库使用lua class对象进行创建! 默认提供了new方法, 使用者可以使用new方法创建一个httpd的app实例.

  1. local httpd = require "httpd"
  2. local app = httpd:new("app")

3. 注册静态文件路径

httpd提供给了内置的静态文件查找能力, 只需要使用者自动使用static方法注册静态文件路径即可.

  1. app:static("static", 30)

static表示使用者想将app/static文件夹当做静态文件的根目录.

4. 设置监听端口

httpd启动需要指定监听的端口, 默认监听所有网卡. 虽然没有使用第一个参数, 但是不可为空.

  1. app:listen("0.0.0.0", 8080)

5. 运行

在初始化完成后, app调用run方法将会启动httpd服务器. run方法后面的代码可能永远不会有机会执行.

  1. app:run() 

6. 运行cf

使用./cfadmin命令运行httpd server, 如果您看到类似运行等字样说明httpd服务已经启动完成, 否则将会有响应的错误提示.

Lua web快速开发指南(4) - 详细了解httpd库的作用

httpd库是基于HTTP 1.1协议实现而来, 内置了高性能的http协议解析器与urldecode解析库.

httpd库默认情况下就能工作的很好, 但是在一些需求较为极端的场景还是需要微调一下参数.

httpd常用的内置方法介绍

1. httpd:timeout(number)

设置每个连接到最大空闲(idle)连接等待时间, 超过这个数值httpd将主动断开连接. (默认值为:30秒)

2. httpd:max_path_size(number)

设置Path的最大长度, 超过这个值httpd将会返回414. (默认值为: 1024)

3. httpd:max_header_size(number)

设置Header最大长度, 超过这个值httpd将会返回431. (默认值为: 65535)

4. httpd:max_body_size(number)

设置Body的最大长度, 超过这个值将会返回413. (默认为 1024 * 1024)

5. httpd:before(function)

before方法决定API与USE路由回调在触发之前的行为, 默认情况下允许所有路由通过.

before方法一般用来设置与修改用户验证路由行为(例如头部验证), 这提供了开发者基于before函数设计中间件的机会.

当开发者设置了function后(即是是一个空函数), 需要利用http库来决定行为.

6. httpd:group(type, prefix, handles)

group方法提供了一种批量注册路由的方式, 为一组同一组路由提供简单便方便在注册方法.

第一个参数type为需要批量注册的路由类型; 初始化httpd对象后, 使用app.USEapp.API进行传值;

第二个参数prefix为string类型的头部; 例如:/api/admin;

第三个参数为一组路由处理函数或处理类数组; 类型为: {route = '/login', class = class};

注意: 此方法仅支持批量注册API与USE路由, 不可同时注册不同类型路由 

7. httpd:static(folder, ttl)

listen方法用于告诉httpd对象监听指定端口.

第一个参数ip暂未被httpd使用(但是必须设置), 默认监听所有网卡的'0.0.0.0'地址与指定的端口号;

backlog为用户最大连接等待队列, 合理的设置能减少连接被重置的情况(默认值为128).

8. httpd:run()

在httpd库所有参数与路由设置完毕之后, 调用run方法开启监听模式.

httpd的请求日志

日志格式为: [年/月/日 时:分:秒] - [ip] - [x-real-ip] - [path] - [method] - [http code] - [request handle timeline]

httpd的中间件

httpd库提供了before方法, 为开发人员自定义'中间件'行为提供了可能. 具体使用方法请参考http库

http content

每个http请求都会在调用before与用户注册的路由时为其传入一个content, 这个Content是客户端请求的所有参数.

args : 支持标准get或者post的参数, 对a[1]=1&a[2]=2将会不会解析为数组类型; 支持multipart/form-data的参数传递方式;

header: 原始header key-value表, 框架层不会进行header进行内容解析. (一般情况下没这个必要);

body : 目前body支持这些类型: multipart/form-dataapplication/x-www-form-urlencodedapplication/jsonapplication/xml;

json/xml: 在body为json类型的时候, content的json属性为true; 在body为xml类型的时候, content的xml属性为true.

file : 当客户端使用multipart/form-data传递数据时将会有这个属性; 这个属性是数组类型;

Lua web快速开发指南(5) - 利用template库构建httpd模板引擎

介绍template

模板引擎是为了使用户界面与业务数据(内容)分离而产生的, 其本身并不是一种深奥的技术.

template模板引擎首先会将合法的模板编译为lua函数, 然后将模板文件和数据通过模板引擎生成一份HTML代码.

cf的admin库整使使用了template来构建服务端渲染页面, 并利用单页面+iframe模式快速完成lua后台开发.

1. template基础语法

在真正使用之前, 我们先来学习一下template常见的一些基本语法:

  • {{ lua expression }} - lua expression是一段lua表达式; 作用为输出表达式的结果, 一些特殊符号将会被转义;
  • {* lua expression *} - lua expression是一段lua表达式; 作用为输出表达式的结果, 不会转义任何符号;
  • {% lua code %} - 执行一段lua代码, 如: {% for i = x, y do %} ... {% end %};
  • {# comments #}- comments仅作为注释, 不会包含在输出字符串内. 这段语法的作用类似lua内的----[[]];
  • {(template)} - 导入其它模板文件; 同时支持传参: {(file.html, { message = "Hello, World" })};

2. 转义字符

  • & 将会转义为 &
  • < 将会转义为 <
  • > 将会转义为 >
  • " 将会转义为 "
  • ' 将会转义为 '
  • / 将会转义为 /

3. API

  • template.compile(html)

参数html为字符串类型, 可以是:模板文件路径、

此方法返回一个渲染函数, 调用这个函数并传入一个table(key-value)作为参数则可以在模板文件内直接引用.

  • template.precompile(view, path, strip)

此方法用来将view预编译为lua的二进制代码块, strip是一个bool类型用来确定是否包含调试信息.

  • template.load(path)

此方法用来重写template内部的加载行为; 默认的模板加载流程为: 检查缓存 -> 读取文件 -> 解析文件 -> 渲染 -> 输出;

path字段为需要加载的文件路径或模板、html代码;

  • template.print(html)

此方法用来重写template内部渲染后的输出行为; 默认的输出行为: print

  • template.caching(Enable)

此方法用来告诉template是否缓存; 默认为true.

开始使用

现在尝试使用模板引擎完成一个静态页面的数据导入工作渲染一个页面并展示给用户看.

首先, 导入template库local template = require "template". 并且将目前我们熟知的编程语言名称都罗列出来.

  1. local languages = { 'C', 'C++', 'Java', 'golang', 'Rust', 'Ruby', 'Python', 'perl', 'Lua' }

然后, 我们在app目录下新建一个view目录, 并在view目录下新建一个名字为base.html的文件。 内容如下: 

  1. <html>
  2. <head>
  3. <title>{*title*}</title>
  4. </head>
  5. <body>
  6. <span><b>{*title*}:</b></span>
  7. <ul>
  8. {% if type(languages) == 'table' then %}
  9. {% for index, lang in ipairs(languages) do %}
  10. <li>{*index..'. '..lang*}</li>
  11. {% end %}
  12. {% end %}
  13. </ul>
  14. {# 没错, 注释不会展示给用户看到! #}
  15. </body>
  16. </html>

最后完成一个/languages的路由注册, 将我们刚刚完成的模板渲染出来.

  1. local template = require "template"
  2. app:use('/languages', function(content)
  3. template.cache = {}
  4. local languages = { 'C', 'C++', 'Java', 'golang', 'Rust', 'Ruby', 'Python', 'perl', 'Lua' }
  5. return template.compile("view/base.html"){
  6. title = '语言列表',
  7. languages = languages
  8. }
  9. end)

template.cache = {}的意思是, 每次都重新刷新缓存去读取文件, 这样方便我们进行调试.

最后打开http://localhost:8080/languages查看效果.

将一个模板分解到多个文件中

当一个项目的业务需求变得非常多的时候, 即是一个单纯的模板页面也会变得非常庞大并且不容维护与阅读.

现在我们来尝试将上面的模板进行模块化.

首先, 我们继续在app目录新建head.htmlcontent.html. 然后将这些代码拷贝进去: 

  1. {# 这是head.html的内容 #}
  2. <title>{*title*}</title> 
  1. {# 这是content.html的内容 #}
  2. <span><b>{*title*}:</b></span>
  3. <ul>
  4. {% if type(languages) == 'table' then %}
  5. {% for index, lang in ipairs(languages) do %}
  6. <li>{*index..'. '..lang*}</li>
  7. {% end %}
  8. {% end %}
  9. </ul>
  10. {# 没错, 注释不会展示给用户看到! #}

然后将原来的base.html修改为:

  1. <html>
  2. <head>
  3. {(view/head.html)}
  4. </head>
  5. <body>
  6. {(view/content.html)}
  7. </body>
  8. </html>

最后, 由于服务器会自动刷新模板缓存, 我们只需要再次刷新浏览器就能查看效果.

Lua web快速开发指南(6) - Cache、DB介绍

"数据库"与"缓存"的基本概念

数据库与缓存是服务端开发人员的必学知识点.

数据库

"数据库"是一种信息记录、存取的虚拟标记地点的集合统称. 比如现实生活中, 我们经常会用到文件柜、书桌等等数据存取容器.

在对容器进行数据存取的时候, 我们会为每一层打上一个标签表示一种分类项. 而这种在数据库中划分子分类形成了的概念. 这就是我们通常所说的结构化数据库.

由于通常数据表之间可能会存在依赖关系, 某一(或者多)层通常可能会用于同一种用途. 这种用途将一层划分为索引表, 二层划分为分类表, 三层划分为数据表.

实现这种功能与依赖关系的数据库, 我们称之为: 关系型数据库. 它可以定义一套规范并且建立数据存取模型, 这样方便维护一整套结构化的数据信息.

每当我们需要对数据进行结构化操作(查询、增加、删除、修改)的时候, 需要在计算机中用一种通俗易懂的语言表达方式来进行助记. 这种结构化查询语言称之为SQL.

缓存

我们通常将数据存储完毕后, 能通过指定或特定的一(多)种方式对数据进行操作. 在项目开发的初期, 这并没有太大的问题.

但是随着数据量的不断增大, 在数据库的内存中已经放不下这么多数据. 我们的数据逐渐无法被加载到内存中: 只会在使用的时候才会进行(随机)读取. 而这会加大磁盘I/O.

我们知道通常磁盘的读写速度基本上会比内存读写慢几个数量级(即使是SSD), 大量请求可能瞬间将磁盘IO占满并出现数据库的CPU利用率低、内存频繁进行修改/置换等问题.

为了解决这些问题, 出现了很多解决方案: 读、写分离、分表分库等等. 虽然有了这些方案, 但是也同样回引来新的问题: 主从同步、分布式事务等问题.

"缓存"则是近十年兴起的概念, 它的本质是一份数据结构化存储在内存中的副本. 高级的缓存我们也可以将其称之为内存数据库NOSQL(非关系型)数据库.

"缓存"也是一种"另类"解决数据库问题点一种手段! 它通过丰富的数据结构扩展了数据模型的组合能力, 通过简单的使用方法与高效的连接方式提供更好数据操作方式.

"缓存"将查询、更新较为频繁的数据组成一个集合加载进内存中, 较少使用的数据序列化到磁盘内部. 高效利用内存的同时, 根据变化的情况合理更新、删除缓存.

这样的方式配合数据库都读、写分离与数据分区将数据合理的从一个数据集副本分散到多个数据集副本, 有效的减少性能问题点产生并且提升了整个业务系统的横向扩展能.

DB库

DB库是cf框架封装自MySQL 4.1协议实现的客户端连接库, 提供MySQL断线重连、SQL重试、连接池等高级特性.

Cache

Cache库是cf封装自Redis 2.0协议实现的客户端连接库, 提供Redis断线重连、命令重试、连接池等高级特性.

Lua Web快速开发指南(7) - 高效的接口调用 - httpc库

httpc库基于cf框架都内部实现的socket编写的http client库.

httpc库内置SSL支持, 在不使用代理的情况下就可以请求第三方接口.

httpc支持header、args、body、timeout请求设置, 完美支持各种httpc调用方式.

API介绍

httpc库使用前需要手动导入httpc库: local httpc = require "httpc".

httpc.get(domain, HEADER, ARGS, TIMEOUT)

调用get方法将会对domain发起一次HTTP GET请求.

domain是一个符合URL定义规范的字符串;

HEADER是一个key-value数组, 一般用于添加自定义头部;

ARGS为请求参数的key-value数组, 对于GET方法将会自动格式化为:args[n][1]=args[n][2]&args[n+1][1]=args[n+1][2];

TIMEOUT为httpc请求的最大超时时间;

httpc.post(domain, HEADER, BODY, TIMEOUT)

调用post方法将会对domain发起一次HTTP POST请求, 此方法的content-type会被设置为:application/x-www-form-urlencoded.

domain是一个符合URL定义规范的字符串;

HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置;

BODY是一个key-value数组, 对于POST方法将会自动格式化为:body[n][1]=body[n][2]&body[n+1][1]=body[n+1][2];

TIMEOUT为httpc请求的最大超时时间;

httpc.json(domain, HEADER, JSON, TIMEOUT)

json方法将会对domain发起一次http POST请求. 此方法的content-type会被设置为:application/json.

HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置;

JSON必须是一个字符串类型;

TIMEOUT为httpc请求的最大超时时间;

httpc.file(domain, HEADER, FILES, TIMEOUT)

file方法将会对domain发起一次http POST请求.

HEADER是一个key-value数组, 一般用于添加自定义头部; 不支持Content-Type与Content-Length设置;

FILES是一个key-value数组, 每个item包含: name(名称), filename(文件名), file(文件内容), type(文件类型)等属性. 文件类型可选.

TIMEOUT为httpc请求的最大超时时间;

httpc 返回值

所有httpc请求接口均会有2个返回值: coderesponse. code为http协议状态码, response为回应body(字符串类型).

参数不正确, 连接被断开等其它错误, code将会为nil, response为错误信息.

"一次性HTTP请求"

什么是一次性httpc请求呢?

每次我们使用httpc库在请求第三方http接口的时候, 都会在接口返回后关闭连接. 这在日常使用中一边也没什么问题.

但是当我们需要多次请求同一个接口的时候, 每次请求完毕就关闭连接显然不是那么高效, 现在我们尝试使用一个http class对象来解决这个问题.

注意: httpc class对象不能对不同域名的接口使用同一个连接, 这会返回一个错误调用给使用者.

httpc库的class对象使用介绍

要使用httpc的class需要导入httpc的class库, 导入方式为: local httpc = require "httpc.class".

当需要使用httpc发起请求之前, 需要先创建一个httpc的对象, 如: local hc = httpc:new {}. httpc对象创建与初始化完毕后, 使用方式同上述API所示.

hchttpc拥有相同的API, 但是需要使用不同的调用方式. 如: hc:gethc:posthc:jsonhc:file.

一旦hc使用完毕时, 需要显示的调用hc:close()方法来关闭创建的httpc对象并销毁httpc的连接.

Lua Web快速开发指南(8) - 利用httpd提供Websocket服务

Websocket的技术背景

WebSocket是一种在单个TCP连接上进行全双工通信的协议, WebSocket通信协议于2011年被IETF定为标准RFC 6455并由RFC7936补充规范.

WebSocket使得客户端和服务器之间的数据交换变得更加简单, 使用WebSocket的API只需要完成一次握手就直接可以创建持久性的连接并进行双向数据传输.

WebSocket支持的客户端不仅限于浏览器(Web应用), 在现今应用市场内的众多App客户端的长连接推送服务都有一大部分是基于WebSocket协议来实现交互的.

Websocket由于使用HTTP协议升级而来, 在协议交互初期需要根据正常HTTP协议交互流程. 因此, Websocket也很容易建立在SSL数据加密技术的基础上进行通信.

协议

WebSocket与HTTP协议实现类似但也略有不同. 前面提到: WebSocket协议在进行交互之前需要进行握手握手协议的交互就是利用HTTP协议升级而来.

众所周知, HTTP协议是一种无状态的协议. 对于这种建立在请求->回应模式之上的连接, 即使在HTTP/1.1的规范上实现了Keep-alive也避免不了这个问题.

所以, Websocket通过HTTP/1.1协议的101状态码进行协议升级协商, 在服务器支持协议升级的条件下将回应升级请求来完成HTTP->TCP协议升级.

原理

客户端将在经过TCP3次握手之后发送一次HTTP升级连接请求, 请求中不仅包含HTTP交互所需要的头部信息, 同时也会包含Websocket交互所独有的加密信息.

当服务端在接受到客户端的协议升级请求的时候, 各类Web服务实现的实际情况, 对其中的请求版本、加密信息、协议升级详情进行判断. 错误(无效)的信息将会被拒绝.

在两端确认完成交互之后, 双方交互的协议将会从抛弃原有的HTTP协议转而使用Websocket特有协议交互方式. 协议规范可以参考RFC文档.

优势

在需要消息推送、连接保持、交互效率等要求下, 两种协议的转变将会带来交互方式的不同.

首先, Websocket协议使用头部压缩技术将头部压缩成2-10字节大小并且包含数据载荷长度, 这显著减少了网络交互的开销并且确保信息数据完整性.

如果假设在一个稳定(可能)的网络环境下将尽可能的减少连接建立开销、身份验证等带来的网络开销, 同时还能拥有比HTTP协议更方便的数据包解析方式.

其次, 由于基于Websocket的协议的在请求->回应上是双向的, 所以不会出现多个请求的阻塞连接的情况. 这也极大程度上减少了正常请求延迟的问题.

最后, Websocket还能给予开发者更多的连接管控能力: 连接超时、心跳判断等. 在合理的连接管理规划下, 这可提供使用者更优质的开发方案.

 

posted @ 2022-03-25 20:21  hanease  阅读(295)  评论(0编辑  收藏  举报