skynet 簡單的消息執行流程情景分析

skynet.lua對比以前優化了一些函數,尤其是對協程的控制,使得消息的處理流程更加清晰。我們現在來一步步剖析這個消息執行流程,加深對skynet reactor模式的理解以及協程的應用。

首先看服務的第一條消息是怎麼產生,又是如何被處理的。在創建一個snlua服務後第一條消息靠什麼來驅動呢?答案是靠自己(第一步還是得靠自己,然後別人纔有機會接觸你),看看下面的代碼可以清楚的看到:

int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
    int sz = strlen(args);
    char * tmp = skynet_malloc(sz);
    memcpy(tmp, args, sz);
    skynet_callback(ctx, l , launch_cb);
    const char * self = skynet_command(ctx, "REG", NULL);
    uint32_t handle_id = strtoul(self+1, NULL, 16);
    // it must be first message
    skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
    return 0;
}

static int launch_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) {
    assert(type == 0 && session == 0);
    struct snlua *l = ud;
    skynet_callback(context, NULL, NULL);
    int err = init_cb(l, context, msg, sz);
    if (err) {
        skynet_command(context, "EXIT", NULL);
    }
    return 0;
}

從上面看到執行第一條的回調函數launch_cb會調用init_cb,就是在這個函數裏開始加載解析lua源文件並執行。

一般lua源文件的主函數爲skynet.start(),他的內部實現爲:

function skynet.start(start_func)
    c.callback(skynet.dispatch_message)
    init_thread = skynet.timeout(0, function()
        skynet.init_service(start_func)
        init_thread = nil
    end)
end
function skynet.timeout(ti, func)
    local session = c.intcommand("TIMEOUT",ti)
    assert(session)
    local co = co_create(func)
    assert(session_id_coroutine[session] == nil)
    session_id_coroutine[session] = co
    return co   -- for debug
end

可以看到實際上這個函數也是由一條消息驅動的,原因這篇文章skynet答疑一 --skynet.start參數爲什麼要在定時器中執行已經講過,這裏就不再多說。而且這個函數裏重新設置了回調函數,回調函數第一次被調用是由於timeout超時消息。我們看看這條消息的類型:

int skynet_timeout(uint32_t handle, int time, int session) {
    if (time <= 0) {
        struct skynet_message message;
        message.source = 0;
        message.session = session;
        message.data = NULL;
        message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;

        if (skynet_context_push(handle, &message)) {
            return -1;
        }
    } else {
        struct timer_event event;
        event.handle = handle;
        event.session = session;
        timer_add(TI, &event, sizeof(event), time);
    }

    return session;
}

可以看到是PTYPE_RESPONSE(1)類型的。這個消息被調用時又該如何執行呢?我們還是看看相關代碼吧:

local function raw_dispatch_message(prototype, msg, sz, session, source)
    -- skynet.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
            local tag = session_coroutine_tracetag[co]
            if tag then c.trace(tag, "resume") end
            session_id_coroutine[session] = nil
            suspend(co, coroutine_resume(co, true, msg, sz))
        end
    else
        ......
    end
end

首先他會查找相關聯的協程,這個協程在skynet.timeout中創建,並且和session id相關聯。創建協程又是一個很難啃的函數:

local function co_create(f)
    local co = table.remove(coroutine_pool)
    if co == nil then
        co = coroutine_create(function(...)
            f(...)
            while true do
                local session = session_coroutine_id[co]
                if session and session ~= 0 then
                    local source = debug.getinfo(f,"S")
                    skynet.error(string.format("Maybe forgot response session %s from %s : %s:%d",
                        session,
                        skynet.address(session_coroutine_address[co]),
                        source.source, source.linedefined))
                end
                -- coroutine exit
                local tag = session_coroutine_tracetag[co]
                if tag ~= nil then
                    if tag then c.trace(tag, "end") end
                    session_coroutine_tracetag[co] = nil
                end
                local address = session_coroutine_address[co]
                if address then
                    session_coroutine_id[co] = nil
                    session_coroutine_address[co] = nil
                end

                -- recycle co into pool
                f = nil
                coroutine_pool[#coroutine_pool+1] = co
                -- recv new main function f
                f = coroutine_yield "SUSPEND"
                f(coroutine_yield())
            end
        end)
    else
        -- pass the main function f to coroutine, and restore running thread
        local running = running_thread
        coroutine_resume(co, f)
        running_thread = running
    end
    return co
end

他利用了協程池的思想。我們再看看raw_dispatch_message函數,找到了session id對應的協程,就開始coroutine_resume執行協程了。協程函數就是skynet.start()裏面的參數函數。

如果這個函數裏沒有阻塞的調用,就像下面一個簡單的函數:

skynet.start(
    function()
       print('test')
    end
)

那麼這個協程函數很快就執行完了。請看co_create的代碼和下面的圖:

 

 

接下來分析while循環,首先是查找協程對應的session id。目前我們並沒有把協程對應的session id記錄下來,只把session id對應的協程記錄了下來,所以這個爲空。接着是用於調試的一些分析,暫且跳過。

到了關鍵的地方,保存協程到協程池,然後該協程調用coroutine_yield "SUSPEND"掛起。回到主函數驅動協程的地方,將會調用suspend:

function suspend(co, result, command)
    if not result then
        local session = session_coroutine_id[co]
        if session then -- coroutine may fork by others (session is nil)
            local addr = session_coroutine_address[co]
            if session ~= 0 then
                -- only call response error
                local tag = session_coroutine_tracetag[co]
                if tag then c.trace(tag, "error") end
                c.send(addr, skynet.PTYPE_ERROR, session, "")
            end
            session_coroutine_id[co] = nil
            session_coroutine_address[co] = nil
            session_coroutine_tracetag[co] = nil
        end
        skynet.fork(function() end) -- trigger command "SUSPEND"
        error(debug.traceback(co,tostring(command)))
    end
    if command == "SUSPEND" then
        dispatch_wakeup()
        dispatch_error_queue()
    elseif command == "QUIT" then
        -- service exit
        return
    elseif command == "USER" then
        -- See skynet.coutine for detail
        error("Call skynet.coroutine.yield out of skynet.coroutine.resume\n" .. debug.traceback(co))
    elseif command == nil then
        -- debug trace
        return
    else
        error("Unknown command : " .. command .. "\n" .. debug.traceback(co))
    end
end

如果協程函數執行沒有錯誤,那麼第二個參數result爲true,第三個參數是協程掛起時傳入的參數,這裏爲"SUSPEND"。將會調用dispatch_wakeup和dispatch_error_queue,我們將有機會說到這兩個函數。

suspend調用結束,那麼消息回調函數執行完畢,一條消息的生命週期走完。

我們分析了一條很簡單,幾乎沒有什麼卵用,但是很完整的消息回調執行步驟。接下來試着分析稍微複雜點,會被阻塞,例如有call調用,假設一個服務A調用服務B:

skynet.start(
    function()
       print('in A service ')
       skynet.call('B', 'lua', 'cmd', ...)
    end
)

當調用call的時候會協程會阻塞:

function skynet.call(addr, typename, ...)
    local p = proto[typename]
    local session = c.send(addr, p.id , nil , p.pack(...))
    if session == nil then
        error("call to invalid address " .. skynet.address(addr))
    end
    return p.unpack(yield_call(addr, session))
end

我們看到通過底層的c調用,call首先會把消息發送給目的地,並且產生一個關聯的session id,然後調用yield_call將協程掛起:

local function yield_call(service, session)
    watching_session[session] = service
    session_id_coroutine[session] = running_thread
    local succ, msg, sz = coroutine_yield "SUSPEND"
    watching_session[session] = nil
    if not succ then
        error "call failed"
    end
    return msg,sz
end

在這個函數裏,將把session id對應的協程記錄下來,然後掛起協程。接下來的代碼分析和上面的一樣,此時A服務由timeout產生的消息將執行完畢了,但是這個函數的代碼卻還沒有執行完,第一次分析的時候還覺得有點不可思議。那麼什麼時候再來執行後面的代碼呢,我們慢慢道來。

假設B服務的主函數代碼也非常簡單,就和第一個例子一樣。第一條由timeout產生的消息執行完了,接下來收到了A服務發過來的消息,由於A是用lua協議發的,type是PTYPE_LUA(10),所以將執行raw_dispatch_message後面的部分:

local function raw_dispatch_message(prototype, msg, sz, session, source)
    -- skynet.PTYPE_RESPONSE = 1, read skynet.h
    if prototype == 1 then
        ......
    else
        local p = proto[prototype]
        if p == nil then
            ......
        end

        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
            trace_source[source] = nil
            if session ~= 0 then
                c.send(source, skynet.PTYPE_ERROR, session, "")
            else
                unknown_request(session, source, msg, sz, proto[prototype].name)
            end
        end
    end
end

我們可以看到B服務要有相應協議的dispatch函數纔有正確的執行,他的動作首先是調用co_create創建一個協程,注意此時將走co_create的後半部分。要明白其中的邏輯,剛開始有點繞,和我初學的時候一樣,下面的圖幫你理解:

 

先是將原先掛起的協程恢復,此時coroutine_yield返回,返回值爲新的協程函數,然後再次掛起,最後返回一個協程。這段代碼着實繞,短短的數幾十行代碼,協程掛起恢復了好幾次。此時協程仍然是掛起的,raw_dispatch_message創建協程(更準確的說是獲取協程池中的一個)之後,先將協程對應的session記錄下來,同時也將協程對應的源地址也記錄下來。然後將再次調用coroutine_resume恢復協程,再次對比上面的圖,f(coroutine_yield()),此時將執行p.dispatch(session,source, p.unpack(msg,sz))函數,也就是B服務註冊的相關協議的函數了。

當協議處理函數執行完畢後,注意此時協程函數仍然在執行,因爲co_create有個while循環。此時將判斷該協程對應的session id還在不在,什麼意思呢?原來A服務是call調用,上面提到B服務收到消息後執行回調函數時記錄了協程對應的session值(注意是send調用是沒有session值的)。那麼這個記錄什麼時候去掉呢?不要忘記了,我們必須對A服務進行回覆,A服務才能恢復。所以某個服務被call時一定要調用skynet.ret或skynet.response給對方發送一條消息,這樣才能清除session記錄,避免了一條警告,同時對方服務恢復執行。

接下來co_create就會掛起了,還是主線程supend接管。這樣A發送給B的一條消息回調就執行完畢了。下次B服務再有消息將會循環上面過程,周而復始。我們看到這種情況下多條消息只會用到一個協程池的一個協程而已。

再來說A服務。A服務掛起後,等待B服務調用skynet.ret回覆,在回覆中會帶上A給B消息的session,同時消息類型爲PTYPE_RESPONSE,這樣一來A服務執行其回調函數,和最上面的一樣,通過session找到其協程,然後恢復。

這篇文章簡單介紹了幾個基本消息的執行流程情景分析,下次再講講fork和sleep消息的執行流程。

 

歡迎加入QQ羣 858791125 討論skynet,遊戲後臺開發,lua腳本語言等問題。

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