launcher的创建
新入门skynet系列视频b站网址 https://www.bilibili.com/video/BV19d4y1678X
# launcher的创建
launcher服务是一个snlua服务。
当调用skynet.newservice请求创建某个服务的时候,实际上会把创建请求发送到launcher服务。launcher来真正启动一个目标服务。launcher本身又是怎么启动的?他是在bootstrap服务里面启动的。看看代码 11行。
bootstrap服务也是一个snlua服务
--bootstrap.lua
local skynet = require "skynet"
local harbor = require "skynet.harbor"
local service = require "skynet.service"
require "skynet.manager" -- import skynet.launch, ...
skynet.start(function()
local standalone = skynet.getenv "standalone"
--skynet.launch 用于启动一个 C 模块的服务。主要意思是 第一个参数是 c 模块的名字 可以是snlua logger...
local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher) --给服务取一个名字叫做.launcher
local harbor_id = tonumber(skynet.getenv "harbor" or 0)
if harbor_id == 0 then
assert(standalone == nil)
standalone = true
skynet.setenv("standalone", "true")
end
skynet.newservice "service_mgr"
pcall(skynet.newservice,skynet.getenv "start" or "main")
skynet.exit()
end)
也就是说 skynet.launch("snlua","launcher")
创建了 launcher服务。这两个参数的意思是 我们是要创建一个snlua服务,对应的脚本文件是launcher.lua。这个调用不会挂起我们当前协程,但不是在当前工作线程完成所有创建任务的。
当前工作线程完成步骤1
当前线程主要是push了 第一个消息 给我们创建的服务。我们跟踪一下
function skynet.launch(...)
local addr = c.command("LAUNCH", table.concat({...}," "))--next
if addr then
return tonumber(string.sub(addr , 2), 16)--把8位16进制数转变成lua表示的十进制数 /这里把开头的冒号去掉了
end
end
跟踪到c层。
static int
lcommand(lua_State *L) {
struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
const char * cmd = luaL_checkstring(L,1);
const char * result;
const char * parm = NULL;
if (lua_gettop(L) == 2) {
parm = luaL_checkstring(L,2);
}
result = skynet_command(context, cmd, parm);//cmd是"LAUNCH" parm是 "snlua launcher"
if (result) {
lua_pushstring(L, result);
return 1;
}
return 0;
}
11行
static struct command_func cmd_funcs[] = {
{ "LAUNCH", cmd_launch },//这里
{ NULL, NULL },
};
const char *
skynet_command(struct skynet_context * context, const char * cmd , const char * param) {
struct command_func * method = &cmd_funcs[0];
while(method->name) {
if (strcmp(cmd, method->name) == 0) {
return method->func(context, param);//这里调用 cmd_launch
}
++method;
}
return NULL;
}
继续跟踪 cmd_launch
static const char *
cmd_launch(struct skynet_context * context, const char * param) {
size_t sz = strlen(param);
char tmp[sz+1];
strcpy(tmp,param);
char * args = tmp;
char * mod = strsep(&args, " \t\r\n");
args = strsep(&args, "\r\n");
struct skynet_context * inst = skynet_context_new(mod,args);//mod是 "snlua" args是 "launcher"
if (inst == NULL) {
return NULL;
} else {
id_to_hex(context->result, inst->handle);//把8位16进制的handle用字符串表示 注意是以冒号开头
return context->result;
}
}
当我们看到 9行,就感觉有点熟悉了。因为我们的logger服务当初也是这样创建的。回顾logger服务。再次看看skynet_context_new
struct skynet_context *
skynet_context_new(const char * name, const char *param) {
struct skynet_module * mod = skynet_module_query(name);//获取模块
void *inst = skynet_module_instance_create(mod);//根据模块创建对应实例
struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
ctx->handle = skynet_handle_register(ctx);//获取一个handle
struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);//创建服务的队列
int r = skynet_module_instance_init(mod, inst, ctx, param);//初始化模块实例 param是 launcher
if (r == 0) {
struct skynet_context * ret = skynet_context_release(ctx);
if (ret) {
ctx->init = true;
}
skynet_globalmq_push(queue);//服务队列加入全局队列
return ret;
}
}
skynet_context_new 是创建一个服务,snlua服务跟logger服务创建过程大致类似。主要不同点在于他们属于不同的模块,所以对应模块的创建函数和初始化函数是不同的。我们主要看snlua模块的初始化实例的函数 snlua_init
static const char *
cmd_reg(struct skynet_context * context, const char * param) {
if (param == NULL || param[0] == '\0') {
sprintf(context->result, ":%x", context->handle);//%x 表示无符号十六进制整数
return context->result;//开头是冒号
}
}
int
snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
int sz = strlen(args);//此时 args 是 "launcher"
char * tmp = skynet_malloc(sz);
memcpy(tmp, args, sz);
skynet_callback(ctx, l , launch_cb);//服务队列里面第一个消息的处理 会调用launch_cb
const char * self = skynet_command(ctx, "REG", NULL);//最终调用 cmd_reg
uint32_t handle_id = strtoul(self+1, NULL, 16);//将字符串转换成unsigned long(无符号长整型数)
// it must be first message
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);//倒数第三个参数为0 表示 session
return 0;
主要做了两件事。
- skynet_callback 设置服务的回调函数 ,也就是处理队列中消息时使用的函数。这里设置的是 launch_cb
- skynet_send 往自己的队列中push了一个消息。
到这里 skynet_context_new 的处理完成。
我们再次回到调用 skynet_context_new 的地方
static const char *
cmd_launch(struct skynet_context * context, const char * param) {
size_t sz = strlen(param);
char tmp[sz+1];
strcpy(tmp,param);
char * args = tmp;
char * mod = strsep(&args, " \t\r\n");
args = strsep(&args, "\r\n");
struct skynet_context * inst = skynet_context_new(mod,args);//mod是 "snlua" args是 "launcher"
id_to_hex(context->result, inst->handle);//把8位16进制的handle用字符串表示 注意是以冒号开头
return context->result;//里面存的是这种":123123bb"
}
我们看 12行 最终返回了 新服务的地址。再回到lua层的起点
function skynet.launch(...)
local addr = c.command("LAUNCH", table.concat({...}," "))--next
if addr then
return tonumber(string.sub(addr , 2), 16)--把8位16进制数转变成lua表示的十进制数 /这里把开头的冒号去掉了
end
end
所以我们调用skynet.launch 最终返回了一个 新服务地址。总结下此时我们所在的bootstarp服务创建了 launcher服务,launcher给它自己的队列里push了第一个消息。launcher队列将来被工作线程执行到时,就会开始处理这个消息。
可能另一个工作线程完成步骤2
现在假设 有一个工作线程来驱动launcher工作了。那么第一个消息的处理会交给 launch_cb
。
static int
launch_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) {
assert(type == 0 && session == 0);
struct snlua *l = ud;
skynet_callback(context, NULL, NULL);//这里取消了回调函数 但是最终lua服务的回调函数的设置 是通过lua服务的skynet.start()函数
int err = init_cb(l, context, msg, sz);//next
return 0;
}
上面取消了回调函数。然后调用init_cb。这个函数你暂时需要关注两点。1.代表当前服务的context被保存在注册表中了。以后在需要context时,可以方便的获取。2. 我们最终执行了一个lua文件。这个lua文件就是launcher.lua服务代表的文件。
static int
init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {
lua_State *L = l->L;
l->ctx = ctx;
lua_gc(L, LUA_GCSTOP, 0);
lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */
lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
luaL_openlibs(L);
luaL_requiref(L, "skynet.profile", init_profile, 0);//当你在lua层require "skynet.profile" 的时候会调用c函数 init_profile ,最终返回一张表
int profile_lib = lua_gettop(L);
// replace coroutine.resume / coroutine.wrap
lua_getglobal(L, "coroutine");
lua_getfield(L, profile_lib, "resume");
lua_setfield(L, -2, "resume");
lua_getfield(L, profile_lib, "wrap");
lua_setfield(L, -2, "wrap");
lua_settop(L, profile_lib-1);
lua_pushlightuserdata(L, ctx);
lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");//把ctx设置到当前服务对应的 lua_State 的全局注册表中 key的名字是 "skynet_context"
luaL_requiref(L, "skynet.codecache", codecache , 0);//当你在lua层require "skynet.codecache" 的时候会调用c函数 codecache ,最终返回一张表
lua_pop(L,1);
lua_gc(L, LUA_GCGEN, 0, 0);
const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");
lua_pushstring(L, path);
lua_setglobal(L, "LUA_PATH");
const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");
lua_pushstring(L, cpath);
lua_setglobal(L, "LUA_CPATH");
const char *service = optstring(ctx, "luaservice", "./service/?.lua");
lua_pushstring(L, service);
lua_setglobal(L, "LUA_SERVICE");
const char *preload = skynet_command(ctx, "GETENV", "preload");
lua_pushstring(L, preload);
lua_setglobal(L, "LUA_PRELOAD");
lua_pushcfunction(L, traceback);
assert(lua_gettop(L) == 1);
const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");//我们真正的lua服务文件是被loader.lua文件加载执行的
int r = luaL_loadfile(L,loader);
lua_pushlstring(L, args, sz);//这里的arges是 "launcher"
r = lua_pcall(L,1,0,1);//开始执行lua文件了 这个文件是 loader.lua
lua_settop(L,0);
if (lua_getfield(L, LUA_REGISTRYINDEX, "memlimit") == LUA_TNUMBER) {
size_t limit = lua_tointeger(L, -1);
l->mem_limit = limit;
skynet_error(ctx, "Set memory limit to %.2f M", (float)limit / (1024 * 1024));
lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, "memlimit");
}
lua_pop(L, 1);
lua_gc(L, LUA_GCRESTART, 0);
return 0;
}
每个snlua服务都有一个 lua_State成员。lua_State代表一个lua环境,没有lua环境,后面是不能把消息交给lua处理的。
配置文件具体是怎么读取的呢 可以看这里 skynet启动时读取配置文件
我们看上面的lua文件是怎么被执行到的。看 49行。实际上我们第一个执行到的文件是 loader.lua。在这个文件里面,才运行了launcher.lua文件。
-- loader.lua
local args = {}
for word in string.gmatch(..., "%S+") do
table.insert(args, word)
end
SERVICE_NAME = args[1] --lua服务指定的文件名字 比如 launcher.lua 对应的名字是 launcher
local main, pattern
local err = {}
for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") do --LUA_SERVICE 是存放lua服务对应文件的所有路径 类似 "./service/?.lua;./skynet-1.5.0/test/?.lua;"
local filename = string.gsub(pat, "?", SERVICE_NAME) ---用 launcher替换掉路径配置中的 "?"
local f, msg = loadfile(filename)
if not f then
table.insert(err, msg)
else
pattern = pat
main = f
break --这里表示从其中一条路径找到一个文件就够了
end
end
-- LUA_PATH LUA_CPATH 这些都是启动skynet时在配置文件里设置的 不过配置文件中使用小写 比如lua_path
LUA_SERVICE = nil
package.path , LUA_PATH = LUA_PATH --等价于 package.path = LUA_PATH ;LUA_PATH = nil
package.cpath , LUA_CPATH = LUA_CPATH --等价于 package.cpath = LUA_CPATH ;LUA_CPATH = nil
--如果pattern类似 ./aaa/bbb/?/main.lua 那么下面返回值service_path是 ./aaa/bbb/?/
--一般情况下pattern类似 ./aaa/bbb/?.lua 那么下面的返回值service_path是 nil
local service_path = string.match(pattern, "(.*/)[^/?]+$")
if service_path then
service_path = string.gsub(service_path, "?", args[1])
package.path = service_path .. "?.lua;" .. package.path
SERVICE_PATH = service_path
else
local p = string.match(pattern, "(.*/).+$")
SERVICE_PATH = p
end
_G.require = (require "skynet.require").require --注意这里首先加载了 skynet.require 然后替换了lua原本的require
main(select(2, table.unpack(args))) --从这里开始执行我们指定的 lua服务文件
lua的模式匹配可以看看这里 lua模式匹配 ;另外
package.path , LUA_PATH = LUA_PATH
这句可以理解为package.path , LUA_PATH = func()
,当前func函数只有一个返回值,但是左边却有两个变量,所以LUA_PATH 只能是nil了
44行 最后开始执行我们的 launcher.lua文件。我们当前只关注下面的代码。
-- launcher.lua
skynet.dispatch("lua", function(session, address, cmd , ...)
cmd = string.upper(cmd)
local f = command[cmd]
if f then
local ret = f(address, ...)
if ret ~= NORET then
skynet.ret(skynet.pack(ret))
end
else
skynet.ret(skynet.pack {"Unknown command"} )
end
end)
skynet.start(function() end)
也就是说执行launcher.lua文件,主要是调用了上面两个函数。skynet.dispatch注册了 lua类型消息的回调函数。也就是收到lua类型消息时,都交给这个函数处理。另外一个是skynet.start函数。skynet.start主要做了两件事。1. 注册回调函数 2.注册一个定时器
function skynet.start(start_func)
c.callback(skynet.dispatch_message)--注册c层处理消息的回调函数,最终会把消息转交给lua层的函数
init_thread = skynet.timeout(0, function()--注册一个定时器
skynet.init_service(start_func)
init_thread = nil
end)
end
第2行是注册回调函数 我们看代码
static int
lcallback(lua_State *L) {
struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
int forward = lua_toboolean(L, 2);
luaL_checktype(L,1,LUA_TFUNCTION);
lua_settop(L,1);
lua_rawsetp(L, LUA_REGISTRYINDEX, _cb);//相当于 注册表[_cb] = fun
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
lua_State *gL = lua_tothread(L,-1);//获取我们的主协程 也就是当前lua层调用skynet.start所在的协程
skynet_callback(context, gL, _cb);//lua服务注册回调函数 在回调函数里面会调用lua应用层的消息分发函数skynet.dispatch_message
return 0;
}
注意上面的 13行,注册了服务的回调函数 _cb。我们看看当我们服务收到消息时,是怎么处理的。
static int
_cb(struct skynet_context * context, void * ud, int type, int session, uint32_t source, const void * msg, size_t sz) {
lua_State *L = ud;//这里是主协程
int trace = 1;
int r;
int top = lua_gettop(L);
if (top == 0) {
lua_pushcfunction(L, traceback);
lua_rawgetp(L, LUA_REGISTRYINDEX, _cb);
} else {
assert(top == 2);
}
lua_pushvalue(L,2);
lua_pushinteger(L, type);
lua_pushlightuserdata(L, (void *)msg);
lua_pushinteger(L,sz);
lua_pushinteger(L, session);
lua_pushinteger(L, source);
r = lua_pcall(L, 5, 0 , trace);//调用lua层的skynet.dispatch_message 其参数是prototype, msg, sz, session, source
if (r == LUA_OK) {
return 0;
}
}
注意 21行 最终把 消息的五个参数 传递给了lua层。接下来关注注册定时器的代码。关于定时器的使用,可以看看这里 初试定时器
function skynet.init_service(start)
local function main()
skynet_require.init_all()
start() --这个就是我们的start_func函数。可以认为是lua层的main函数
end
local ok, err = xpcall(main, traceback)
if not ok then
skynet.error("init service failed: " .. tostring(err))
skynet.send(".launcher","lua", "ERROR")
skynet.exit()
else
skynet.send(".launcher","lua", "LAUNCHOK")--任何服务在完成start后 都会发送LAUNCHOK 消息给 launcher服务
end
end
function skynet.start(start_func)
c.callback(skynet.dispatch_message)--注册c层处理消息的回调函数,最终会把消息转交给lua层的函数
init_thread = skynet.timeout(0, function()--注册一个定时器
skynet.init_service(start_func)
init_thread = nil
end)
end
上面代码注册了一个定时器。定时器会获取一个协程,这个协程的任务函数是一个 匿名函数。当定时器触发时,匿名函数被执行,这个匿名函数内部会调用我们注册的启动函数 start_func ,然后会发送一个消息通知 launcher。回到我们 launcher.lua文件
-- launcher.lua
skynet.dispatch("lua", function(session, address, cmd , ...)
cmd = string.upper(cmd)
local f = command[cmd]
if f then
local ret = f(address, ...)
if ret ~= NORET then
skynet.ret(skynet.pack(ret))
end
else
skynet.ret(skynet.pack {"Unknown command"} )
end
end)
skynet.start(function() end)
我们发现 我们通过skynet.start注册的star_func函数没有做任何事情。最后定时器协程被回收了。此时认为launcher服务启动完成了。over
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本