skynet如何启动一个lua服务

    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服务的服务。


三、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;
}

至此,我们将进入C语言实现部分。先总结一下lua部分的流程:
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的函数体比较长,其中重要的步骤包括:
 (1)根据参数实例化一个模块(这里是snlua)  :
 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服务来进行了。


  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X-Programer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值