skynet.newservice简介:服务的启动

skynet是一个轻量级的游戏服务器框架。

简介

skynet的体系中,服务是一个基础概念。通常,我们使用skynet.newservice来启动一个snlua服务。
那么,当我们写下local addr = skynet.newservice("test")这行代码时,系统是怎么运作的呢?
思考一下这些问题:

  • 调用skynet.newservice会不会发生阻塞?
  • 如果test服务在skynet.start时调用了skynet.exitaddr会是什么值?
  • 如果test服务在skynet.start时出现错误,addr又会是什么值?
  • test服务是不是一定要调用skynet.start
  • 如果要传一些复杂的参数,又要怎么做?

skynet.newservice的代码实现

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

skynet.newservice的代码很简单,只调用了一个skynet.call,而skynet.call是阻塞的,所以skynet.newservice也是阻塞的。
这个call发了一条lua消息给.launcher服务,接下来看看.launcher服务相关的代码:

--skynet/service/launcher.lua
local function launch_service(service, ...)
    local param = table.concat({...}, " ")
    local inst = skynet.launch(service, param)
    local session = skynet.context()
    local response = skynet.response()
    if inst then
        services[inst] = service .. " " .. param
        instance[inst] = response
        launch_session[inst] = session
    else
        response(false)
        return
    end
    return inst
end

function command.LAUNCH(_, service, ...)
    launch_service(service, ...)
    return NORET
end

这里又调用到skynet.launch,实际上是发了个LAUNCH指令到底层,这里创建了一个snlua服务:

--skynet/lualib/skynet/manager.lua
function skynet.launch(...)
    local addr = c.command("LAUNCH", table.concat({...}," "))
    if addr then
        return tonumber(string.sub(addr , 2), 16)
    end
end

要注意前面的.launcher服务的command.LAUNCH函数是忽略返回的,所以此时skynet.newservice还处于阻塞状态,等待.launcher的返回。
那什么时候会返回响应呢?

回到前面的launch_service函数,可以看到skynet.launch成功后并没有直接返回,而是生成一个响应函数response,存储在表instance中。
搜索这个instance,我们可以在command.LAUNCHOK中找到它的使用:

--skynet/service/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服务收到LAUNCHOK的指令之后,才会返回给newservice的调用者。
问题又来了,什么时候发送LAUNCHOK呢?答案是在skynet.init_service中。

而调用skynet.init_service的,一共有三个函数:

  • skynet.start
  • skynet.forward_type
  • skynet.filter

所以,在服务的启动脚本中,我们必须调用这三个函数中的其中一个(通常都是skynet.start),否则的话,调用方永远都收不到返回的数据。
以在main服务中,创建新服务test为例,流程如下图所示:

skynet_service.png

新服务启动时,调用skynet.exit,调用者收到的addr是什么?

我们看一下skynet.exit:

--skynet/lualib/skynet.lua
function skynet.exit()
    fork_queue = { h = 1, t = 0 }   -- no fork coroutine can be execute after skynet.exit
    skynet.send(".launcher","lua","REMOVE",skynet.self(), false)

    --其他代码...
    --...
end

这里看到,新服务发送了REMOVE指令到.launcher服务,而.launcherREMOVE的处理如下:

--skynet/service/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

对于刚启动的服务来说,这里会调用到对应的responseresponse需要两个参数,这里第一个参数是true,第二个参数为nil,而第二个参数是返回地址,也就是说,调用者收到的addrnil值。

skynet_service_exit.png

新服务启动报错的话,又返回什么呢

新服务启动的时候,无论是用skynet.start还是skynet.forward_type,最终都是调用skynet.init_service,来看看代码:

--skynet/lualib/skynet.lua
function skynet.init_service(start)
    local function main()
        skynet_require.init_all()
        start()
    end
    local ok, err = xpcall(main, traceback)
    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

可以看到,对start函数的调用,是通过xpcall来调用的,如果报错的话,会发送ERROR.launcher服务。

--skynet/service/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

这里response参数是falseresponseskynet.response生成的一个函数,相关代码如下:

--skynet/lualib/skynet.lua
function skynet.response(pack)
    --其他代码...
    --...
    local function response(ok, ...)
        --其他代码...
        --...
        if ok then
            ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, pack(...))
            if ret == false then
                -- If the package is too large, returns false. so we should report error back
                c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
            end
        else
            ret = c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
        end
        --其他代码...
        --...
    end
    --其他代码...
    --...

    return response
end

可以看到,当传入的okfalse的时候,会发送一个PTYPE_ERROR类型的消息给调用者。
而当我们require"skynet"时,对PTYPE_ERROR默认的处理函数是_error_dispatch,具体的流程可以看看源码,这里简而言之,就是调用call的那条协程会触发一个call failerror

所以,当新服务的启动函数出错时,在新服务中会报错,中断,而调用者在skynet.call()中也会报call fail的错,从而中断执行,也就不会有addr的返回了。

skynet_service_error.png

如果启动服务要传比较复杂的参数,要怎么做比较好

skynet.newservice(service_name, ...)后面是可以带多个参数的,但这些参数只能是数字或字符串,回看前面的skynet.launch的代码,里面是调用了c.command("LAUNCH", table.concat({...}," ")),这里可以看到,传递的参数通过table.concat打包成字符串,以空格隔开。如果我们的参数中带有空格,或者我们想要传个table,那就不支持了。
通常来说,我们可以先启动服务,在skynet.start中做些简单的功能,调用skynet.dispatch("lua", ...)来处理lua消息,通过lua消息来做初始化,这样就能传送复杂的参数了:

local addr  = skynet.newservice("test")
skynet.send(addr, "lua", "start", {address='0.0.0.0',port=8888,nodelay=true})

总结

现在,我们可以回答最初的问题了:

  • 调用skynet.newservice会不会发生阻塞?

    • 会阻塞,如果服务没启动完,会一直等待下去。
  • 如果test服务在start时调用了exitaddr会是什么值?

    • nil
  • 如果test服务在start时出现错误,addr又会是什么值?

    • skynet.newservice会报错,没有返回值
  • test服务是不是一定要调用skynet.start

    • 不一定,也可以调用skynet.forward_typeskynet.filter
  • 如果要传一些复杂的参数,又要怎么做?

    • 将服务的创建和启动分开,创建后发送lua消息初始化服务。

最后再思考一个问题:启动系统的时候,第一个服务又是什么时候启动的呢?答案可以看看这里:skynet 之 main 服务的启动

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

推荐阅读更多精彩内容