探索skynet(四):服务之间的通信


《探索skynet(三):消息队列》中已经提到,skynet中每个服务都有自己的地址和消息队列。有了这个基础,理解服务之间的消息通信,就比较简单了。

skynet.call

以最常用到的skynet.call为例,它通过调用skynet.core.send(也即,lua-skynet.c中的lsend函数)–> skynet_send函数 –> skynet_context_push函数,向目标服务的消息队列中插入了一条消息。

插入消息之后,会返回给lua层一个session id,而在lua函数skynet.call中,则会调用coroutine_yield(”CALL”, session) 来依据session缓存。

对于服务的消息队列的回调函数注册和实际的回调处理,在《探索skynet(三):消息队列》里已经提到过了。这里,我们需要留意的是,在lua层实现的回调函数中,一般是通过skynet.ret调用来传送返回值的。例如在skynet_sample/lualib/service.lua中

service.lua
1
2
3
4
5
6
7
8
9
10
11
-- some other code
skynet.dispatch("lua", function (_,_, cmd, ...)
local f = funcs[cmd]
if f then
skynet.ret(skynet.pack(f(...)))
else
log("Unknown command : [%s]", cmd)
skynet.response()(false)
end
end
-- some other code

skynet.ret会调用coroutine_yield(“RETURN”, msg, sz)。

协程

CALL和RETURN看上去就是一对儿,事实上也确实是这样的。搜索CALL和RETURN两个字符串,发现他们是在suspend这个lua函数中被处理的。那么suspend又是从何而来呢?

《探索skynet(二):skynet如何启动一个服务》中我们提到过,当一个服务启动好之后,会设置其消息队列的回调函数为skynet.dispatch_message。在dispatch_message和其调用的raw_dispatch_message函数中,可以看到suspend和coroutine_resume函数的调用。

如果是对协程比较熟悉的程序员,应该能看出一点眉目了。

先看看suspend中对CALL和RETURN的处理吧:

skynet.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function suspend(co, result, command, param, size)
-- some other code
if command == "CALL" then
session_id_coroutine[param] = co
elseif command == "RETURN" then
local co_session = session_coroutine_id[co]
local co_address = session_coroutine_address[co]
if param == nil or session_response[co] then
error(debug.traceback(co))
end
session_response[co] = true
local ret
if not dead_service[co_address] then
ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, param, size) ~= nil
if not ret then
-- If the package is too large, returns nil. so we should report error back
c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
end
elseif size ~= nil then
c.trash(param, size)
ret = false
end
return suspend(co, coroutine_resume(co, ret))
-- some other code
end

可以看到对于CALL,就是简单的将param和co进行map存储,这里的param其实就是session id,co则是生成的coroutine。

而对于RETURN,则是取出coroutine对应的服务session id和地址,将对消息处理的结果返回给对应的源服务,然后接着suspend。

这里涉及到两个重要的函数coroutine_yield和coroutine_resume。
skynet的服务对于每一条msg,都会启动一个coroutine来处理。coroutine_yield交回控制权给另一个coroutine;coroutine_resume则是唤醒coroutine继续执行(从上次yield的地方)。

由于,suspend函数调用时,都会将coroutine_resume的结果传递进去,也就是说,一旦有coroutine_yield,那么就会从coroutine_resume的地方唤醒,从而进入suspend的流程。

那么,对于lua服务实现的一个消息处理函数来说,有两种可能:

第一种,比较简单,本地处理完消息,直接通过skynet.ret返回;
第二种,在本地处理消息的过程中,又有skynet.call这种远程调用,之后,才通过skynet.ret返回。

那接下来就看看这两种情况下,协程之间是如何合作的:

直接skynet.ret返回

假设服务A调用服务B的一个函数,那么服务B在处理这个消息时,通过skynet.dispatch_message和raw_dispatch_message,通过如下代码:

skynet.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- some other code
local f = p.dispatch
if f then
local ref = watching_service[source]
if ref then
watching_service[source] = ref + 1
else
watching_service[source] = 1
end
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)))
-- some other code

这样将lua的回掉函数包装成了一个coroutine对象co,并通过coroutine_resume将控制权交给co去执行。由于这里的回调函数简单,所以没多久,就直接遇到了skynet.ret语句。前面已经知道,skynet.ret语句实际上是coroutine_yield了一个RETURN消息。那么控制权回到服务B这里的suspend,对RETURN消息进行处理。处理结果我们前面也已经看到了,就是向服务A发送了一份RESPONSE消息,然后就又suspend了,并且恢复了co的执行。这次执行,co将yield EXIT(在co_create函数中),这时进行一些清理工作,这次消息处理就结束了。

skynet.call + skynet.ret

假设服务A调用服务B的一个函数,而服务B在处理这个消息时,先是向服务C发起了一次skynet.call,然后才进行skynet.ret。

这种情况下,仍然会先生成一个coroutine对象co,遇到skynet.call(yield CALL),那么co会交出执行权,有服务B的suspend处理CALL消息。这里对CALL的处理仅仅是记录了这个co,然后就结束了整个raw_dispatch_message,那么这个co将一直阻塞在yield CALL处(因为没有coroutine_resume来唤醒它)。

当服务C处理完服务B对其的调用时,会返回skynet.ret。根据之前的叙述,其实是服务C向服务B发了一条RESPONSE类型的消息。对于这种类型的消息,服务B有特殊处理:

skynet.lua
1
2
3
4
5
6
7
8
9
10
11
12
-- some other code
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
session_id_coroutine[session] = nil
suspend(co, coroutine_resume(co, true, msg, sz))
end
-- some other code

简单来说,这里就是根据session id,从之前存储的co对象中,取出了对应co,并且唤醒它。那么co将从skynet.call之后继续执行。之后,如果继续遇到skynet.call,则重复这一过程;如果遇到了skynet.ret,那么就走上一部分说的逻辑。总之,消息处理的整个流程就完全清楚了。

总结

经过探索,以及之前对消息队列机制的认识,这次彻底明白了skynet服务之间是如何进行通信的,尤其是skynet.call这种同步的、有返回值的通信过程。其实skynet也支持异步的服务间调用,道理也大同小异,有兴趣的读者可以自行阅读源代码~

推荐阅读:
探索skynet(一):从skynet_sample说起
探索skynet(二):skynet如何启动一个服务
探索skynet(三):消息队列

转载请注明出处: http://blog.guoyb.com/2017/03/26/skynet-4/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

评论