skynet 源码阅读笔记 —— 如何在 lua 服务中启动另一个 lua 服务

在上一篇文章中《skynet 源码阅读笔记 —— 引导服务 bootstrap 的启动》,我们探讨了 bootstrap 服务的启动细节,其中 bootstrap 服务的核心在于 bootstrap.lua 脚本的执行。而这篇博客会借助 bootstrap.lua 脚本中的部分内容来说明如何在一个 lua 服务内启动其他的 lua 服务

--引用 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 在文件的最开始处,执行了 local skynet = require "skynet" 以及 require "skynet.manager", 这都是为了要在 bootstrap.lua 文件中,引用 skynet 为 lua 服务所设计的 api,对应文件及 api 如下:

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

skynet.launch 及 skynet.name 的作用

对于 skynet.start函数我们放到后面讨论,这里先分析 skynet.launch 以及 skynet.name 两个函数,这两个函数定义如下:

--manager.lua
--bootstrap 中是这样调用 skynet.launch 函数的:skynet.launch("snlua","launcher")
function skynet.launch(...)
    --相当于执行 c.comand("LAUNCH", "snlua laucher"),
    local addr = c.command("LAUNCH", table.concat({...}," "))
    if addr then
        return tonumber("0x" .. string.sub(addr , 2))
    end
end

这里简单地说明一下 c 的意义,c 是定义在 skynet.lua 中的一个变量,其中保存了一张表。这张表可以由函数luaopen_skynet_core 创建。在这张表中定义了一个命令接口 command,对应的实现如下:

//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 函数的主要工作便是将对应的命令和参数转发回 C 层的 cmd_launch 函数中,这个函数最终会创建一个新的 snlua 类型的 C 服务 inst。而在创建这个 snlua 服务的过程中也会对其进行初始化,这个过程可见前一篇文章中所提到的 bootstrap 服务的创建及初始化,这里就不再赘述。

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

skynet.name 函数也会调用 c.command 接口来向对应的服务发送命令,只不过这次发送的是 NAME 命令,并且最终会调用 cmd_name函数来为服务进行命名。

如何在 lua 服务中创建一个新的 lua 服务

在说完上面两个 api 后,我们再来看看 skynet.newservice 的作用。skynet 在 lua 层一共有两种不同的创建服务的方式:一种是 skynet.launch 创建用 C 编写的服务,而另一种方式则是调用 skynet.newservice 创建 lua 服务。以上述的 bootstrap 服务和 service_mgr 服务为例,创建 lua 服务的流程大致如下:

1.在 bootstrap 的 start_func 中执行 skynet.newservice "service_mgr",此时 bootstrap 服务陷入阻塞状态;
2.在 service_mgr 服务被创建出来以后,执行 service_mgr.lua 这个脚本,在这个脚本中会执行 skynet.start 函数,表示 service_mgr 服务正式启动,能够正常地接收消息;
3.service_mgr 的 skynet.start 返回,bootstrap 服务的skynet.newservice函数返回,并获得了 service_mgr 服务的句柄

了解了这个基本过程后,让我们来看看 skynet.newservice 是如何定义的:

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

在上述代码中,bootstrap服务的skynet.newservice向launcher服务发送了一条命令,并阻塞等待launcher的返回执行结果。这条命令会传递到 launcher.lua中,并最终调用command.LAUNCH,进而调用launch_service:

function command.LAUNCH(_, service, ...)
    launch_service(service, ...)
    return NORET
end
local function launch_service(service, ...)
    local param = table.concat({...}, " ")
    --创建一个 lua 服务并获得该服务的句柄
    local inst = skynet.launch(service, param)
    local session = skynet.context()
    --调用 skynet.response() 获得一个 response 闭包
    local response = skynet.response()
    if inst then
        --将服务句柄和服务的命令形式以键值对的形式保存
        services[inst] = service .. " " .. param
        --保存闭包,这个 response 闭包最终会等 skynet.start 返回后再调用
        instance[inst] = response
        launch_session[inst] = session
    else
        response(false)
        return
    end
    return inst
end

在上述代码中,launch_service 在创建 service_mgr 服务后会调用相应的 service_mgr.lua 脚本。在对应的脚本中有一个 skynet.start 函数,其对应实现如下:

--skynet.lua
function skynet.start(start_func)
    --将对应服务的回调函数设置为 skynet.dispatch_message
    c.callback(skynet.dispatch_message)
    --执行服务脚本中传入的 start_func 函数
    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 模块在调用 launch_cb 函数时会执行 skynet_callback(context, NULL, NULL); 将回调函数置为 NULL,而在 skynet.start 函数中才将对应服务的回调函数置为 skynet.dispatch_message,然后调用 skynet.init_service(start_func)对服务进行初始化。而 skynet.init_service(start_func) 则会调用 start_func 函数完成对服务真正意义上的初始化,并根据初始化的结果向 launcher 发送成功或失败的消息。以下分别讨论:

  • 当初始化结果成功时,服务会向 launcher 发送 LAUNCHOK 的命令,这会触发 comand.LAUNCHOK 的执行,其中 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

从上述代码中可以看出,在执行初始化成功后,launcher会将之前调用 launch_service 时保存的闭包取出来执行,传入的第一个参数为 true 表示初始化成功。

  • 当初始化结果失败时,服务会向 launcher 发送 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会取出对应的 response 闭包并执行,传入参数为 false 表示初始化失败。随后当skynet.send返回后,调用 skynet.exit 函数移除初始化失败的服务。

--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")
    -- 退出服务后让出处理机权限
    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

在执行 skynet.exit的过程中,会向 launcher 发送 REMOVE 命令,而这个命令最终会调用 command.REMOVE 函数。command.REMOVE会取出相应闭包,并判断该闭包是否已经被执行过。这代表了两种情况:一种是因为初始化出错而导致触发了 command.ERROR,这个过程中执行了 response 闭包;另一种就是服务自己调用了 skynet.exit() 自行退出,此时 response 闭包还没有被执行过。

当 service_mgr 服务的skynet.start 函数返回后,bootstrap 服务也重新进入运行状态,继续启动其他的服务(比如 main 服务),整体的过程与启动 service_mgr 是相同的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容