skynet框架 源码分析 三


       今天我们来读service_lua.c文件。

       这个文件很重要,它是模块snlua的源文件,也是各个lua服务节点的制造者。比如:agent服务节点,watchdog服务节点,launch服务节点等。

       让我们来看看这个制造者是如何运作的。

       拿agent举例说:

              gate节点在服务端与新到的客户端连接建立成功之后,会向watchdog服务节点发送一条文本消息,消息内容含有open指令,意在生成两个新的服务节点,分别是client和agent服务节点。

watchdog收到消息后会运行command:open接口:

function command:open(parm)
	local fd,addr = string.match(parm,"(%d+) ([^%s]+)")
	fd = tonumber(fd)
	print("agent open", self, string.format("%d %d %s", self, fd, addr))
	local client = skynet.launch("client", fd)
	local agent = skynet.launch("snlua","agent",skynet.address(client))
	if agent then
		agent_all[self] = { agent , client }
		skynet.send(gate, "text", "forward" , self, skynet.address(agent) , skynet.address(client))
	end
end

       执行该接口时,watchdog会执行两条指令skynet.launch(client, ...)和skynet.launch(snalu, agent, ....)分别载入client节点,和agent节点。agent节点是由模块snlua制造的。让我们看看接下来会发生什么。
       watchdog通过脚本调用执行skynet.so库文件中的skynet_command接口载入节点agent。而后,初始化节点agent的上下文,并调用模块snlua的snlua_create接口,生成一个节点agent基础的基础数据,调用snlua_init初始化agent节点的数据并发送了一条消息(执行脚本agent.lua,当然现在还未执行)。此时,最基础的一个agent服务节点就已经产生了,当然,它收到的第一条消息就是读取agent.lua文件,并执行。
       新的worker线程,执行到agent节点时,首先,执行agent.lua脚本文件。里面包括,引入skynet.lua文件,设置client的handle值。skynet.lua比较复杂。skynet.lua中会引入skynet.so
库文件并调用luaopen_skynet_c接口,向当前lua状态机上的添加一张表,而后将表的地址赋给本地变量c。本地变量c表中记录了很多函数指针。当然经常导C++对象或者函数指针到lua中的人对这一块肯定不陌生。不过,我要说的是接下来要发生的逻辑。

local skynet = require "skynet"
local client = ...

skynet.register_protocol {
	name = "client",
	id = 3,
	pack = function(...) return ... end,
	unpack = skynet.tostring,
	dispatch = function (session, address, text)
		-- It's client, there is no session
		skynet.send("LOG", "text", "client message :" .. text)
		local result = skynet.call("SIMPLEDB", "text", text)
		skynet.ret(result)
	end
}

skynet.start(function()
	skynet.send(client,"text","Welcome to skynet")
end)

       首先通过skynet.register_protocol注册一个客户端消息处理函数,里面包含发送内容到log服务,调用simpledb执行指令,最后返回结果到客户端。

       接下来是skynet.start接口,这个接口很重要。我们看看里面写了什么。

function skynet.start(start_func)	
	c.callback(dispatch_message)
	trace_handle = assert(c.stat "trace")
	skynet.timeout(0, function()
		init_service(start_func)
	end)
end

       首先,执行c.callback,调用skynet.so库文件的接口_callback,设置agent节点的回调函数,这样每次有消息发送到agent节点,都会用函数dispatch_message来处理。而后,才会执行传进来的闭包 start_func。

       接下来dispatch_message通过pcall调用了接口raw_dispatch_message,而dispatch_message本事并没有太多逻辑。我们看看raw_dispatch_message接口是在干什么。

local function raw_dispatch_message(prototype, msg, sz, session, source, ...)
	-- 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
			c.trace_switch(trace_handle, session)
			session_id_coroutine[session] = nil
			suspend(co, coroutine.resume(co, msg, sz))
		end
	else
		local p = assert(proto[prototype], prototype)
		local f = p.dispatch
		if f then
			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, ...)))
		else
			print("Unknown request :" , p.unpack(msg,sz))
			error(string.format("Can't dispatch type %s : ", p.name))
		end
	end
end

       若消息类型值为1,则是返回消息,比方说向simpledb发送了一条文本消息,而后收到其返回结果。当然,不管raw_dispatch_message中走哪条分支,最终基本上都会调用suspend接口,并调用coroutine.resume接口,也就是用协程的方式运行某个lua闭包。解释一下lua协程。按我的理解lua协程其实只开辟了一个线程栈,但没有像操作系统的线程那样,成为一个调度单位。只是在lua主线程执行到某一处时,通过coroutine.resume切换到协程中执行,当执行完之后,通过coroutine.yield返回到主线程栈上继续执行。也就是说协程只是新建了一个空间来执行逻辑,执行完将执行结果返回给主线程,或者执行到一半,切换到主线程,等待下次被唤起继续执行。而之前注册的消息处理函数,就是在协程中执行的。如果已经理解了协程的概念,继续看其中的函数就不会太麻烦。比方说看skynet.call接口时,看到接口skynet.call时:

return p.unpack(coroutine_yield("CALL", assert(session, "call to invalid address")))
  有一个coroutine_yield,这时候,就是将"CALL", session返回给lua状态机的主线程,主线程是在suspend接口中调用该协程的,在收到返回数据后,继续执行suspend接口。
-- suspend is local function
function suspend(co, result, command, param, size)
	if not result then
		trace_count()
		error(debug.traceback(co,command))
	end
	if command == "CALL" then
		c.trace_register(trace_handle, param)
		session_id_coroutine[param] = co
	elseif command == "SLEEP" then
		c.trace_register(trace_handle, param)
		session_id_coroutine[param] = co
		sleep_session[co] = param
	elseif command == "RETURN" then
		local co_session = session_coroutine_id[co]
		local co_address = session_coroutine_address[co]
		-- PTYPE_RESPONSE = 1 , see skynet.h
		if param == nil then
			trace_count()
			error(debug.traceback(co))
		end
		-- c.send maybe throw a error, so call trace_count first.
		-- The coroutine execute time after skynet.ret() will not be trace.
		trace_count()
		c.send(co_address, 1, co_session, param, size)
		return suspend(co, coroutine.resume(co))
	elseif command == "EXIT" then
		-- coroutine exit
		session_coroutine_id[co] = nil
		session_coroutine_address[co] = nil
	else
		trace_count()
		error("Unknown command : " .. command .. "\n" .. debug.traceback(co))
	end
	trace_count()
	dispatch_wakeup()
end

        第一个co就不解释了,result是执行协程的结果,若成功返回true,否则返回false。command就是“CALL”,后面的是返回的参数。比方说现在param 就等于 session。此时,执行command == “call” 将 key = session,value = co保存到表session_id_coroutine表中。例如:当simpledb节点返回数据给agent节点时,首先,在simpledb中执行dispatch接口,执行skynet.ret接口,此时,协程会被挂起,且切换到simpledb主线程,而后返回一条“RETURN”command给suspend接口,suspend接口中从session_coroutine_id中取出session_id,从session_coroutine_address取出handle值。最后发送消息给该handle,也就是agent节点。如果要问为什么消息类型是返回类型,请看上段代码的:

		-- c.send maybe throw a error, so call trace_count first.
		-- The coroutine execute time after skynet.ret() will not be trace.
		trace_count()
		c.send(co_address, 1, co_session, param, size)
              c.send(xxx, 1 --- 此处1对应PTYPE_RESPONSE就是标明该消息是返回数据。









你可能感兴趣的:(linux)