使用golua库改造Loolob服务器的MusicFree接口部分
实战使用golua,将原go实现的MusicFree接口改造为lua实现,并添加热更新功能
引入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
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
}接入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接口
- 封装函数,将request请求丢给lua执行
原gin框架每个请求都是在独立的线程中执行,这里转换成lua有两种处理方式:一是在每个请求的handler中都启用一个lua虚拟机,来响应请求。二是将请求都交由同一个虚拟机处理
这里采用第二种方式,通过channel将请求都合并到lua线程中,交由同一个lua虚拟机执行。原来并行的请求响应,将变为串行执行,简化了逻辑
1 | // 将请求转发至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
26
27
28
29func (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中响应请求
将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
57var 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
}在lua中获取请求
1 | -- 封装Context.Data |
- 在lua中处理请求,以get接口为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function 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 | func (slf *MusicFreePlugin) Init() { |
2. 通过nil变量控制lua虚拟机的退出
收到reload请求,向当前lua虚拟机发送一个nil,通知虚拟机退出
1 | // 向lua虚拟机发送nil |
收到nil后跳出事件循环,结束执行
1 | local function main() |
3. 在当前虚拟机退出后,重启虚拟机重新加载lua文件
虚拟机退出后,直接重启。此时若本地lua文件已经更新过了,则新启动的虚拟机将执行更新后的逻辑
1 | func (slf *MusicFreePlugin) onLuaComplete(err error) { |