服务端Skynet(三)——启动lua服务
参考文献
在源码浅析和消息调度机制两篇文章中基本上了解了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层服务完整的创建流程。