游戏服务器开发中的其中一个难点:隔离性。在C/C++写的服务器中,一行代码中的空指针访问,就会导致整个服务器进程crash。
解决方式是:沙盒机制。
Skynet 的沙盒是利用Lua 实现的, 称为服务 snlua 。
下面重点讲这个沙盒是如何实现的
Skynet 启动过程, 主要是启动了一些沙盒服务。
Skynet 配置文件一般是 Config 文件。
按照默认配置,启动时,部分日志如下:
$ ./skynet examples/config
[:01000001] LAUNCH logger
[:01000002] LAUNCH snlua bootstrap
[:01000003] LAUNCH snlua launcher
[:01000004] LAUNCH snlua cmaster
[:01000004] master listen socket 0.0.0.0:2013
[:01000005] LAUNCH snlua cslave
[:01000005] slave connect to master 127.0.0.1:2013
[:01000004] connect from 127.0.0.1:55126 4
[:01000006] LAUNCH harbor 1 16777221
[:01000004] Harbor 1 (fd=4) report 127.0.0.1:2526
[:01000005] Waiting for 0 harbors
[:01000005] Shakehand ready
[:01000007] LAUNCH snlua datacenterd
[:01000008] LAUNCH snlua service_mgr
[:01000009] LAUNCH snlua main
...
第一个启动的服务是 logger ,这个服务在之前已经介绍过了,是用C语言实现的。用来打印日志。
bootstrap 这个配置项关系着 skynet 运行的第二个服务。默认的 bootstrap 配置项为
snlua bootstrap
这意味着,skynet 会启动一个 snlua 沙盒服务,并将 bootstrap 作为参数传给它。
按默认配置,服务会加载 service/bootstrap.lua 作为入口脚本。启动后,这个 snlua 服务同样可以称为 bootstrap 服务。
bootstrap 服务, 会根据配置启动其他系统服务, 其中启动了 launcher 服务。更多细节可以见Bootstrap 。
最后,它启动了 main 服务。 main.lua 就是业务逻辑的入口。
Lua代码里, 启动其他沙盒服务有2个API
例如,服务 bootstrap 启动服务 launcher
-- bootstrap.lua
local launcher = assert(skynet.launch("snlua","launcher"))
代码跟踪:
最终载入了一个 snlua 服务,用 launcher.lua 作为入口脚本。
那么, skynet.newservice 有什么不同那 ?
这个函数跟 launch 的区别是: 通过发送消息给服务 launcher, 由 launcher 来统一启动指定服务。
代码跟踪:
下面讲沙盒具体的启动过程
启动服务 launcher 取例
skynet_context_new("snlua", "launcher")
服务的创建函数
struct snlua {
lua_State * L;
struct skynet_context * ctx; // 服务的句柄
size_t mem; // 当前使用的内存量,单位是byte
size_t mem_report; // 每次超过这个值,会产生日志告警
size_t mem_limit; // 内存上限
};
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;
}
每一个 snlua 服务都绑定了一个Lua VM。 Lua VM实现是线程安全的。
既然可以限制每个VM的内存,那么应该限制多少?
官方的建议:
玩家代理服务,可以设置上限到 128 M 左右。当然以过往经验,在正常情况通常应保持在 10M 以下。
读者可能还有一个疑问:每个服务一个 Lua VM, 函数字节码在进程里不是有很多份吗?
针对这个问题,云风大牛已经解决了:对Lua源码做了修改,可以支持多个Lua VM 共用函数字节码。
下面,看初始化函数
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_cb);
const char * self = skynet_command(ctx, "REG", NULL);
uint32_t handle_id = strtoul(self+1, NULL, 16);
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
return 0;
}
消息触发执行 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);
init_cb(l, context, msg, sz);
...
}
函数 init_cb
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_pushlightuserdata(L, ctx);
lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");
// 省略 ...
lua_pushcfunction(L, traceback);
assert(lua_gettop(L) == 1);
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);
if (r != LUA_OK) {
skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
// 省略 ...
}
Lua 加载 lancher.lua 。 最重要的是在Lua代码中注册了服务的消息分发函数
skynet.register_protocol {
name = "text",
id = skynet.PTYPE_TEXT,
unpack = skynet.tostring,
dispatch = function(session, address , cmd)
if cmd == "" then
command.LAUNCHOK(address)
elseif cmd == "ERROR" then
command.ERROR(address)
else
error ("Invalid text command " .. cmd)
end
end,
}
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)
对每一种类型的消息,都需要注册一个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
那么,服务收到一个消息后,又是如何执行这个Lua 分发函数的?
lancher.lua 最后一行
skyent.start(function () end)
function skynet.start(start_func)
c.callback(skynet.dispatch_message)
-- 这里可以理解为,直接执行初始化函数 start_func
skynet.timeout(0, function()
skynet.init_service(start_func)
end)
end
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);
// 寄存器中保存分发函数,register[&_cb] = skynet.dispatch_message
lua_rawsetp(L, LUA_REGISTRYINDEX, _cb);
// 取出状态机的主线程,注意:snlua 沙盒是由主线程进行调度
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
// 主线程
lua_State *gL = lua_tothread(L,-1);
// forward 模式下,这个消息处理完,并不释放内存
if (forward) {
skynet_callback(context, gL, forward_cb);
} else {
skynet_callback(context, gL, _cb);
}
return 0;
}
-- 代码片段来自 skynet.lua@raw_dispatch_message
local p = proto[prototype]
local f = p.dispatch
-- 针对这个新请求,创建出一个线程。切换协程
local co = co_create(f)
session_coroutine_id[co] = session
session_coroutine_address[co] = source
suspend(co, coroutine_resume(co, session,source, p.unpack(msg,sz)))
通过 proto,消息就能被之前 skynet.register_protocol 注册的分发函数进行处理了。
总结: