skynet 源碼閱讀筆記 —— 如何在 lua 服務(wù)中啟動(dòng)另一個(gè) lua 服務(wù)

在上一篇文章中《skynet 源碼閱讀筆記 —— 引導(dǎo)服務(wù) bootstrap 的啟動(dòng)》,我們探討了 bootstrap 服務(wù)的啟動(dòng)細(xì)節(jié),其中 bootstrap 服務(wù)的核心在于 bootstrap.lua 腳本的執(zhí)行。而這篇博客會(huì)借助 bootstrap.lua 腳本中的部分內(nèi)容來說明如何在一個(gè) lua 服務(wù)內(nèi)啟動(dòng)其他的 lua 服務(wù)

--引用 skynet.lua 中的接口
local skynet = require "skynet"
local harbor = require "skynet.harbor"
require "skynet.manager"    -- import skynet.launch, ...

skynet.start(function()
    local standalone = skynet.getenv "standalone"

    local launcher = assert(skynet.launch("snlua","launcher"))
    skynet.name(".launcher", launcher)
    local harbor_id = tonumber(skynet.getenv "harbor" or 0)
    if harbor_id == 0 then
        assert(standalone ==  nil)
        standalone = true
        skynet.setenv("standalone", "true")

        local ok, slave = pcall(skynet.newservice, "cdummy")
        if not ok then
            skynet.abort()
        end
        skynet.name(".cslave", slave)

    else
        if standalone then
            if not pcall(skynet.newservice,"cmaster") then
                skynet.abort()
            end
        end

        local ok, slave = pcall(skynet.newservice, "cslave")
        if not ok then
            skynet.abort()
        end
        skynet.name(".cslave", slave)
    end

    if standalone then
        local datacenter = skynet.newservice "datacenterd"
        skynet.name("DATACENTER", datacenter)
    end
    skynet.newservice "service_mgr"
    pcall(skynet.newservice,skynet.getenv "start" or "main")
    skynet.exit()
end)

在上述代碼中,我們可以看到 bootstrap.lua 在文件的最開始處,執(zhí)行了 local skynet = require "skynet" 以及 require "skynet.manager", 這都是為了要在 bootstrap.lua 文件中,引用 skynet 為 lua 服務(wù)所設(shè)計(jì)的 api,對(duì)應(yīng)文件及 api 如下:

lualib/skynet.lua: skynet.startskynet.newservice
lualib/skynet/manager.lua: skynet.launchskynet.name

skynet.launch 及 skynet.name 的作用

對(duì)于 skynet.start函數(shù)我們放到后面討論,這里先分析 skynet.launch 以及 skynet.name 兩個(gè)函數(shù),這兩個(gè)函數(shù)定義如下:

--manager.lua
--bootstrap 中是這樣調(diào)用 skynet.launch 函數(shù)的:skynet.launch("snlua","launcher")
function skynet.launch(...)
    --相當(dāng)于執(zhí)行 c.comand("LAUNCH", "snlua laucher"),
    local addr = c.command("LAUNCH", table.concat({...}," "))
    if addr then
        return tonumber("0x" .. string.sub(addr , 2))
    end
end

這里簡(jiǎn)單地說明一下 c 的意義,c 是定義在 skynet.lua 中的一個(gè)變量,其中保存了一張表。這張表可以由函數(shù)luaopen_skynet_core 創(chuàng)建。在這張表中定義了一個(gè)命令接口 command,對(duì)應(yīng)的實(shí)現(xiàn)如下:

//lua-skynet.c
static int lcommand(lua_State *L) {
    struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
    const char * cmd = luaL_checkstring(L,1);
    const char * result;
    const char * parm = NULL;
    if (lua_gettop(L) == 2) {
        parm = luaL_checkstring(L,2);
    }
    result = skynet_command(context, cmd, parm);
    if (result) {
        lua_pushstring(L, result);
        return 1;
    }
    return 0;
}
//skynet_service.c
static const char * cmd_launch(struct skynet_context * context, const char * param) {
    size_t sz = strlen(param);
    char tmp[sz+1];
    strcpy(tmp,param);
    char * args = tmp;
    char * mod = strsep(&args, " \t\r\n");
    args = strsep(&args, "\r\n");
    struct skynet_context * inst = skynet_context_new(mod,args);
    if (inst == NULL) {
        return NULL;
    } else {
        id_to_hex(context->result, inst->handle);
        return context->result;
    }
}

lcommand 函數(shù)的主要工作便是將對(duì)應(yīng)的命令和參數(shù)轉(zhuǎn)發(fā)回 C 層的 cmd_launch 函數(shù)中,這個(gè)函數(shù)最終會(huì)創(chuàng)建一個(gè)新的 snlua 類型的 C 服務(wù) inst。而在創(chuàng)建這個(gè) snlua 服務(wù)的過程中也會(huì)對(duì)其進(jìn)行初始化,這個(gè)過程可見前一篇文章中所提到的 bootstrap 服務(wù)的創(chuàng)建及初始化,這里就不再贅述。

function skynet.name(name, handle)
    if not globalname(name, handle) then
        c.command("NAME", name .. " " .. skynet.address(handle))
    end
end

skynet.name 函數(shù)也會(huì)調(diào)用 c.command 接口來向?qū)?yīng)的服務(wù)發(fā)送命令,只不過這次發(fā)送的是 NAME 命令,并且最終會(huì)調(diào)用 cmd_name函數(shù)來為服務(wù)進(jìn)行命名。

如何在 lua 服務(wù)中創(chuàng)建一個(gè)新的 lua 服務(wù)

在說完上面兩個(gè) api 后,我們?cè)賮砜纯?skynet.newservice 的作用。skynet 在 lua 層一共有兩種不同的創(chuàng)建服務(wù)的方式:一種是 skynet.launch 創(chuàng)建用 C 編寫的服務(wù),而另一種方式則是調(diào)用 skynet.newservice 創(chuàng)建 lua 服務(wù)。以上述的 bootstrap 服務(wù)和 service_mgr 服務(wù)為例,創(chuàng)建 lua 服務(wù)的流程大致如下:

1.在 bootstrap 的 start_func 中執(zhí)行 skynet.newservice "service_mgr",此時(shí) bootstrap 服務(wù)陷入阻塞狀態(tài);
2.在 service_mgr 服務(wù)被創(chuàng)建出來以后,執(zhí)行 service_mgr.lua 這個(gè)腳本,在這個(gè)腳本中會(huì)執(zhí)行 skynet.start 函數(shù),表示 service_mgr 服務(wù)正式啟動(dòng),能夠正常地接收消息;
3.service_mgr 的 skynet.start 返回,bootstrap 服務(wù)的skynet.newservice函數(shù)返回,并獲得了 service_mgr 服務(wù)的句柄

了解了這個(gè)基本過程后,讓我們來看看 skynet.newservice 是如何定義的:

function skynet.newservice(name, ...)
    return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end

在上述代碼中,bootstrap服務(wù)的skynet.newservice向launcher服務(wù)發(fā)送了一條命令,并阻塞等待launcher的返回執(zhí)行結(jié)果。這條命令會(huì)傳遞到 launcher.lua中,并最終調(diào)用command.LAUNCH,進(jìn)而調(diào)用launch_service:

function command.LAUNCH(_, service, ...)
    launch_service(service, ...)
    return NORET
end
local function launch_service(service, ...)
    local param = table.concat({...}, " ")
    --創(chuàng)建一個(gè) lua 服務(wù)并獲得該服務(wù)的句柄
    local inst = skynet.launch(service, param)
    local session = skynet.context()
    --調(diào)用 skynet.response() 獲得一個(gè) response 閉包
    local response = skynet.response()
    if inst then
        --將服務(wù)句柄和服務(wù)的命令形式以鍵值對(duì)的形式保存
        services[inst] = service .. " " .. param
        --保存閉包,這個(gè) response 閉包最終會(huì)等 skynet.start 返回后再調(diào)用
        instance[inst] = response
        launch_session[inst] = session
    else
        response(false)
        return
    end
    return inst
end

在上述代碼中,launch_service 在創(chuàng)建 service_mgr 服務(wù)后會(huì)調(diào)用相應(yīng)的 service_mgr.lua 腳本。在對(duì)應(yīng)的腳本中有一個(gè) skynet.start 函數(shù),其對(duì)應(yīng)實(shí)現(xiàn)如下:

--skynet.lua
function skynet.start(start_func)
    --將對(duì)應(yīng)服務(wù)的回調(diào)函數(shù)設(shè)置為 skynet.dispatch_message
    c.callback(skynet.dispatch_message)
    --執(zhí)行服務(wù)腳本中傳入的 start_func 函數(shù)
    init_thread = skynet.timeout(0, function()
        skynet.init_service(start_func)
        init_thread = nil
    end)
end
function skynet.init_service(start)
    local ok, err = skynet.pcall(start)
    if not ok then
        skynet.error("init service failed: " .. tostring(err))
        skynet.send(".launcher","lua", "ERROR")
        skynet.exit()
    else
        skynet.send(".launcher","lua", "LAUNCHOK")
    end
end

在上一篇文章中,我們提到了 snlua 模塊在調(diào)用 launch_cb 函數(shù)時(shí)會(huì)執(zhí)行 skynet_callback(context, NULL, NULL); 將回調(diào)函數(shù)置為 NULL,而在 skynet.start 函數(shù)中才將對(duì)應(yīng)服務(wù)的回調(diào)函數(shù)置為 skynet.dispatch_message,然后調(diào)用 skynet.init_service(start_func)對(duì)服務(wù)進(jìn)行初始化。而 skynet.init_service(start_func) 則會(huì)調(diào)用 start_func 函數(shù)完成對(duì)服務(wù)真正意義上的初始化,并根據(jù)初始化的結(jié)果向 launcher 發(fā)送成功或失敗的消息。以下分別討論:

  • 當(dāng)初始化結(jié)果成功時(shí),服務(wù)會(huì)向 launcher 發(fā)送 LAUNCHOK 的命令,這會(huì)觸發(fā) comand.LAUNCHOK 的執(zhí)行,其中 command.LAUNCHOK 的定義如下:
--launcher.lua
function command.LAUNCHOK(address)
    -- init notice
    local response = instance[address]
    if response then
        response(true, address)
        instance[address] = nil
        launch_session[address] = nil
    end

    return NORET
end

從上述代碼中可以看出,在執(zhí)行初始化成功后,launcher會(huì)將之前調(diào)用 launch_service 時(shí)保存的閉包取出來執(zhí)行,傳入的第一個(gè)參數(shù)為 true 表示初始化成功。

  • 當(dāng)初始化結(jié)果失敗時(shí),服務(wù)會(huì)向 launcher 發(fā)送 ERROR 的命令。
--launcher.lua
function command.ERROR(address)
    -- see serivce-src/service_lua.c
    -- init failed
    local response = instance[address]
    if response then
        response(false)
        launch_session[address] = nil
        instance[address] = nil
    end
    services[address] = nil
    return NORET
end

與前面 command.LAUNCHOK類似,command.ERROR會(huì)取出對(duì)應(yīng)的 response 閉包并執(zhí)行,傳入?yún)?shù)為 false 表示初始化失敗。隨后當(dāng)skynet.send返回后,調(diào)用 skynet.exit 函數(shù)移除初始化失敗的服務(wù)。

--skynet.lua
function skynet.exit()
    fork_queue = {} -- no fork coroutine can be execute after skynet.exit
    skynet.send(".launcher","lua","REMOVE",skynet.self(), false)
    -- report the sources that call me
    for co, session in pairs(session_coroutine_id) do
        local address = session_coroutine_address[co]
        if session~=0 and address then
            c.send(address, skynet.PTYPE_ERROR, session, "")
        end
    end
    for resp in pairs(unresponse) do
        resp(false)
    end
    -- report the sources I call but haven't return
    local tmp = {}
    for session, address in pairs(watching_session) do
        tmp[address] = true
    end
    for address in pairs(tmp) do
        c.send(address, skynet.PTYPE_ERROR, 0, "")
    end
    c.command("EXIT")
    -- 退出服務(wù)后讓出處理機(jī)權(quán)限
    coroutine_yield "QUIT"
end
--launcher.lua
function command.REMOVE(_, handle, kill)
    services[handle] = nil
    local response = instance[handle]
    if response then
        -- instance is dead
        response(not kill)  -- return nil to caller of newservice, when kill == false
        instance[handle] = nil
        launch_session[handle] = nil
    end

    -- don't return (skynet.ret) because the handle may exit
    return NORET
end

在執(zhí)行 skynet.exit的過程中,會(huì)向 launcher 發(fā)送 REMOVE 命令,而這個(gè)命令最終會(huì)調(diào)用 command.REMOVE 函數(shù)。command.REMOVE會(huì)取出相應(yīng)閉包,并判斷該閉包是否已經(jīng)被執(zhí)行過。這代表了兩種情況:一種是因?yàn)槌跏蓟鲥e(cuò)而導(dǎo)致觸發(fā)了 command.ERROR,這個(gè)過程中執(zhí)行了 response 閉包;另一種就是服務(wù)自己調(diào)用了 skynet.exit() 自行退出,此時(shí) response 閉包還沒有被執(zhí)行過。

當(dāng) service_mgr 服務(wù)的skynet.start 函數(shù)返回后,bootstrap 服務(wù)也重新進(jìn)入運(yùn)行狀態(tài),繼續(xù)啟動(dòng)其他的服務(wù)(比如 main 服務(wù)),整體的過程與啟動(dòng) service_mgr 是相同的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容