Introduction to Skynet


代码结构

首先要关注一下config.path文件。

skynet/example/config.path

root = "./"
luaservice = root.."service/?.lua;"..root.."test/?.lua;"..root.."examples/?.lua;"..root.."test/?/init.lua"
lualoader = root .. "lualib/loader.lua"
lua_path = root.."lualib/?.lua;"..root.."lualib/?/init.lua"
lua_cpath = root .. "luaclib/?.so"
snax = root.."examples/?.lua;"..root.."test/?.lua"

root代表的是skynet所在的目录。 luaservice代表服务所在的目录。 lualoader 用哪一段 lua 代码加载 lua 服务。通常配置为 lualib/loader.lua ,再由这段代码解析服务名称,进一步加载 lua 代码。不用去修改它。 lua_path和lua_cpath代表require要加载的文件所在的目录。 snax代表使用snax框架写的服务所在的目录。

例子分析

Skynet启动examples/config脚本,通过start=”main”开启了第一个Skynet的服务main.lua,这个文件也在examples中,我们看看这个文件写的什么:

local skynet = require "skynet"
local sprotoloader = require "sprotoloader"

local max_client = 64

skynet.start(function()
    skynet.error("Server start")
    skynet.uniqueservice("protoloader")
    if not skynet.getenv "daemon" then
        local console = skynet.newservice("console")
    end
    skynet.newservice("debug_console",8000)
    skynet.newservice("simpledb")
    local watchdog = skynet.newservice("watchdog")

    -- 向watchdog发送一个 lua类型的消息 cmd为start, 执行watchdog的CMD.start()
    skynet.call(watchdog, "lua", "start", {
        port = 8888,  -- 最多允许 1024 个外部连接同时建立
        maxclient = max_client, -- 最多允许 max_client 个外部连接同时建立
        nodelay = true, -- 给外部连接设置  TCP_NODELAY 属性
    })

    skynet.error("Watchdog listen on", 8888)
    skynet.exit()
end)

第一行先引用skynet这个库,这个是用lua写的,即lualib/skynet.lua。里面定义了这些接口:

skynet.start(): 用于”服务(service)“的入口函数,加载lua的service服务时会先运行这里的代码,它调用了luaclib-src/lua_skynet.c里面的callback(),最终调用Skynet的框架skynet_callback()来设置回调函数。

skynet.newservice(): 用于启动一个lua写的”服务(service)“,省略掉.lua后缀名。它调用了skynet.call(), 然后skynet.call()调用luaclib-src/lua_skynet.c里面的send(), 最终调用Skynet的框架的skynet_send()压入队列。skynet.call()用于发送一条消息给Skynet的框架。消息会压入队列,等待Skynet框架的调度。

skynet.exit(): 移除”服务(service)“,通过skynet.send()发送一条消息给Skynet框架来移除lua的这个”服务(service)“。

skynet.monitor(): 用于监视”服务(service)“,看它是否关闭。

main.lua一共打开了四个服务:

1、service_mgr这个是系统的模块,用于管理服务。

2、console这个是系统的模块,用于输出。

3、simpledb这个是例子的模块,用于管理Key–Value数据。

4、watchdog这个是例子的模块,用于监视socket端口,等待数据。

main.lua没有调用其它函数, 加载完服务, 它也就完成了任务, 所以它最后调用了skynet.exit()把自己杀掉了。

现在Skynet已经启动了watchdog服务,监听着8888端口,等待客户端的链接。

下面是watchdog服务的skynet_start()开始函数:

--- 一个通用模板 lualib/snax/gateserver.lua 来启动一个网关服务器,通过 TCP 连接和客户端交换数据
--- service/gate.lua 是一个实现完整的网关服务器,同时也可以作为 snax.gateserver 的使用范例


--- examples/watchdog.lua 是一个可以参考的例子,它启动了一个 service/gate.lua 服务,并将处理外部连接的消息转发处理

local skynet = require "skynet"

local CMD = {}
local SOCKET = {}
local gate
local agent = {}


-- 看门狗建立了agent并通过start指令将所有信息转交给它
function SOCKET.open(fd, addr)
    skynet.error("New client from : " .. addr)
    agent[fd] = skynet.newservice("agent") -- 为gate的连接创建agent
    skynet.call(agent[fd], "lua", "start", { gate = gate, client = fd, watchdog = skynet.self() }) --start agent
end

local function close_agent(fd)
    local a = agent[fd]
    agent[fd] = nil
    if a then
        skynet.call(gate, "lua", "kick", fd)
        -- disconnect never return
        skynet.send(a, "lua", "disconnect")
    end
end

function SOCKET.close(fd)
    print("socket close",fd)
    close_agent(fd)
end

function SOCKET.error(fd, msg)
    print("socket error",fd, msg)
    close_agent(fd)
end

function SOCKET.warning(fd, size)
    -- size K bytes havn't send out in fd
    print("socket warning", fd, size)
end

-- 在gate在收到数据时被传递触发
function SOCKET.data(fd, msg)
end

function CMD.start(conf)
    skynet.call(gate, "lua", "open" , conf) -- 在外部向你定义的gate服务发送启动消息,并传入启动配置(端口,最大连接数等)来启动gate服务。
end

function CMD.close(fd)
    close_agent(fd)
end

skynet.start(function()
    -- 调用skynet.dispatch函数注册      也可以调用skynet.register_protocol注册
    skynet.dispatch("lua",
        function(session, source, cmd, subcmd, ...)
            if cmd == "socket" then
                local f = SOCKET[subcmd]
                f(...)
                -- socket api don't need return
            else
                local f = assert(CMD[cmd])
                skynet.ret(skynet.pack(f(subcmd, ...))) -- subcmd is port, ... are maxclient nodelay
            end
        end
    )
    gate = skynet.newservice("gate")
end)

skynet.dispatch()这个服务的回调函数,通过SOCKET[]来调用函数,这些函数有:

SOCKET.open()打开agent服务并启动,使用gate来管理socket。SOCKET.close()关闭agent服务。

SOCKET.error()打印错误信息。

SOCKET.data()有数据到来。

下面就来看看agent服务的代码:

local skynet = require "skynet"
local netpack = require "skynet.netpack"
local socket = require "skynet.socket"
local sproto = require "sproto"
local sprotoloader = require "sprotoloader"

local WATCHDOG
local host
local send_request

local CMD = {}
local REQUEST = {}
local client_fd

function REQUEST:get()
    print("get", self.what)
    local r = skynet.call("SIMPLEDB", "lua", "get", self.what)
    return { result = r }
end

function REQUEST:set()
    print("set", self.what, self.value)
    local r = skynet.call("SIMPLEDB", "lua", "set", self.what, self.value)
end

function REQUEST:handshake()
    return { msg = "Welcome to skynet, I will send heartbeat every 5 sec." }
end

function REQUEST:quit()
    skynet.call(WATCHDOG, "lua", "close", client_fd)
end

local function request(name, args, response)
    local f = assert(REQUEST[name])
    local r = f(args)
    if response then
        return response(r)
    end
end

local function send_package(pack)
    local package = string.pack(">s2", pack)
    socket.write(client_fd, package)
end

skynet.register_protocol {
    name = "client",
    id = skynet.PTYPE_CLIENT,
    unpack = function (msg, sz)
        return host:dispatch(msg, sz)
    end,
    dispatch = function (_, _, type, ...)
        if type == "REQUEST" then
            local ok, result  = pcall(request, ...)
            if ok then
                if result then
                    send_package(result)
                end
            else
                skynet.error(result)
            end
        else
            assert(type == "RESPONSE")
            error "This example doesn't support request client"
        end
    end
}

function CMD.start(conf)
    local fd = conf.client
    local gate = conf.gate
    WATCHDOG = conf.watchdog
    -- slot 1,2 set at main.lua
    host = sprotoloader.load(1):host "package"
    send_request = host:attach(sprotoloader.load(2))
    skynet.fork(function()
        while true do
            send_package(send_request "heartbeat")
            skynet.sleep(500)
        end
    end)

    client_fd = fd
    skynet.call(gate, "lua", "forward", fd)
end

function CMD.disconnect()
    -- todo: do something before exit
    skynet.exit()
end

skynet.start(function()
    skynet.dispatch("lua", function(_,_, command, ...)
        local f = CMD[command]
        skynet.ret(skynet.pack(f(...)))
    end)
end)

前面watchdog调用SOCKET.open()的时候就调用了这里的CMD.start(),在客户端输出了”Welcome to skynet”。

Agent服务的核心就是注册了协议,并根据协议把数据发送给simpledb服务去处理:

最后我们看看simpledb服务:

local skynet = require "skynet"
require "skynet.manager"    -- import skynet.register
local db = {}

local command = {}

function command.GET(key)
    return db[key]
end

function command.SET(key, value)
    local last = db[key]
    db[key] = value
    return last
end

skynet.start(function()
    skynet.dispatch("lua", function(session, address, cmd, ...)
        cmd = cmd:upper()
        if cmd == "PING" then
            assert(session == 0)
            local str = (...)
            if #str > 20 then
                str = str:sub(1,20) .. "...(" .. #str .. ")"
            end
            skynet.error(string.format("%s ping %s", skynet.address(address), str))
            return
        end
        local f = command[cmd]
        if f then
            skynet.ret(skynet.pack(f(...)))
        else
            error(string.format("Unknown command %s", tostring(cmd)))
        end
    end)
    skynet.register "SIMPLEDB"
end)

simpledb服务只是很简单的处理了SET和GET。

以上只是大概浏览了一遍Skynet附带的例子,了解了一些Skynet提供给lua使用的接口,其他接口可以查看skynet.lua代码。

模块加载

skynet_module_init在skynet-main.c中被调用,传进来的path是在运行时config中配置的,如果config文件中没有配置cpath,默认将cpath的值设为./cservice/?.so,加载cpath目录下的so文件。

从get_api可以看出来,skynet要求模块的create/init/release/signal方法的命名是模块名加一个下划线,后面带create/init/release/signal。在skynet/service-src目录下有现成的例子,大家可以去看一下。

到这里,整个模块加载功能就分析完了。从启动流程来分析是,首先在config文件中配置一个cpath,它包含了你想要加载的so的路径。然后skynet-main.c在启动的时候会把cpath读出来,设进moduls->path中。在skynet-server.c中的skynet_context_new中会调用skynet_module_query,skynet_module_query首先会在列表中查询so是否已经加载,如果没有就直接加载它。

模块一定要包含有四个函数init/create/release/signal,它的命名格式为,假定模块名为xxx,那么就是xxx_create/xxx_init/xxx_release/xxx_signal。这四个函数是干嘛用的?

create做内存分配。init做初始化,它可能会做一些其它的事情,比如打开网络,打开文件,函数回调挂载等等。relase做资源回收,包括内存资源,文件资源,网络资源等等,signal是发信号,比如kill信号,告诉模块该停了。

#define MAX_MODULE_TYPE 32

//这里定义了模块列表数据结构
struct modules {
    int count;
    struct spinlock lock;
    const char * path;
    struct skynet_module m[MAX_MODULE_TYPE]; //最多只能加载32个模块
};

static struct modules * M = NULL;

//内部函数,打开一个动态库
static void *
_try_open(struct modules *m, const char * name) {
    const char *l;
    const char * path = m->path;
    size_t path_size = strlen(path);
    size_t name_size = strlen(name);

    int sz = path_size + name_size;
    //search path
    void * dl = NULL;
    char tmp[sz];
    //遍历路径查找so,路径以;分隔
    do
    {
        memset(tmp,0,sz);
        while (*path == ';') path++;
        if (*path == '\0') break;
        //取出路径名
        l = strchr(path, ';');
        if (l == NULL) l = path + strlen(path);
        int len = l - path;
        int i;
        //如果路径带有匹配字符 '?'
        for (i=0;path[i]!='?' && i < len ;i++) {
            tmp[i] = path[i];
        }
        memcpy(tmp+i,name,name_size);
        if (path[i] == '?') {
            strncpy(tmp+i+name_size,path+i+1,len - i - 1);
        } else {
            fprintf(stderr,"Invalid C service path\n");
            exit(1);
        }
        //dlope打开so
        dl = dlopen(tmp, RTLD_NOW | RTLD_GLOBAL);
        path = l;
    }while(dl == NULL);

    if (dl == NULL) {
        fprintf(stderr, "try open %s failed : %s\n",name,dlerror());
    }

    return dl;
}

//根据模块名在模块列表中查找
static struct skynet_module * 
_query(const char * name) {
    int i;
    for (i=0;i<M->count;i++) {
        if (strcmp(M->m[i].name,name)==0) {
            return &M->m[i];
        }
    }
    return NULL;
}

static void *
get_api(struct skynet_module *mod, const char *api_name) {
    size_t name_size = strlen(mod->name);
    size_t api_size = strlen(api_name);
    char tmp[name_size + api_size + 1];
        //将模块名附到tmp中
    memcpy(tmp, mod->name, name_size);
        //将方法名附到tmp中
    memcpy(tmp+name_size, api_name, api_size+1);
    char *ptr = strrchr(tmp, '.');
    if (ptr == NULL) {
        ptr = tmp;
    } else {
        ptr = ptr + 1;
    }
        // dlsym是一个系统函数,根据函数名字获取函数地址(指针)
    return dlsym(mod->module, ptr);
}

static int
open_sym(struct skynet_module *mod) {
    mod->create = get_api(mod, "_create");  //获取create方法
    mod->init = get_api(mod, "_init");  //获取init方法
    mod->release = get_api(mod, "_release");  //获取release方法
    mod->signal = get_api(mod, "_signal");  //获取signal方法

    return mod->init == NULL;  //然而这里只判定只要实现了init就可以了
}

//根据模块名查找模块
struct skynet_module * 
skynet_module_query(const char * name) {
        //先到列表里查
    struct skynet_module * result = _query(name);
    if (result)
        return result;

    SPIN_LOCK(M)

    result = _query(name); // double check
    //在列表里没查到
    if (result == NULL && M->count < MAX_MODULE_TYPE) {
        int index = M->count;
        //打开so
        void * dl = _try_open(M,name);
        if (dl) {
            M->m[index].name = name;
            M->m[index].module = dl;
            //获取so中的init/create/release/signal方法地址
            if (open_sym(&M->m[index]) == 0) {
                M->m[index].name = skynet_strdup(name);
                M->count ++;
                result = &M->m[index];
            }
        }
    }

    SPIN_UNLOCK(M)

    return result;
}

//添加模块到模块列表
void 
skynet_module_insert(struct skynet_module *mod) {
    SPIN_LOCK(M)

        //模块是不是已经在列表中了
    struct skynet_module * m = _query(mod->name);
    assert(m == NULL && M->count < MAX_MODULE_TYPE);
    int index = M->count;
    M->m[index] = *mod;
    ++M->count;

    SPIN_UNLOCK(M)
}

void * 
skynet_module_instance_create(struct skynet_module *m) {
    if (m->create) {
        return m->create(); //对应上文说的,调用模块的create函数
    } else {
        return (void *)(intptr_t)(~0);
    }
}

int
skynet_module_instance_init(struct skynet_module *m, void * inst, struct skynet_context *ctx, const char * parm) {
    return m->init(inst, ctx, parm); //对应上文说的,调用模块的init函数
}

void 
skynet_module_instance_release(struct skynet_module *m, void *inst) {
    if (m->release) {
        m->release(inst); //对应上文说的,调用模块的release函数
    }
}

void
skynet_module_instance_signal(struct skynet_module *m, void *inst, int signal) {
    if (m->signal) {
        m->signal(inst, signal); //对应上文说的,调用模块的release函数
    }
}

//初始化模块列表数据结构
void 
skynet_module_init(const char *path) {
    struct modules *m = skynet_malloc(sizeof(*m));
    m->count = 0;
    m->path = skynet_strdup(path);

    SPIN_INIT(m)

    M = m;
}

服务启动

先看一个skynet_sample中启动agent服务的例子:

local function new_agent()
    return skynet.newservice "agent"
end

skynet.newservice的实现也很简单:

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

这里,.launcher是一个服务的名字,后面都是通过skynet.call传给.launcher的参数。

先来看看这个.launcher是什么

local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher)

通过wiki我们知道,bootstrap.lua是skynet服务的启动入口,在这里,调用了skynet.launch,启动了一个launcher服务,并将其命名为.launcher。 skynet.launch的实现如下:

local c = require "skynet.core"
function skynet.launch(...)
    local addr = c.command("LAUNCH", table.concat({...}," "))
    if addr then
        return tonumber("0x" .. string.sub(addr , 2))
    end
end

这里的skynet.core是一个C语言模块,至此,我们将进入C语言实现部分,调用skynet.core.command(“LAUNCH”, “snlua launcher”)

C部分

skynet.core其实是在lua_skynet.c中定义的,其command对应于lcommand函数。 这时的参数其实都压进了lua_State中。

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;
}

在lcommand中,cmd应该是LAUNCH,parm应该是 snlua launcher。context暂时按下不表,来看看skynet_command的实现。

static struct command_func cmd_funcs[] = {
    { "TIMEOUT", cmd_timeout },
    ...
    { "LAUNCH", cmd_launch },
    ...
    { NULL, NULL },
};

const char * 
skynet_command(struct skynet_context * context, const char * cmd , const char * param) {
    struct command_func * method = &cmd_funcs[0];
    while(method->name) {
        if (strcmp(cmd, method->name) == 0) {
            return method->func(context, param);
        }
        ++method;
    }

    return NULL;
}

所以,这里会转发到cmd_launch函数。

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;
    }
}

在cmd_launch中,mod是snlua,args是“snlua launcher”,会根据这两个参数,重新构造一个skynet_context出来。

skynet_context_new的函数体比较长,其中重要的步骤包括:

在第1步中,加载模块(snlua)并调用了模块的create函数。

struct snlua *
snlua_create(void) {
    struct snlua * l = skynet_malloc(sizeof(*l));
    memset(l,0,sizeof(*l));
    l->mem_report = MEMORY_WARNING_REPORT;
    l->mem_limit = 0;
    l->L = lua_newstate(lalloc, l);
    return l;
}

这里,新创建了一个lua_State。因此,正如wiki中所说,snlua是lua的一个沙盒服务,保证了各个lua服务之间是隔离的。

而第3步,其实是调用了snlua模块的init函数。

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;
}

这里,设置了当前模块的callback为launch_cb,因此之后skynet_send消息,将由launch_cb处理。

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重置了服务的callback(因为snlua只用负责在沙盒中启动lua服务,其他消息应由lua程序处理),之后,调用了init_cb。

init_cb中除了设置各种路径、栈数据之外,和我们关心的lua程序有关的,是这样的一行:

const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");

int r = luaL_loadfile(L,loader);
if (r != LUA_OK) {
    skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1));
    report_launcher_error(ctx);
    return 1;
}
lua_pushlstring(L, args, sz);
r = lua_pcall(L,1,0,1);

这里,就又通过C语言的lua接口,调用回了lua层面。

总结一下,C语言层面的处理流程是这样的:

skynet.core.command–>lcommand–>skynet_command–>cmd_launch–>skynet_context_new–>snlua_create–>snlua_init–>loader.lua

回到lua

loader.lua的功能也很简单,就是在沙盒snlua中,加载并执行lua程序,这里也就是launcher.lua。

在launcher.lua中,通过skynet.register_protocol和skynet.dispatch,设置了launcher服务对各种消息的处理函数。而在skynet.start的调用中:

function skynet.start(start_func)
    c.callback(skynet.dispatch_message)
    skynet.timeout(0, function()
        skynet.init_service(start_func)
    end)
end

这里又重新设置了服务的callback。所以,所谓启动一个服务,其实就是将用lua编写的若干回调函数,挂载到对消息队列的处理中去。具体到这里的launcher服务,其实就是在lua层面,将command.LAUNCH等lua函数,挂接到消息队列中的LAUNCH等消息的回调函数。

最初的最初

作为创建服务的服务.launcher,它自己又是被谁创建的呢?前面我们看到,它是在bootstrap.lua中创建出来的。那么bootstrap.lua又是什么时候启动的呢?

这里,我们需要看一下skynet启动时的第一个函数入口,main函数。

main函数隐藏在skynet_main.c中,当其解析完传入的config文件之后,就转到了skynet_start。

在skynet_start函数中,调用了bootstrap(ctx, config->bootstrap),其中,就像前面讲到的流程一样,直接走到了skynet_context_new这一步。

一般默认,config->bootstrap项就是snlua bootstrap。那这里其实就是启动调用bootstrap.lua,,其中会启动launcher服务。有了launcher服务,之后的服务启动,就都可以交由launcher服务来进行了。

调试

接下来,你可以使用 nc 127.0.0.1 8000 接入调试控制台。正确接入的话,会看到

Welcome to skynet console

这行字。

如果你 list 的话,可以看到所有服务:

:01000004       snlua cmaster
:01000005       snlua cslave
:01000007       snlua datacenterd
:01000008       snlua service_mgr
:0100000a       snlua console
:0100000b       snlua debug_console 8000
:0100000c       snlua simpledb
:0100000d       snlua watchdog
:0100000e       snlua gate

我们可以用 simpledb 这个服务做实验。注意:目前仅限于调试同一进程内的服务。(这个限制是因为实现者特别懒)

输入 debug c 或 debug :0100000c 可以 attach 进 simpledb ,然后你会看到 :0100000c> 这样的提示符。

你可以输入 … 来检查当前消息是什么。通常你会看到这样的信息(表示当前是一个 timeout 消息)。因为这个时候 simpledb 在不停的调用 timer 保持和你的交互。

1       userdata: (nil) 0       226     0

当然,你也可以运行你想运行的任何 lua 代码。

调试器在这里只提供了一个叫 watch 的函数,让我们下一个条件断点,并跟踪运行它。

:0100000c>watch("lua", function(_,_,cmd) return cmd=="get" end)

这时,启动一下测试客户端,并输入 get hello 。

./3rd/lua/lua examples/client.lua

我们会看到,在输入 get hello 后,调试控制台的提示符会变成 ./examples/simpledb.lua(18) 表示停在了 simpledb.lua 的 18 行。接下来可以用 … 检查这个函数的参数。用 n 继续一行行运行,直到消息处理完毕。

:0100000c>./examples/simpledb.lua(18)>...
get hello
./examples/simpledb.lua(18)>n
./examples/simpledb.lua(19)>n
./examples/simpledb.lua(23)>n
:0100000c>

如果用 s 的话还会跟踪进入子函数内部。为了方便调试,调试器不会进入定义在 skynet.lua 的函数里(通常你不需要关心 skynet 本身的实现)。

另外,调试器还提供了一个叫 _CO 的变量,保存在正在调试的协程对象。如果你想使用 debug api ,这个变量可能有用。例如,可以用 debug.traceback(_CO) 查看调用栈:

:0100000c>watch "lua"
:0100000c>./examples/simpledb.lua(18)>_CO
thread: 0x7fe7f9811dc8
./examples/simpledb.lua(18)>debug.traceback(_CO)
stack traceback:
        ./examples/simpledb.lua:18: in upvalue 'dispatch'
        ./lualib/skynet/remotedebug.lua:150: in upvalue 'f'
        ./lualib/skynet.lua:111: in function <./lualib/skynet.lua:105>
./examples/simpledb.lua(18)>s
./examples/simpledb.lua(19)>s
./examples/simpledb.lua(10)>s
./examples/simpledb.lua(11)>debug.traceback(_CO)
stack traceback:
        ./examples/simpledb.lua:11: in local 'f'
        ./examples/simpledb.lua:20: in upvalue 'dispatch'
        ./lualib/skynet/remotedebug.lua:150: in upvalue 'f'
        ./lualib/skynet.lua:111: in function <./lualib/skynet.lua:105>
./examples/simpledb.lua(11)>c