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"过程中还处理了额外的事情。

posted on 2018-03-02 16:24  RainRill  阅读(3396)  评论(0编辑  收藏  举报

导航