0%

在Go中运行Lua脚本(三)

使用golua库改造Loolob服务器的MusicFree接口部分

实战使用golua,将原go实现的MusicFree接口改造为lua实现,并添加热更新功能

引入lua

  1. 封装lua对象池

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    // 自定义Lua对象
    type Lua struct {
    *lua.State

    mutex sync.Mutex

    // 是否运行中
    isRunning bool

    // 回调引用
    listCallbackRefs []int

    // go对象表,通过lua直接创建go对象
    mapGoStruct map[string]interface{}

    // 通过go向lua发送数据,go端由Send函数发送,lua端通过Receive接收
    chanToLua chan interface{}

    // 通过lua向go发送数据,lua端由Send函数发送,go端通过Receive接收
    chanToGo chan interface{}

    // 回调函数,将lua数据转换为go数据类型
    callbackLuaToGo func(*Lua) interface{}

    // 回调函数,将go数据转换为lua数据
    callbackGoToLua func(*Lua, interface{}) int
    }

    // lua向go发送数据
    func lua_Send(state *lua.State) int {
    l := ToLua(state.Index)
    if l == nil {
    state.ArgError(1, "error runtime")
    return -1
    }
    if l.callbackLuaToGo != nil {
    v := l.callbackLuaToGo(l)
    l.chanToGo <- v
    } else {
    var data map[string]interface{}
    v := lua_luar.LuaToGo(l.State, 1, &data)
    l.chanToGo <- v
    }
    return 0
    }

    // lua接收来自go的数据
    func lua_Recv(state *lua.State) int {
    l := ToLua(state.Index)
    if l == nil {
    state.ArgError(1, "error runtime")
    return -1
    }
    data, ok := <-l.chanToLua
    if !ok {
    state.PushNil()
    return 1
    }
    if l.callbackGoToLua != nil {
    return l.callbackGoToLua(l, data)
    } else {
    lua_luar.GoToLua(l.State, data)
    return 1
    }
    }

    // 从对象池中取出虚拟机并执行
    func Run(
    file string,
    onInit func(*Lua),
    onRecv func(interface{}),
    onComplete func(error),
    ) *Lua {
    // 取出
    l := pool.Get().(*Lua)
    registerLua(l)

    l.mutex.Lock()
    l.isRunning = true
    l.init()
    l.mutex.Unlock()

    preloads := []func(*Lua){
    func(l *Lua) {
    lua_core.Preload(l.State)
    },
    onInit,
    }

    // 运行lua
    go func(l *Lua) {
    err := l.loadFile(file, preloads)
    l.clear()
    l.mutex.Lock()
    l.isRunning = false
    l.mutex.Unlock()
    if onComplete != nil {
    onComplete(err)
    }
    // 回收
    unregisterLua(l)
    pool.Put(l)
    }(l)

    // 接收来自lua的参数
    go func(l *Lua) {
    if onRecv != nil {
    l.receive(onRecv)
    }
    }(l)

    return l
    }
  2. 接入Loolob服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 初始化MusicFree接口部分时启动
    func (slf *MusicFreePlugin) startLua() {
    slf.mu.Lock()
    slf.lua = lblua.Run(
    "./lua/musicfreeplugin/main.lua",
    slf.onLuaInit,
    slf.onLuaRecv,
    slf.onLuaComplete,
    )
    slf.mu.Unlock()
    slf.lua.RegisterGoToLua(slf.onLuaTrans)
    fmt.Println("启动lua")
    }

改造原MusicFree接口

  1. 封装函数,将request请求丢给lua执行

原gin框架每个请求都是在独立的线程中执行,这里转换成lua有两种处理方式:一是在每个请求的handler中都启用一个lua虚拟机,来响应请求。二是将请求都交由同一个虚拟机处理

这里采用第二种方式,通过channel将请求都合并到lua线程中,交由同一个lua虚拟机执行。原来并行的请求响应,将变为串行执行,简化了逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 将请求转发至lua虚拟机,并阻塞等待
func (slf *MusicFreePlugin) requestToLua(route string, c *gin.Context) {
if c != nil {
done := make(chan any)
cb := func() {
close(done)
}
slf.mu.RLock()
slf.lua.Send(luaEvt{Route: route, Context: c, Complete: cb})
slf.mu.RUnlock()
// 设置5秒超时
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
select {
case <-done: // 耗时操作
c.Done()
case <-ctx.Done():
c.AbortWithStatus(http.StatusRequestTimeout)
}
} else {
slf.mu.RLock()
slf.lua.Send(luaEvt{Route: route, Context: nil, Complete: nil})
slf.mu.RUnlock()
}
}
  1. 修改原接口,将请求都转发至lua
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    func (slf *MusicFreePlugin) Init() {
    // ...
    group.POST("search", slf.search)
    group.GET("get", slf.get)
    group.GET("getrecommendsheettags", slf.getrecommendsheettags)
    group.POST("getrecommendsheetsbytag", slf.getrecommendsheetsbytag)
    group.POST("getmusicsheetinfo", slf.getmusicsheetinfo)
    // ...
    }

    func (slf *MusicFreePlugin) search(c *gin.Context) {
    slf.requestToLua("search", c)
    }

    func (slf *MusicFreePlugin) get(c *gin.Context) {
    slf.requestToLua("get", c)
    }

    func (slf *MusicFreePlugin) getrecommendsheettags(c *gin.Context) {
    slf.requestToLua("getrecommendsheettags", c)
    }

    func (slf *MusicFreePlugin) getrecommendsheetsbytag(c *gin.Context) {
    slf.requestToLua("getrecommendsheetsbytag", c)
    }

    func (slf *MusicFreePlugin) getmusicsheetinfo(c *gin.Context) {
    slf.requestToLua("getmusicsheetinfo", c)
    }

在lua中响应请求

  1. 将Context对象注册进lua

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    var METAKEY_Context = "Context"
    var METAAPI_Context = map[string]lua.LuaGoFunction{
    // ...
    "Data": lua_Context_Data,
    "Query": lua_Context_Query,
    "JSON": lua_Context_JSON,
    // ...
    }

    // func (c *Context) Data(code int, contentType string, data []byte)
    func lua_Context_Data(state *lua.State) int {
    L := lblua.ToLua(state.Index)
    obj := (*gin.Context)(L.ToGoPointer(1))
    if obj == nil {
    L.ArgError(1, "error type.")
    return -1
    }
    param2 := int(L.ToInteger(2))
    param3 := L.ToString(3)
    param4 := []byte(L.ToBytes(4))

    obj.Data(param2, param3, param4)

    return 0
    }

    // func (c *Context) Query(key string) (value string)
    func lua_Context_Query(state *lua.State) int {
    L := lblua.ToLua(state.Index)
    obj := (*gin.Context)(L.ToGoPointer(1))
    if obj == nil {
    L.ArgError(1, "error type.")
    return -1
    }
    param2 := L.ToString(2)

    result1 := obj.Query(param2)

    L.PushString(result1)
    return 1
    }

    // func (c *Context) JSON(code int, obj any)
    func lua_Context_JSON(state *lua.State) int {
    L := lblua.ToLua(state.Index)
    obj := (*gin.Context)(L.ToGoPointer(1))
    if obj == nil {
    L.ArgError(1, "error type.")
    return -1
    }
    param2 := int(L.ToInteger(2))
    param3 := L.ToGoPointer(3)

    obj.JSON(param2, param3)

    return 0
    }
  2. 在lua中获取请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-- 封装Context.Data
local DoneJson = function(evt, code, tb)
if evt == nil then
return
end
if evt.Context ~= nil then
evt.Context:Data(code, "application/json", json.encode(tb))
end
if evt.Complete ~= nil then
evt.Complete()
end
end

-- 封装Context.File
local DoneFile = function(evt, fp, size)
if evt == nil then
return
end
if evt.Context ~= nil then
evt.Context:FileStream(fp, size)
end
if evt.Complete ~= nil then
evt.Complete()
end
end

--从channel接收请求,并执行对应的逻辑
local function main()
-- ......
while true do
-- ......
local evt = core.Recv()
if evt == nil then
break
end
local Route = evt.Route
if Plugin[Route] ~= nil then
unsafe_xpcall(Plugin[Route], function(err)
debug.PrintWarn("处理事件[%s]失败: %s", Route, err)
DoneJson(evt, 500, { error = err })
end, Plugin, evt)
end
end
end
  1. 在lua中处理请求,以get接口为例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function Plugin:get(evt)
    local c = evt.Context
    local md5 = c:Query("id") -- 获取歌曲id
    if #md5 <= 0 then
    DoneJson(evt, 500, { error = "error id" })
    return
    end
    local item = nil
    -- 从缓存数据中查找对应的歌曲
    for _, v in pairs(self.MusicList) do
    if v.MD5 == md5 then
    item = v
    break
    end
    end
    if item == nil then
    DoneJson(evt, 500, { error = "error id" })
    return
    end
    -- 将歌曲返回
    DoneFile(evt, item.FPath, self.ChunkSize)
    end

添加热更新功能

由于golua库,不支持预编译字节码,每次调用LoadFile函数,都是从本地加载lua文件执行。

所以实现热更新,只需要更新本地文件后,重新调用LoadFile函数即可

1. 新增reload接口

1
2
3
4
5
6
7
8
9
10
func (slf *MusicFreePlugin) Init() {
// ...
group.GET("reload", slf.reload)
// ...
}

func (slf *MusicFreePlugin) reload(c *gin.Context) {
slf.Reload()
c.JSON(200, gin.H{"success": "true"})
}

2. 通过nil变量控制lua虚拟机的退出

收到reload请求,向当前lua虚拟机发送一个nil,通知虚拟机退出

1
2
3
4
5
6
// 向lua虚拟机发送nil
func (slf *MusicFreePlugin) Reload() {
slf.mu.Lock()
slf.lua.Send(nil)
slf.mu.Unlock()
}

收到nil后跳出事件循环,结束执行

1
2
3
4
5
6
7
8
9
10
11
local function main()
-- ...
while true do
-- ...
local evt = core.Recv()
if evt == nil then
break
end
-- ...
end
end

3. 在当前虚拟机退出后,重启虚拟机重新加载lua文件

虚拟机退出后,直接重启。此时若本地lua文件已经更新过了,则新启动的虚拟机将执行更新后的逻辑

1
2
3
4
5
6
func (slf *MusicFreePlugin) onLuaComplete(err error) {
if err != nil {
fmt.Printf("lua 异常退出: %v\n", err)
}
slf.startLua()
}