服务端Skynet(三)——启动lua服务

本文详细解析了Skynet框架中Lua服务的启动过程,包括Lua层和C层的交互流程,以及如何调回Lua环境执行用户代码。通过具体示例帮助读者理解整个启动流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

服务端Skynet(三)——启动lua服务


参考文献

skynet设计综述

skynet源码赏析

在源码浅析和消息调度机制两篇文章中基本上了解了skynet 中 服务 与 消息调度 相关的理论基础。但是没有提及服务是这么注册到skynet_modules管理模块的,服务运行在什么环境。现在通过分析lua层创建服务的流程理解一下。

1、lua创建流程

--[[
先总结一下lua部分创建服务的流程:
newservice --> skynet.call.launcher --> .launcher=skynet.launch(“snlua”, “launcher”) --> skynet.core.command(“LAUNCH”, “snlua launcher”)
]]

​ 每个skynet进程在启动时,都会启动一个lua层的launcher服务,该服务主要负责skynet运作期间,服务的创建工作。我们在lua层创建一个lua层服务时,通常会调用skynet.newservice函数:

-- skynet.lua
function skynet.newservice(name, ...)
  --发送消息给launcher服务,告诉launcher服务,要去创建一个snlua的c服务,并且绑定一个lua_State ,该lua_State运行名称为name的lua脚本(这个脚本是入口)
	return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end

-- launcher.lua
local function launch_service(service, ...)
    --将c服务名称、脚本名称和参数,拼成一个字符串,并下传给c层
	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

function command.LAUNCH(_, service, ...)
	launch_service(service, ...)
	return NORET
end

这里实质是调用另外一个服务(.launcher)完成skynet服务的创建。 关于.launcher:

--[[
	bootstrap.lua 
	skynet服务的启动入口 在这里调用了skynet.launch,启动了一个launcher服务 
	具体的启动流程:
		在传入config后 -> skynet_start(&config) -> bootstrap(ctx, config->bootstrap) -> 启动调用bootstrap.lua(创建launcher服务)
	后续服务的创建都可以交给.launcher 服务进行

	为什么要用另一个服务创建新服务? 主要目的是为了方便管理所有服务,比如统计,gc,杀掉服务等。
]]
local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher)

skynet.launch的实现如下:

--manager.lua
local skynet = require "skynet"  
local c = require "skynet.core" 	--C语言模块  command方法
 
function skynet.launch(...)
	local addr = c.command("LAUNCH", table.concat({...}," "))
	if addr then
		return tonumber("0x" .. string.sub(addr , 2))
	end
end

2、C语言流程

/*
	总结一下C语言的处理流程:  
	skynet.core.command --> lcommand --> skynet_command --> cmd_launch --> skynet_context_new --> snlua_create --> snlua_init --> 加载loader.lua
*/

通过上面lua流程 现在跑到了skynet.core.command 看一下对应的C代码:

//lua_skynet.c   lcommand 对应lua中command
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;
}

/*
	lcommand 核心是调了skynet_command  看一下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 },
    };
*/
//通过上文 这里cmd初始是 LAUNCH
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;
}

//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");
    //snlua_create 这里mod:snlua args:snlua launcher
	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;
	}
}


// skynet_context_new  
struct skynet_context * skynet_context_new(const char * name, const char *param) {  
	struct skynet_module * mod = skynet_module_query(name);

	if (mod == NULL)
		return NULL;
	//根据参数实例化一个模块
    /*
		skynet_module_instance_create 这里会调用模块的create方法
		例如snlua 的snlua_create   //service_snlua.c
		lua_newstate(lalloc, l);  //snlua_create  创建了一个lua_State
		snlua是lua的一个沙盒服务,保证了各个lua服务之间是隔离的 

    */
	void *inst = skynet_module_instance_create(mod); // 
	if (inst == NULL)
		return NULL;
    //初始化并注册 ctx 创建该服务的消息队列
	struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));
	CHECKCALLING_INIT(ctx)

	ctx->mod = mod;
	ctx->instance = inst;
	ATOM_INIT(&ctx->ref , 2);
	ctx->cb = NULL;
	ctx->cb_ud = NULL;
	ctx->session_id = 0;
	ATOM_INIT(&ctx->logfile, (uintptr_t)NULL);

	ctx->init = false;
	ctx->endless = false;

	ctx->cpu_cost = 0;
	ctx->cpu_start = 0;
	ctx->message_count = 0;
	ctx->profile = G_NODE.profile;
	// 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);
	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;
	}
}
}  

此时,我们就已经创建了一个snlua的c服务,在创建snlua服务的过程中,会对新的snlua服务进行初始化操作:

// service_snlua.c
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
	int sz = strlen(args);
	char * tmp = skynet_malloc(sz);
	memcpy(tmp, args, sz);
	skynet_callback(ctx, l , _launch);
    //完成注册以后,向自己发送了一个消息,本snlua服务在接收到消息以后,就会调用_launch函数
    //snlua服务的回调函数会被赋空值,并进行一次snlua绑定的lua_State的初始化
	const char * self = skynet_command(ctx, "REG", NULL);
	uint32_t handle_id = strtoul(self+1, NULL, 16);
	// it must be first message
	skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
	return 0;
}

// snlua服务的callback函数
static int _launch(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);
    
	int err = _init(l, context, msg, sz);
	if (err) {
		skynet_command(context, "EXIT", NULL);
	}

	return 0;
}

初始化lua_state:

// service_snlua.c
static int _init(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {
	lua_State *L = l->L;
    //保存服务指针 以方便lua层调c使用
	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);

    //设置lua服务脚本的存放路径  c服务so库的存放路径  lualib的存放路径
	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);

    //------------------------------------------------------------调回lua---------------------------------------------------
    //lua_State会加载一个用于执行指定脚本的loader.lua脚本,并将参数传给这个脚本
    //(参数就是snlua服务绑定的lua脚本名称和传给这个脚本的参数拼起来的字符串,比如要启动一个名为scene的服务,那么对应的脚本名称就是scene.lua)
	const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
	// 加载loader模块代码
	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);
    //把服务名等参数传入,执行loader模块代码,实际上是通过loader加载和执行服务代码 
	r = lua_pcall(L,1,0,1);
    //------------------------------------------------------------调回lua---------------------------------------------------
	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);

	lua_gc(L, LUA_GCRESTART, 0);

	return 0;
}

3、调回到lua

--[[
	loader.lua
	
	在沙盒snlua中,加载并执行lua程序,可能执行如下几件事情:
    定义消息回调函数
    注册lua类型的消息回调函数
    注册除了lua类型以外的其他类型消息处理协议
    调用skynet.start函数,也就是将skynet.dispatch_message函数注册为lua服务的回调函数,所有派发给该lua服务的消息,都会传给这个函数;以及在下一帧执行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

_G.require = (require "skynet.require").require

main(select(2, table.unpack(args)))

skynet的lua服务,有一个proto表用于存放不同的消息类型的消息处理协议,一个协议,一般包含以下内容

  • name:表示协议类型的字符串,如lua类型,其值则为”lua”
  • id:标识协议类型的整型值,类型有
local skynet = {
	-- read skynet.h
	PTYPE_TEXT = 0,
	PTYPE_RESPONSE = 1,
	PTYPE_MULTICAST = 2,
	PTYPE_CLIENT = 3,
	PTYPE_SYSTEM = 4,
	PTYPE_HARBOR = 5,
	PTYPE_SOCKET = 6,
	PTYPE_ERROR = 7,
	PTYPE_QUEUE = 8,	-- used in deprecated mqueue, use skynet.queue instead
	PTYPE_DEBUG = 9,
	PTYPE_LUA = 10,
	PTYPE_SNAX = 11,
}
  • pack:发送消息时,对消息进行打包的函数
  • unpack:接收到消息时,先通过unpack函数,对消息进行解包后,再传给dispatch函数,最后实现消息回调
  • dispatch:消息队列里的指定类型的消息,最终会传到指定类型的dispatch函数来,这个函数一般是用户自己指定

4、example服务案例

现在通过启动一个example服务来举例说明,example服务的定义如下所示:

-- example.lua
local skynet = require "skynet"

skynet.register_protocol {
	name = "text",
	id = skynet.PTYPE_TEXT,
	unpack = function (msg, sz)
		return skynet.tostring(msg, sz)
	end,
	dispatch = function (_, _, type, arg)
		skynet.error(arg)
	end
}

local CMD = {}

function CMD.do_something(...)
	-- TODO
end

skynet.dispatch("lua", function(_,_, command, ...)
	local f = CMD[command]
	skynet.ret(skynet.pack(f(...)))
end)

skynet.start(function()
	-- TODO
end)

启动一个example服务,意味着loader脚本,最终执行的那个脚本就是example.lua这个脚本,在执行这个脚本的过程中,首先和其他所有的lua服务一样,这个example服务需要调用skynet.lua里的api,因此,需要require一下skynet.lua这个脚本,而在require的过程中,就已经注册了几个回调消息处理协议:

-- skynet.lua

...

----- register protocol
do
	local REG = skynet.register_protocol

	REG {
		name = "lua",
		id = skynet.PTYPE_LUA,
		pack = skynet.pack,
		unpack = skynet.unpack,
	}

	REG {
		name = "response",
		id = skynet.PTYPE_RESPONSE,
	}

	REG {
		name = "error",
		id = skynet.PTYPE_ERROR,
		unpack = function(...) return ... end,
		dispatch = _error_dispatch,
	}
end

...

这里事先注册了lua类型,response类型和error类型的消息处理协议,也就是说,一个lua服务至少保证lua类型、response类型和error类型默认有消息处理协议,而注册函数的流程就是将这个消息处理协议的结构存入proto表中,当一个lua服务接收到消息时,则会根据其消息类型,在proto表中找到对应的处理协议以后,调用该协议的unpack函数,将参数解包以后,再传给该协议的dispatch函数,最后达到驱动lua服务的目的:

-- skynet.lua
function skynet.register_protocol(class)
	local name = class.name
	local id = class.id
	assert(proto[name] == nil)
	assert(type(name) == "string" and type(id) == "number" and id >=0 and id <=255)
	proto[name] = class
	proto[id] = class
end

现在回到我们的example脚本,它注册了一个text类消息处理协议,然后为lua协议注册了一个回调函数,我们注意到,在skynet.lua这个脚本中,虽然有在proto表中注册lua类型协议(其中包括解包函数unpack),但是没有定义消费lua类型消息的回调函数dispatch,这个dispatch函数,需要用户自己定义,一般使用skynet.dispatch来完成

-- skynet.lua
function skynet.dispatch(typename, func)
	local p = proto[typename]
	if func then
		local ret = p.dispatch
		p.dispatch = func
		return ret
	else
		return p and p.dispatch
	end
end

exmaple脚本为lua协议,注册了一个lua层消息回调函数,后面example服务接收到的lua类型消息,都会被传到这个函数内
最后,example脚本执行了skynet.start函数,完成启动一个lua服务的最后工作

-- skynet.lua
function skynet.start(start_func)
	c.callback(skynet.dispatch_message)
	skynet.timeout(0, function()
		skynet.init_service(start_func)
	end)
end

这个函数,首先lua服务注册了一个lua层的消息回调函数,前面已经讨论过,一个c服务在消费次级消息队列的消息时,最终会调用callback函数,而这里做的工作则是,通过这个c层的callback函数,再转调lua层消息回调函数skynet.dispatch_message

// lua-skynet.c
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);

    ...

	return 0;
}

static int
_callback(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);

	lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
	lua_State *gL = lua_tothread(L,-1);

	if (forward) {
		skynet_callback(context, gL, forward_cb);
	} else {
		skynet_callback(context, gL, _cb);
	}

	return 0;
}

这里将snlua这个skynet_context的callback函数赋值为_cb,而_cb最终又会通过lua_State转调lua层的skynet.dispatch_message函数,也就是说,发送给snlua服务的消息,最终都是交给lua层去处理的
在完成lua层callback函数的注册以后,接下来就是执行lua服务的启动函数

-- skynet.lua
local function init_template(start)
	init_all()
	init_func = {}
	start()
	init_all()
end

function skynet.pcall(start)
	return xpcall(init_template, debug.traceback, start)
end

function skynet.init_service(start)
	local ok, err = skynet.pcall(start)
	if not ok then
		skynet.error("init service failed: " .. tostring(err))
		skynet.send(".launcher","lua", "ERROR")
		skynet.exit()
	else
		skynet.send(".launcher","lua", "LAUNCHOK")
	end
end

这里并没有立即执行这个start函数,而是故意放在了下一帧进行。到了目前这一步,整个example服务就被启动起来了,虽然他并没有执行什么逻辑,但是却展现了一个lua层服务完整的创建流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值