今天我们来读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制造的。让我们看看接下来会发生什么。
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就是标明该消息是返回数据。