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也支持異步的服務間調用,道理也大同小異,有興趣的讀者可以自行閱讀源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章