Skynet服务器框架(四) Lua服务创建和启动剖析

前言:

之前从Skynet启动过程,解读了skynet的启动部分C语言编写的底层源码 Skynet服务器框架(二)C源码剖析启动流程,最后成功启动了引导的lua服务bootstrap.lua,接下来我们要尝试自定义一个lua服务,并让它启动起来。

bootstrap实现功能:

bootstrap.lua源码:

local skynet = require "skynet"
local harbor = require "skynet.harbor"
require "skynet.manager"    -- import skynet.launch, ...
local memory = require "memory"

skynet.start(function()
    local sharestring = tonumber(skynet.getenv "sharestring" or 4096)
    memory.ssexpand(sharestring)

    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"
    --根据Conifg中配置的start项启动下一个lua服务,假如无此项配置,则启动main.lua
    pcall(skynet.newservice,skynet.getenv "start" or "main")
    --退出bootstrap服务
    skynet.exit()
end)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

源码解析:

内容解析,在官方的文档Bootstrap已经做了详细的阐述:

这段脚本通常会:

  • 根据 standalone 配置项判断你启动的是一个 master 节点还是 slave 节点。如果是 master 节点还会进一步的通过 harbor 是否配置为 0 来判断你是否启动的是一个单节点 skynet 网络。 
    • 单节点模式下: 
      是不需要通过内置的 harbor 机制做节点间通讯的。但为了兼容(因为你还是有可能注册全局名字),需要启动一个叫做 cdummy 的服务,它负责拦截对外广播的全局名字变更
    • 多节点模式: 
      对于 master 节点,需要启动 cmaster 服务作节点调度用。此外,每个节点(包括 master 节点自己)都需要启动 cslave 服务,用于节点间的消息转发,以及同步全局名字。
  • 接下来在 master 节点上,还需要启动 DataCenter 服务
  • 然后,启动用于 UniqueService 管理的 service_mgr 。
  • 最后,它从 config 中读取 start 这个配置项,作为用户定义的服务启动入口脚本运行。成功后,把自己退出

这个 start 配置项,才是用户定义的启动脚本,默认值为 "main",对应main.lua脚本。

创建自定义lua服务:

在编写自定义的lua服务时,可以查询skynet lua开发的官方API

首先在test目录下(也可以选择其他目录,只要是config配置文件中 luaservice 项包含的目录即可)创建一个我们自己的lua脚本,这里我取名为firsttest.lua,表示自定义第一个服务的测试,简单功能是直接在回调函数中输出一段日志,然后按照以下步骤来实现代码:

  • 引入或者说是创建一个skynet服务:

    local skynet = require "skynet"
    • 1
  • 调用skynet.start接口,并定义传入回调函数:

    skynet.start(function()
    skynet.error("Server First Test")
    end)
    • 1
    • 2
    • 3

启动lua服务:

1.原理解析:

参考 bootstrap.lua 中的源码,我们得到在lua中启动一个服务的接口:

skynet.newservice(name, ...)
  • 1

它的定义在 lualib/skynet.lua 中,传入参数 name 是用来创建lua服务的 lua脚本名称,这些lua脚本存放的目录取决于config 配置文件中 luaservice 项的配置信息,例如这里我们配置信息为:

luaservice = root.."service.lua;"..root.."test.lua;"..root.."examples.lua"
  • 1

表示skynet通过 skynet.newservice 查询与 name 参数匹配的lua脚本会查找 service 、test 和 example 这三个目录下的 .lua 脚本。

2.实现步骤:

从bootstrap服务的解析,我们知道最后被启动的用户第一个lua服务是main.lua我们要让上面的自定的firsttest.lua运行启动,有两种办法:

  • 方法一:直接修改config配置文件中的start项为"firsttest"

    start = "firsttest"
    • 1
  • 方法二:在main.lua中通过skynet.newservice启动:

    skynet.newservice("firsttest")
    • 1

用上述的任何一个步骤完成配置之后,回到skynet根目录,运行skynet服务器启动指令,如下:

linsh@ubuntu:/application/skynet$ ./skynet examples/config
[:01000001] LAUNCH logger 
[:01000002] LAUNCH snlua bootstrap
[:01000003] LAUNCH snlua launcher
[:01000004] LAUNCH snlua cmaster
[:01000004] master listen socket 0.0.0.0:2013
[:01000005] LAUNCH snlua cslave
[:01000005] slave connect to master 127.0.0.1:2013
[:01000004] connect from 127.0.0.1:52686 4
[:01000006] LAUNCH harbor 1 16777221
[:01000004] Harbor 1 (fd=4) report 127.0.0.1:2526
[:01000005] Waiting for 0 harbors
[:01000005] Shakehand ready
[:01000007] LAUNCH snlua datacenterd
[:01000008] LAUNCH snlua service_mgr
[:01000009] LAUNCH snlua main
[:01000009] Server start
[:0100000a] LAUNCH snlua protoloader
[:0100000b] LAUNCH snlua console
[:0100000c] LAUNCH snlua firsttest
[:0100000c] Server First Test!
[:0100000d] LAUNCH snlua debug_console 8000
[:0100000d] Start debug console at 127.0.0.1:8000
[:0100000e] LAUNCH snlua simpledb
[:0100000f] LAUNCH snlua watchdog
[:01000010] LAUNCH snlua gate
[:01000010] Listen on 0.0.0.0:8888
[:01000009] Watchdog listen on 8888
[:01000009] KILL self
[:01000002] KILL self
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31


服务分类:

1.全局唯一服务:

全局唯一的服务等同于单例,即不管调用多少次创建接口,最后都只会创建一个此类型的服务实例且全局唯一。

  • 创建接口:

    skynet.uniqueservice(global,...) 
    • 1

    当参数 global=true 时,则表示此服务在所有节点之间是唯一的。

  • 查询接口: 
    假如不清楚当前创建了此全局服务没有,可以通过以下接口来查询:

    skynet.queryservice(global,...) 
    • 1

    global=true 时如果还没有创建过目标服务则一直等下去,直到目标服务被(其他服务触发而)创建。

2.普通服务:

每调用一次创建接口就会创建出一个对应的服务实例,可以同时创建成千上万个,用唯一的id来区分每个服务实例。使用的创建接口是:

skynet.newservice(name, ...)
  • 1

3.创建区别:

关于全局服务的创建,其实也跟普通服务一样,是用 skynet.call (异步变同步)的方式让 service_mgr 服务创建目标服务(类似于通知 launch 服创建服务一样)。区别在于创建全局服务时,假如 service_mgr 这边如已创建则直接返回服务地址;如没则创建;如正在创建则等结果。


skynet.newservice 源码剖析:

上面的操作我们已经知道如何调用此接口来创建和启动lua服务了,但是关于启动的具体实现过程需要深入剖析一下:

首先,在 lualib/skynet.lua 中找到了 skynet.newservice 接口定义的位置:

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

通过调用 skynet.call 接口,向 laucher 服务(源码是laucher.lua)发送一个 LAUNCH 消息,从而调用 local function launch_service(service, ...) 接口来实现的。也就是说,所有创建skynet服务(除了laucher 服务自身)的操作都由 laucher 服务统一实现和管理的。

1. skynet.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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

先在 proto 进程管理器字典中获取指定服务类型 typename 对应的进程:

local p = proto[typename]
  • 1

再通过 skynet.core 这个C模块的 send 功能,向指定服务发送数据:

local session = c.send(addr, p.id , nil , p.pack(...))
  • 1
  • addr 是服务的地址;
  • p 是处理 addr 对应服务的进程;
  • c 是一个C模块:local c = require "skynet.core" ,即 skynet.core 模块。

2. skynet.send 接口:

skynet.core 模块的源码在 lualib-src/lua-skynet.c 中,查看 luaopen_skynet_core 可以看到 { "send" , lsend },,即 skynet.core.send 其实就对应 lsend 方法:

//lua-skynet.c

/*
    uint32 address/string address
    integer type
    integer session
    string message
     lightuserdata message_ptr
     integer len
 */
static int lsend(lua_State *L) {
    struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
    //获取第一个形参(uint32或string)
    uint32_t dest = (uint32_t)lua_tointeger(L, 1);
    const char * dest_string = NULL;
    if (dest == 0) {
        if (lua_type(L,1) == LUA_TNUMBER) {
            return luaL_error(L, "Invalid service address 0");
        }
        dest_string = get_dest_string(L, 1);
    }
    //获取第二个形参
    int type = luaL_checkinteger(L, 2);
    int session = 0;
    if (lua_isnil(L,3)) {
        type |= PTYPE_TAG_ALLOCSESSION;
    } else {
        //获取第三个形参
        session = luaL_checkinteger(L,3);
    }
    //获取第四个形参
    int mtype = lua_type(L,4);
    switch (mtype) {
    case LUA_TSTRING: {
        size_t len = 0;
        void * msg = (void *)lua_tolstring(L,4,&len);
        if (len == 0) {
            msg = NULL;
        }
        if (dest_string) {
            session = skynet_sendname(context, 0, dest_string, type, session , msg, len);
        } else {
            session = skynet_send(context, 0, dest, type, session , msg, len);
        }
        break;
    }
    case LUA_TLIGHTUSERDATA: {
        void * msg = lua_touserdata(L,4);
        //获取第五个形参
        int size = luaL_checkinteger(L,5);
        if (dest_string) {
            session = skynet_sendname(context, 0, dest_string, type | PTYPE_TAG_DONTCOPY, session, msg, size);
        } else {
            session = skynet_send(context, 0, dest, type | PTYPE_TAG_DONTCOPY, session, msg, size);
        }
        break;
    }
    default:
        luaL_error(L, "skynet.send invalid param %s", lua_typename(L, lua_type(L,4)));
    }
    if (session < 0) {
        // send to invalid address
        // todo: maybe throw an error would be better
        return 0;
    }
    lua_pushinteger(L,session);
    return 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

当然,launch 服务最终还是调用的 skynet.launch("snlua","xxx") 来创建服务。


总结:

在skynet中的lua服务都是通过 laucher 服务(laucher.lua)来创建的(当然,除了 laucher 服务自身),而不是直接在调用 skynet.newservice 的服务上直接执行创建操作,这种实现方式的好处就是方便所有服务的跟踪和管理。

参考资料:

posted @ 2017-11-27 19:35  decode126  阅读(1829)  评论(0编辑  收藏  举报