skynet是如何启动一个lua语言编写的服务的的呢?skynet服务,在skynet框架中,具体是以什么形式存在的呢?
每个skynet进程在启动时,都会启动一个lua层的launcher服务,该服务主要负责skynet运作期间,服务的创建工作。我们在lua层创建一个lua层服务时,通常会调用skynet.newservice函数。带着问题,我们来一起看看skynet的源码。
lua部分
一、从skynet服务创建的接口说起,方式如下:
skynet.newservice(name, ...)
这个skynet.newservice函数的实现在skynet.lua文件中。
-- skynet.lua
function skynet.newservice(name, ...)
return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end
实际上是调用另外一个服务(.launcher)完成skynet服务的创建。
此时会发送消息给launcher服务,告诉launcher服务,要去创建一个snlua的c服务,并且绑定一个lua_State,该lua_State运行名称为name的lua脚本(这个脚本是入口),这里将c服务名称、脚本名称和参数,拼成一个字符串,并下传给c层。
我们先来看看这个.launcher是个什么鬼。
--bootstrap.lua
local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher)
通过wiki我们知道,bootstrap.lua是skynet服务的启动入口,在这里,调用了skynet.launch,启动了一个launcher服务,并将其命名为.launcher。
二、看下launcher服务的处理:
-- launcher.lua
-- 处理服务的创建
local function launch_service(service, ...)
local param = table.concat({...}, " ")
local inst = skynet.launch(service, param)
local response = skynet.response()
if inst then
services[inst] = service .. " " .. param
instance[inst] = response
else
response(false)
return
end
return inst
end
-- 处理 LAUNCH 类消息
function command.LAUNCH(_, service, ...)
launch_service(service, ...)
return NORET
end
-- 处理launcher服务接收到的消息
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.launch(service, param),这里的
service是snlua。
为什么要通过另外一个服务创建新的服务?主要目的是为了方便管理所有服务,比如统计,gc,杀掉服务等。
在launch_service中,调用到了skynet.launch。在最开始就看到了,.launcher的创建,也是通过调用skynet.launch实现的。这样,通过.launcher创建一个服务,和创建.launcher的服务的流程,就统一起来了。
.launcher是一个用于创建其他lua服务的服务。
.launcher是一个用于创建其他lua服务的服务。
三、skynet.launch的实现如下:
--manager.lua
local skynet = require "skynet"
local c = require "skynet.core"
function skynet.launch(...)
local addr = c.command("LAUNCH", table.concat({...}," "))
if addr then
return tonumber("0x" .. string.sub(addr , 2))
end
end
这里的skynet.core是一个C语言模块,编译成动态库给lua使用,
调用skynet.core.command(“LAUNCH”, “snlua launcher”)。
可以在loadfunc时利用luaopen_* 找到这个c函数。实际接口函数如下:
//lua-skynet.c
int
luaopen_skynet_core(lua_State *L) {
luaL_checkversion(L);
luaL_Reg l[] = {
{ "send" , lsend },
{ "genid", lgenid },
{ "redirect", lredirect },
{ "command" , lcommand },
{ "intcommand", lintcommand },
{ "error", lerror },
{ "tostring", ltostring },
{ "harbor", lharbor },
{ "pack", luaseri_pack },
{ "unpack", luaseri_unpack },
{ "packstring", lpackstring },
{ "trash" , ltrash },
{ "callback", lcallback },
{ "now", lnow },
{ NULL, NULL },
};
luaL_newlibtable(L, l);
lua_getfield(L, LUA_REGISTRYINDEX, "skynet_context");
struct skynet_context *ctx = lua_touserdata(L,-1);
if (ctx == NULL) {
return luaL_error(L, "Init skynet context first");
}
luaL_setfuncs(L,l,1);
return 1;
}
newservice–>skynet.call .launcher–>.launcher=skynet.launch(“snlua”, “launcher”)–>skynet.core.command(“LAUNCH”, “snlua launcher”)
C部分
四、接下来我们看看skynet.core中是如何实现的。
skynet.core其实是在lua_skynet.c中定义的,其command对应于lcommand函数。 这时的参数其实都压进了lua_State中。
//lua_skynet.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);
if (result) {
lua_pushstring(L, result);
return 1;
}
return 0;
}
在lcommand中,主要是调用 skynet_command(context, cmd, parm), cmd应该是LAUNCH,parm应该是 snlua launcher。context暂时按下不表,来看看skynet_command的实现。
//skynet-server.c
static struct command_func cmd_funcs[] = {
{ "TIMEOUT", cmd_timeout },
{ "REG", cmd_reg },
{ "QUERY", cmd_query },
{ "NAME", cmd_name },
{ "EXIT", cmd_exit },
{ "KILL", cmd_kill },
{ "LAUNCH", cmd_launch },
{ "GETENV", cmd_getenv },
{ "SETENV", cmd_setenv },
{ "STARTTIME", cmd_starttime },
{ "ABORT", cmd_abort },
{ "MONITOR", cmd_monitor },
{ "STAT", cmd_stat },
{ "LOGON", cmd_logon },
{ "LOGOFF", cmd_logoff },
{ "SIGNAL", cmd_signal },
{ 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);
}
++method;
}
return NULL;
}
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);// 实例化上下文
if (inst == NULL) {
return NULL;
} else {
id_to_hex(context->result, inst->handle);
return context->result;
}
}
在cmd_launch中,skynet_context_new(mod,args),mod是snlua,args是“snlua launcher”,会根据这两个参数,重新构造一个skynet_context出来
再看下这个skynet_context_new函数的实现。
// skynet_server.c
struct skynet_context *
skynet_context_new(const char * name, const char *param) {
/* 这一步加载name的动态库,这里是snlua.so
* snlua模块是 service_snlua.c 然后通过以下接口调用代码
* skynet_module_instance_create() --> snlua_create()
* skynet_module_instance_init() --> snlua_init()
* skynet_module_instance_release() --> snlua_release()
* skynet_module_instance_signal() --> snlua_signal()
*/
struct skynet_module * mod = skynet_module_query(name);
if (mod == NULL)
return NULL;
void *inst = skynet_module_instance_create(mod); // 执行snlua_create() 完成服务初始化
if (inst == NULL)
return NULL;
struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
CHECKCALLING_INIT(ctx)
ctx->mod = mod;
ctx->instance = inst;
ctx->ref = 2;
ctx->cb = NULL;
ctx->cb_ud = NULL;
ctx->session_id = 0;
ctx->logfile = NULL;
ctx->init = false;
ctx->endless = false;
// Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
ctx->handle = 0;
ctx->handle = skynet_handle_register(ctx);
struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);
// init function maybe use ctx->handle, so it must init at last
context_inc();
CHECKCALLING_BEGIN(ctx)
int r = skynet_module_instance_init(mod, inst, ctx, param); // 执行snlua_init() 完成服务的创建
CHECKCALLING_END(ctx)
if (r == 0) {
struct skynet_context * ret = skynet_context_release(ctx);
if (ret) {
ctx->init = true;
}
skynet_globalmq_push(queue);
if (ret) {
skynet_error(ret, "LAUNCH %s %s", name, param ? param : "");
}
return ret;
} else {
skynet_error(ctx, "FAILED launch %s", name);
uint32_t handle = ctx->handle;
skynet_context_release(ctx);
skynet_handle_retire(handle);
struct drop_t d = { handle };
skynet_mq_release(queue, drop_message, &d);
return NULL;
}
}
skynet_context_new的函数体比较长,其中重要的步骤包括:
void *inst = skynet_module_instance_create(mod); // 执行snlua_create() 完成服务初始化
(2)初始化struct skynet_context * ctx并注册ctx,创建此服务的消息队列:
struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
CHECKCALLING_INIT(ctx)
ctx->mod = mod;
ctx->instance = inst;
ctx->ref = 2;
ctx->cb = NULL;
ctx->cb_ud = NULL;
ctx->session_id = 0;
ctx->logfile = NULL;
ctx->init = false;
ctx->endless = false;
// Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
ctx->handle = 0;
(3)根据参数,初始化前面创建的模块(snlua)
int r = skynet_module_instance_init(mod, inst, ctx, param); // 执行snlua_init() 完成服务的创建
(4)把此服务的消息队列加入到全局消息队列
skynet_globalmq_push(queue);
在第1步中,加载模块(snlua)并调用了模块的create函数。
//service_snlua.c
struct snlua *
snlua_create(void) {
struct snlua * l = skynet_malloc(sizeof(*l));
memset(l,0,sizeof(*l));
l->mem_report = MEMORY_WARNING_REPORT;
l->mem_limit = 0;
l->L = lua_newstate(lalloc, l);
return l;
}
这里,新创建了一个lua_State。因此,正如wiki中所说,snlua是lua的一个沙盒服务,保证了各个lua服务之间是隔离的。
而第3步,其实是调用了snlua模块的init函数。
//service_snlua.c
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); // 设置回调函数为 NULL
int err = init_cb(l, context, msg, sz);
if (err) {
skynet_command(context, "EXIT", NULL);
}
return 0;
}
这里,launch_cb重置了服务的callback(因为snlua只用负责在沙盒中启动lua服务,其他消息应由lua程序处理),之后,调用了init_cb。
//service_snlua.c
// 完成服务的实例化,执行服务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);
lua_pushlightuserdata(L, ctx);
lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");
luaL_requiref(L, "skynet.codecache", codecache , 0);
lua_pop(L,1);
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");
int r = luaL_loadfile(L,loader); // 加载loader模块代码
if (r != LUA_OK) {
skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
lua_pushlstring(L, args, sz);
// 把服务名等参数传入,执行loader模块代码,实际上是通过loader加载和执行服务代码
r = lua_pcall(L,1,0,1);
if (r != LUA_OK) {
skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
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;
}
c初始化lua_State,先是将服务指针,skynet_context保存起来,以方便lua层调c的时候使用,然后就是一些配置设置,如设置lua服务脚本的存放路径,c服务so库的存放路径,lualib的存放路径等(加载和调用的时候,回到这些路径里找),然后该lua_State会加载一个用于执行指定脚本的loader.lua脚本,并将参数传给这个脚本(参数就是snlua服务绑定的lua脚本名称和传给这个脚本的参数拼起来的字符串,比如要启动一个名为scene的服务,那么对应的脚本名称就是scene.lua)
init_cb中除了设置各种路径、栈数据之外,和我们关心的lua程序有关的,是这样的一行:
//service_snlua.c
const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
int r = luaL_loadfile(L,loader);
if (r != LUA_OK) {
skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
lua_pushlstring(L, args, sz);
r = lua_pcall(L,1,0,1);
这里,就又通过C语言的lua接口,调用回了lua层面。
总结一下,C语言层面的处理流程是这样的:
skynet.core.command –> lcommand –> skynet_command –> cmd_launch –> skynet_context_new –> snlua_create –> snlua_init –> 加载loader.lua
回到lua
--loader.lua
local args = {}
for word in string.gmatch(..., "%S+") do
table.insert(args, word)
end
SERVICE_NAME = args[1]
local main, pattern
local err = {}
for pat in string.gmatch(LUA_SERVICE, "([^;]+);*") do
local filename = string.gsub(pat, "?", SERVICE_NAME)
local f, msg = loadfile(filename)
if not f then
table.insert(err, msg)
else
pattern = pat
main = f
break
end
end
if not main then
error(table.concat(err, "\n"))
end
LUA_SERVICE = nil
package.path , LUA_PATH = LUA_PATH
package.cpath , LUA_CPATH = LUA_CPATH
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
if LUA_PRELOAD then
local f = assert(loadfile(LUA_PRELOAD))
f(table.unpack(args))
LUA_PRELOAD = nil
end
main(select(2, table.unpack(args)))
loader.lua的功能也很简单,就是在沙盒snlua中,加载并执行lua程序,这里也就是launcher.lua。
在launcher.lua中,通过skynet.register_protocol和skynet.dispatch,设置了launcher服务对各种消息的处理函数。而在skynet.start的调用中
--skynet.lua
function skynet.start(start_func)
c.callback(skynet.dispatch_message)
skynet.timeout(0, function()
skynet.init_service(start_func)
end)
end
这里又重新设置了服务的callback。所以,所谓启动一个服务,其实就是将用lua编写的若干回调函数,挂载到对消息队列的处理中去。具体到这里的launcher服务,其实就是在lua层面,将command.LAUNCH等lua函数,挂接到消息队列中的LAUNCH等消息的回调函数。
最初的最初
作为创建服务的服务.launcher,它自己又是被谁创建的呢?前面我们看到,它是在bootstrap.lua中创建出来的。那么bootstrap.lua又是什么时候启动的呢?这里,我们需要看一下skynet启动时的第一个函数入口,main函数。
main函数隐藏在skynet_main.c中,当其解析完传入的config文件之后,就转到了skynet_start。
在skynet_start函数中,调用了bootstrap(ctx, config->bootstrap),其中,就像前面讲到的流程一样,直接走到了skynet_context_new这一步。
一般默认,config->bootstrap项就是snlua bootstrap。那这里其实就是启动调用bootstrap.lua,,其中会启动launcher服务。有了launcher服务,之后的服务启动,就都可以交由launcher服务来进行了。