游戏服务器开发中的其中一个难点:隔离性。在C/C++写的服务器中,一行代码中的空指针访问,就会导致整个服务器进程crash。

解决方式是:沙盒机制。

Skynet 的沙盒是利用Lua 实现的, 称为服务 snlua 。

下面重点讲这个沙盒是如何实现的

  • Skynet 启动
  • 沙盒启动API
  • snlua 启动

Skynet 启动

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 就是业务逻辑的入口。

沙盒启动API

Lua代码里, 启动其他沙盒服务有2个API

  • skynet.launch
  • skynet.newservice

例如,服务 bootstrap 启动服务 launcher

    -- bootstrap.lua
    local launcher = assert(skynet.launch("snlua","launcher"))

代码跟踪:

  • manage.lua @launch
  • lua-skynet.c @lcommand
  • skynet_service.c @skynet_command
  • skynet_service.c @cmd_launch
  • skynet_service.c @skynet_context_new

最终载入了一个 snlua 服务,用 launcher.lua 作为入口脚本。

那么, skynet.newservice 有什么不同那 ?

这个函数跟 launch 的区别是: 通过发送消息给服务 launcher, 由 launcher 来统一启动指定服务。

代码跟踪:

  • skynet.lua @newservice
  • 执行 skynet.call(“.launcher”, “lua” , “LAUNCH”, “snlua”, name, …)
  • 触发 launcher 执行代码
    • launcher.lua @command.LAUNCH
    • 执行 skynet.launch(service, param)

下面讲沙盒具体的启动过程

snlua 启动

启动服务 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
  • 服务自己发了第一个包给自己, 内容是 “launcher”

消息触发执行 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 进行具体的初始化

函数 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;
	}
	// 省略 ...
}
  • 设置寄存器的值, register[“skynet_context”] = ctx. 这个值之后会被 lua_skynet.c 这个模块访问。 这样,Lua 库函数就可以使用服务的API了。
  • 加载 loader.lua, 这里参数跟踪是字符串 “launcher”, 错误处理函数是 traceback 。
  • loader.lua 功能非常简单,根据配置项 LUA_SERVICE 找到服务 launcher 对应的源码。这里默认是 launcher.lua 。

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
  • skynet.start 是服务启动的最后一行代码。 调用了 c.callback
  • c.callback 就是 lua-skynet.c@lcallback
  • lcallback调用 skynet_callback。绑定消息回调函数 _cb.
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;
}

  • 当这个服务收到消息时,会触发 _cb 函数.
  • _cb 函数,驱动Lua虚拟机的主线程,执行 skynet.dispatch_message.
  • skynet.dispatch_message 会调用 skynet.lua@raw_dispatch_message
	-- 代码片段来自 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 注册的分发函数进行处理了。

总结:

  • skynet.newservice 可以启动一个沙盒服务
  • skynet.register_protocol 或者 skynet.dispatch 可以注册沙盒的消息分发函数
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐