skynet源码分析之snlua服务的启动流程(一)
skynet绝大部分服务类型是snlua,它是运行Lua脚本的服务,在用skynet框架上开发游戏服务器时,大部分逻辑都是snlua服务,90%以上只需写Lua代码即可,所以很有必要了解snlua服务相关内容。由于篇幅较多,打算分三篇文章介绍,都写完后再一起发布出去。本篇主要介绍snlua服务的启动流程,相关代码主要在service-src/service_snlua.c,lualib-src/lua-skynet.c,lualib/skynet.lua,lualib/loader.lua。bootstrap服务是skynet启动时创建的第一个snlua服务,以bootstrap为例说明snlua服务的启动流程。
1 // skynet-src/skynet_start.c 2 static void 3 bootstrap(struct skynet_context * logger, const char * cmdline) { 4 ... 5 struct skynet_context *ctx = skynet_context_new(name, args); // name="snlua" args="bootstrap" 6 ... 7 }
创建一个snlua类型的ctx,会调用snlua_init,注册消息回调函数launch_cb(第7行),然后给自己发第一条消息(第11行),至此ctx创建完成,但snlua服务的初始流程还未完成。
1 // service-src/service_snlua.c 2 int 3 snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) { 4 int sz = strlen(args); 5 char * tmp = skynet_malloc(sz); 6 memcpy(tmp, args, sz); 7 skynet_callback(ctx, l , launch_cb); 8 const char * self = skynet_command(ctx, "REG", NULL); 9 uint32_t handle_id = strtoul(self+1, NULL, 16); 10 // it must be first message 11 skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz); 12 return 0; 13 }
服务收到第一条消息后,先把消息回调函数至为NULL(之前设置的回调函数已失效,之后在Lua层会重新设置),然后调用消息回调函数init_cb,
1 // service-src/service_snlua.c 2 static int 3 launch_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) { 4 ... 5 skynet_callback(context, NULL, NULL); 6 int err = init_cb(l, context, msg, sz); 7 ... 8 }
在init_cb里进行Lua层的初始化,比如初始化LUA_PATH,LUA_CPATH,LUA_SERVICE等全局变量,主要有几个点:
1. 第7,8行,将ctx设置到LUA_REGISTRYINDEX里,以便在C与Lua的交互中可以获取到ctx
2. 10-12行,设置全局变量LUA_PRELOAD
3. 18行,加载loader.lua脚本
4. 25行,运行loader.lua,参数是“bootstrap”
1 // service-src/service_snlua.c 2 static int 3 init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) { 4 lua_State *L = l->L; 5 l->ctx = ctx; 6 ... 7 lua_pushlightuserdata(L, ctx); 8 lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context"); 9 ... 10 const char *preload = skynet_command(ctx, "GETENV", "preload"); 11 lua_pushstring(L, preload); 12 lua_setglobal(L, "LUA_PRELOAD"); 13 14 lua_pushcfunction(L, traceback); 15 assert(lua_gettop(L) == 1); 16 17 const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua"); 18 int r = luaL_loadfile(L,loader); 19 if (r != LUA_OK) { 20 skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1)); 21 report_launcher_error(ctx); 22 return 1; 23 } 24 lua_pushlstring(L, args, sz); 25 r = lua_pcall(L,1,0,1); 26 if (r != LUA_OK) { 27 skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1)); 28 report_launcher_error(ctx); 29 return 1; 30 } 31 ... 32 return 0; 33 }
在loader.lua里,主要做几点:
1. 第7行,设置全局变量SERVICE_NAME,因此在Lua层可以用SERVICE_NAME获取当前服务的名称
2. 11-22行,获取需启动的服务的Lua脚本(比如bootstrap.lua)的路径,并加载它(loadfile)
3. 24-28行,如果skynet启动配置里设置了LUA_PRELOAD,加载并运行它。每个snlua服务都加载了LUA_PRELOAD,所以经常把一个游戏里一些公用的配置放到LUA_PRELOAD里
4. 30行,运行Lua服务的入口脚本,比如bootstrap.lua,除第一个参数以外的所有参数(第一个参数是服务的名称)
1 -- lualib/loader.lua 2 local args = {} 3 for word in string.gmatch(..., "%S+") do 4 table.insert(args, word) 5 end 6 7 SERVICE_NAME = args[1] 8 9 local main, pattern 10 11 local err = {} 12 for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") do 13 local filename = string.gsub(pat, "?", SERVICE_NAME) 14 local f, msg = loadfile(filename) 15 if not f then 16 table.insert(err, msg) 17 else 18 pattern = pat 19 main = f 20 break 21 end 22 end 23 ... 24 if LUA_PRELOAD then 25 local f = assert(loadfile(LUA_PRELOAD)) 26 f(table.unpack(args)) 27 LUA_PRELOAD = nil 28 end 29 30 main(select(2, table.unpack(args)))
Lua服务的入口脚本必须包含2点:1. require "skynet",这样才能使用skynet.lua里的接口;2. 调用skyne.start函数
1 local skynet = require "skynet" 2 skynet.start(function() 3 ... 4 end)
skynet.lua提供了很多api供Lua服务调用,第1行代码是local c = require "skynet.core",skynet.core是由C编写的so库,so库里提供很多api供Lua层调用(lualib-src/lua-skynet.c)。require "skynet"过程中还做了其他事情放在下一篇介绍。
第6行,提供了很多注册函数供Lua层调用
26-31行,从LUA_REGISTERINDEX表中获取ctx(在init_cb里设置的),这些注册函数共用ctx这个上值,在C api里通过lua_upvalueindex(1)获取这个ctx,然后对ctx进行相应处理。
1 // lualib-src/lua-skynet.c 2 LUAMOD_API int 3 luaopen_skynet_core(lua_State *L) { 4 uaL_checkversion(L); 5 6 luaL_Reg l[] = { 7 { "send" , lsend }, 8 { "genid", lgenid }, 9 { "redirect", lredirect }, 10 { "command" , lcommand }, 11 { "intcommand", lintcommand }, 12 { "error", lerror }, 13 { "tostring", ltostring }, 14 { "harbor", lharbor }, 15 { "pack", luaseri_pack }, 16 { "unpack", luaseri_unpack }, 17 { "packstring", lpackstring }, 18 { "trash" , ltrash }, 19 { "callback", lcallback }, 20 { "now", lnow }, 21 { NULL, NULL }, 22 }; 23 24 luaL_newlibtable(L, l); 25 26 lua_getfield(L, LUA_REGISTRYINDEX, "skynet_context"); 27 struct skynet_context *ctx = lua_touserdata(L,-1); 28 if (ctx == NULL) { 29 return luaL_error(L, "Init skynet context first"); 30 } 31 luaL_setfuncs(L,l,1); 32 33 return 1; 34 }
Lua服务入口的第二件事是调用skynet.start,重新设置消息回调函数(第3行,之前设置的launch_cb回调函数已经失效了)
1 -- lualib/skynet.lua 2 function skynet.start(start_func) 3 c.callback(skynet.dispatch_message) 4 ... 5 end
调用C层的lcallback,通过lua_upvalueindex获取函数的上值ctx,然后设置服务的消息回调函数为_cb,此时Lua堆栈上有且仅有一个元素lua函数(skynet.dispatch_message)
1 // lualib/lua-skynet.c 2 static int 3 lcallback(lua_State *L) { 4 struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1)); 5 int forward = lua_toboolean(L, 2); 6 luaL_checktype(L,1,LUA_TFUNCTION); 7 lua_settop(L,1); 8 lua_rawsetp(L, LUA_REGISTRYINDEX, _cb); 9 10 lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD); 11 lua_State *gL = lua_tothread(L,-1); 12 13 if (forward) { 14 skynet_callback(context, gL, forward_cb); 15 } else { 16 skynet_callback(context, gL, _cb); 17 } 18 19 return 0; 20 }
在_cb里,最终会调用Lua层的dispatch_message,参数依次是:type, msg, sz, session, source。所以,snlua类型的服务收到消息时最终会调用Lua层的消息回调函数skynet.dispatch_message。
1 // lualib/lua-skynet.c 2 static int 3 _cb(struct skynet_context * context, void * ud, int type, int session, uint32_t source, const void * msg, size_t sz) { 4 ... 5 lua_pushinteger(L, type); 6 lua_pushlightuserdata(L, (void *)msg); 7 lua_pushinteger(L,sz); 8 lua_pushinteger(L, session); 9 lua_pushinteger(L, source); 10 11 r = lua_pcall(L, 5, 0 , trace); 12 ... 13 end
这就是snlua服务的启动流程。除了以上介绍,剩余的一些事情放到下一篇介绍,比如require "skynet"过程中还处理了额外的事情。