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的函数体比较长,其中重要的步骤包括:
- 根据参数实例化一个模块(这里是snlua)
- 创建此服务的消息队列
- 根据参数,初始化前面创建的模块(snlua)
在第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