skynet.call流程

本来想自己写下这个流程的,但是看到网上有人已经写了,就直接转过来吧,修正了原文中的一处错误。

原文:探索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
-- 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
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
-- 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)))
    ...
end

这样将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,然后这个co就挂起在了调用call的地方,直到对方返回一个Respone才会被唤醒(加粗部分是纠正原文错误的地方)。

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

--skynet.lua
-- 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
    ...
end

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

总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值