skynet启动一个C服务

查看怎么启动C服务的最好的办法是打断点,看源码。

启动logger日志服务

我拿日志服务来举例子,怎么启动日志服务的呢?

  1. 传参数name = "logger", param = nullstruct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);;
  2. 从modules服务模块中取出名字为 logger的服务, struct skynet_module * result = _query(name);;
  3. 如果找不到则从cpath目录中找对应的.so文件,打开服务,并且存储到modules模块中。使用的函数是static void * _try_open(struct modules *m, const char * name);
  4. 创建服务实例m->create(),放到上下文skynet_contextinst中;
  5. skynet_context放到skynet_context list中, skynet_handle_register(ctx);;
  6. 创建logger服务对应的次级消息队列struct message_queue *queue,并且把消息队列push到全局消息队列中skynet_mq_create(ctx->handle);skynet_globalmq_push(queue);;
  7. 服务实例初始化m->init(),int r = skynet_module_instance_init(mod, inst, ctx, param);

总结下来,启动一个服务的过程就是,1.获取服务;2.创建服务实例;3.创建服务对应的上下文,并且把上下文放到skynet_context list管理模块中;4.绑定次级消息队列与对应的服务;5.服务初始化; 这五步过程。

snlua服务

boostrap服务是通过snlua服务启动的,所以在启动boostrap服务的时候先把snlua服务启动了,并放在了modules模块中,snlua服务的主要作用是启动lua服务。

  1. 创建snlua服务,先调用 snlua_create
struct snlua * snlua_create(void) {
    //初始化snlua结构
	struct snlua * l = skynet_malloc(sizeof(*l));
	memset(l,0,sizeof(*l));
	l->mem_report = MEMORY_WARNING_REPORT;
    ...
    //初始化一个lua虚拟机
	l->L = lua_newstate(lalloc, l);
    ...
	return l;
}
  • 创建一个snlua结构,创建一个Lua虚拟机,内存分配指定的是lalloc,目的是为了监控Lua分配的内存。MEMORY_WARNING_REPORT为Lua服务的内存阀值,超过该值,会报警
  1. snlua服务被创建后,初始化,调用的 snlua_init
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
    ...
    //指定回掉函数
	skynet_callback(ctx, l , launch_cb);
    //取本地服务的句柄
	const char * self = skynet_command(ctx, "REG", NULL);
   	uint32_t handle_id = strtoul(self+1, NULL, 16);
	// it must be first message
    //发送第一条消息,在launc_cb可见
	skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
	return 0;
}
  • 指定初始化的回调函数,launch_cb
  • 取得本地服务句柄
  • 向服务发送第一条消息, 打上PTYPE_TAG_DONTCOPY的tag,这表示skynet内部不会重新分配内存拷贝tmp。
  1. launch_cb函数处理snlua服务收到自己给自己发送的第一条消息,删除回调函数,并且调用init_cb函数,前面删除了回调,后面必定会把回调函数给加上(下文启动lua服务的时候会在lua脚本里设置回调),直接看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_gc(L, LUA_GCSTOP, 0);
    ...
    //LUA_PATH:Lua搜索路径,在config.lua_path指定。
	const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");
	lua_pushstring(L, path);
	lua_setglobal(L, "LUA_PATH");

    //LUA_CPATH:C模块的搜索路径,在config.lua_cpath指定。
	const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");
	lua_pushstring(L, cpath);
	lua_setglobal(L, "LUA_CPATH");

    //LUA_SERVICE:Lua服务的搜索路径,在config.luaservice指定。
	const char *service = optstring(ctx, "luaservice", "./service/?.lua");
	lua_pushstring(L, service);
	lua_setglobal(L, "LUA_SERVICE");

    //LUA_PRELOAD:预加载脚本,这些脚本会在所有服务开始之前执行,可以用它来初始化一些全局的设置。执行loader.lua,把要执行的脚本传进去,由loader去加载执行,skynet初始执行bootstrap.lua。
    const char *preload = skynet_command(ctx, "GETENV", "preload");
	lua_pushstring(L, preload);
	lua_setglobal(L, "LUA_PRELOAD");
	const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");

	int r = luaL_loadfile(L,loader);
    ...
	lua_pushlstring(L, args, sz);
    //执行lua脚本,调用脚本里 skynet.start的地方
	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_gc(L, LUA_GCRESTART, 0);

	return 0;
}

总结一下init_cb做的事情,设置lua全局变量:

  • LUA_PATH:Lua搜索路径,在config.lua_path指定。
  • LUA_CPATH:C模块的搜索路径,在config.lua_cpath指定。
  • LUA_SERVICE:Lua服务的搜索路径,在config.luaservice指定。
  • LUA_PRELOAD:预加载脚本,这些脚本会在所有服务开始之前执行,可以用它来初始化一些全局的设置。
  • 执行loader.lua,把要执行的脚本传进去,由loader去加载执行,skynet初始执行bootstrap.lua.

最后一步执行loader.lua,这一步很重要, r = lua_pcall(L,1,0,1);, 初始执行boostrap.lua脚本文件中,skynet.start()的地方。其他lua服务的skynet.start()也是在这里调用的。

boostrap服务

boostrap是引导服务,他是通过snlua服务启动的,属于lua服务。我们可以从boostrap.lua脚本看到该服务做的事情如下:

  • 启动launcher服务,这个launcher服务为服务启动器
  • 如果指定harborid,则说明这是一个主从分布式的skynet结构,不过skynet已经不推荐使用这种构架,略过。
  • 调用skynet.newservice启动datacenterd服务
  • 调用skynet.newservice启动service_mgr服务
  • 调用skynet.newservice启动 start or main服务 ,这是我们逻辑的入口

skynet启动一个lua服务

我们知道了snlua服务和boostrap服务的启动, skynet.launch是创建服务的通用版本,要在Lua创建某个C写的服务,可以使用它。但如果要创建一个Lua服务,则应该使用skynet.newservice

如果我有在A服务创建B服务,则有:

  1. 调用skynet.newservice(B,...)这个函数使A阻塞;
  2. B服务创建成功,B.lua这个脚本被执行,skynet.start(function()...end)这个函数被调用,表示服务B启动,可以接收消息,此详细过程可以看snlua服务启动的最后一步执行loader.lua;
  3. skynet.start(function() ... end)这个函数执行完了之后A的skynet.newservice(B,...)才返回,并且A得到了B的句柄。

内部怎么实现的呢?

  • skynet.newservice()函数实现
function skynet.newservice(name, ...)
	return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end

调用的是.launcher服务的LAUNCH函数,参数是snlua name, .launcher服务收到了消息后的处理:

--launcher.lua
local function launch_service(service, ...)
	local param = table.concat({...}, " ")
    --创建一个服务,并返回服务句柄
	local inst = skynet.launch(service, param)
	local session = skynet.context()
    --取一个resonse闭包,先存起来,等skynet.start返回再调用。
	local response = skynet.response()
	if inst then
		services[inst] = service .. " " .. param
		instance[inst] = response
		launch_session[inst] = session
	else
		response(false)
		return
	end
	return inst
end

function command.LAUNCH(_, service, ...)
	launch_service(service, ...)
	return NORET
end
  • 实际上只用看launch_service函数即可, service=snlua param=服务名B
  • 通过snlua创建lua服务B,并返回B的句柄
  • 取一个resonse闭包,先存起来,等skynet.start返回再调用。

B服务创建后,调用B.lua脚本,调用脚本里的skynet.start函数

function skynet.start(start_func)
	c.callback(skynet.dispatch_message)
	init_thread = skynet.timeout(0, function()
		skynet.init_service(start_func)
		init_thread = nil
	end)
end
  • 前文snlua启动的时候,在函数launch_cb中,删除了回调函数,这里c.callback,重新设置了收到消息后的回调。
  • c.callback,代码在lua-skynet.c中,,以后向这个服务发消息,skynet.dispatch_message就会被调用。

调用skynet.init_service(start_func):

function skynet.init_service(start)
	local function main()
		skynet_require.init_all()
		start()
	end
	local ok, err = xpcall(main, traceback)
	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

B服务在skynet.start调用完毕之后,会发送 LAUNCHOK消息:

function command.LAUNCHOK(address)
	-- init notice
	local response = instance[address]
	if response then
		response(true, address)
		instance[address] = nil
		launch_session[address] = nil
	end

	return NORET
end
  • instance取出response函数,调用它,传入true表示成功,后面跟的address就是skynet.call的返回值,这样A服务终于从skynet.newservice返回,并得到了B的地址(句柄)。
  • 所有经过skynet.newsevice创建的服务,都会记录在launcher服务中,launcher提供了些函数用于查询服务的状态。

为什么skynet.newservice阻塞

为什么A创建B服务的时候skynet.newservice会阻塞,并等到到B服务创建成功并返回句柄,才继续往下走,请看 skynet.call的代码

function skynet.call(addr, typename, ...)
	local tag = session_coroutine_tracetag[running_thread]
	if tag then
		c.trace(tag, "call", 2)
		c.send(addr, skynet.PTYPE_TRACE, 0, tag)
	end

	local p = proto[typename]
	local session = c.send(addr, p.id , nil , p.pack(...))
	if session == nil then
		error("call to invalid address " .. skynet.address(addr))
	end
	return p.unpack(yield_call(addr, session))
end
  • c.send直接返回,阻塞在return p.unpack(yield_call(addr, session))
  • yield_call是挂起了当前协程

在B服务创建成功,并且调用skynet.repsonse()之后,往A服务发送了一个协议skynet.PTYPE_RESPONSE

ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, pack(...))

A服务收到skynet.PTYPE_RESPONSE协议后,会回到yield_call挂起协程的地方

local function raw_dispatch_message(prototype, msg, sz, session, source)
	-- skynet.PTYPE_RESPONSE = 1, read skynet.h
	if prototype == 1 then
		local co = session_id_coroutine[session]
		if co == "BREAK" then
			session_id_coroutine[session] = nil
		elseif co == nil then
			unknown_response(session, source, msg, sz)
		else
			local tag = session_coroutine_tracetag[co]
			if tag then c.trace(tag, "resume") end
			session_id_coroutine[session] = nil
            -- 在这一步重新调用协程函数
			suspend(co, coroutine_resume(co, true, msg, sz))
		end
        ...
end
  • 这样就能解释为为什么skynet.newservice是阻塞的了。
  • 同理skynet.call是阻塞的.

--完--