根据前面logger服务的知识,我们知道:
snlua服务也有create,init,cb,release一共4个接口
至于snlua模块的加载详细见《Skynet源码之:模块加载》
snlua的启动
1,首先看看 snlua 是在哪里启动的呢?
在 config 配置中,有 config.bootstrap = “snlua bootstrap” 作为参数传入
在 skynet_start.c 文件,第 285 行,首先开启了第一个服务 logger
在 skynet_start.c 文件,第 285 行,继续调用 bootstrap() 函数(注意不是 bootstrap.lua 服务)
bootstrap(ctx, config->bootstrap) 开启新的服务,参数是:
ctx是 skynet_context * logger,cmdline 是 snlua bootstrap(就是配置中的 config->bootstrap = “snlua bootstrap”)
代码如下:
// 由bootstrap函数,启动bootstrap服务
// 特别注意:通过bootstrap(logger, cmdline)传入的logger对bootstrap服务的启动没有任何关系
// 只是为了:bootstrap服务 万一在启动失败的时候,可以释放logger服务
static void
bootstrap(struct skynet_context * logger, const char * cmdline) {
// *********************************begin
int sz = strlen(cmdline);
char name[sz+1];
char args[sz+1];
int arg_pos;
sscanf(cmdline, "%s", name);
arg_pos = strlen(name);
if (arg_pos < sz) {
while(cmdline[arg_pos] == ' ') {
arg_pos++;
}
strncpy(args, cmdline + arg_pos, sz);
} else {
args[0] = '\0';
}
// *********************************end
// 上面这段代码就是把 字符串cmdline 解析并存储在args
// 重点代码看这里:name = "snlua",args = "bootstrap"
// 这里就是启动 snlua 服务,同时把 bootstrap 作为参数传给snlua服务
struct skynet_context *ctx = skynet_context_new(name, args);
if (ctx == NULL) {
skynet_error(NULL, "Bootstrap error : %s\n", cmdline);
// 如果启动失败,就需要把刚才启动的logger服务释放掉
skynet_context_dispatchall(logger);
exit(1);
}
}
2,接下来就是比较熟悉的服务启动的函数了
详细请看《Skynet源码之:服务实现》
// 传入的参数 name = "logger",args = "bootstrap"
// 如果用户配置了bootstrap,config->bootstrap最后解析出来的就是对应的 name = “value1" 和 args = “value2"
// 用户没有配置bootstrap,config->bootstrap最后解析出来的就是对应的 name = "logger" 和 args = "bootstrap"
// 但是一般都会配置为 bootstrap = "snlua bootstrap"
struct skynet_context *
skynet_context_new(const char * name, const char *param) {
struct skynet_module * mod = skynet_module_query(name); // 重点代码:查找模块 snlua
// 查找的过程,查看:模块加载的代码解析
// skynet_module_query(name)实际返回的是:一个指向 struct skynet_module地址 的指针
// 并且这个地址存在 modules.skynet_module[32]数组中的
// 详细看模块加载的代码解析
if (mod == NULL)// 假如没找到,就返回NULL,这个服务找不到实现的动态库
return NULL;
void *inst = skynet_module_instance_create(mod); // 重点代码:创建模块实例 inst
// 重点来了:读取出了模块的句柄,开始使用模块内的函数了
// 在模块初始化的时候,这些函数都是已经加载好了的
// 模块句柄是mod,具体的函数是create,init,release,signal
// .... 代码省略
ctx->mod = mod; // 记录模块地段
ctx->instance = inst; // 记录实列的内存地址
// .... 代码省略
int r = skynet_module_instance_init(mod, inst, ctx, param); // 重点代码:初始化模块
// 这个时候就是直接调用 snlua动态库里面的snlua_init函数了
if (r == 0) {
struct skynet_context * ret = skynet_context_release(ctx);
if (ret) {
ctx->init = true; // 把此服务的初始化标识设置为true
}
skynet_globalmq_push(queue);
if (ret) {
skynet_error(ret, "LAUNCH %s %s", name, param ? param : "");
}
return ret;
}
// .... 代码省略
}
snlua_create
1,首先看看skynet_module_instance_create(mod)的实现
void *
skynet_module_instance_create(struct skynet_module *m) {
if (m->create) {
return m->create(); // 这里实质就是调用snlua动态库中的 snlua_create()
} else {
return (void *)(intptr_t)(~0);
}
}
// 最后我们知道,m->create() 实际就是调用了 snlua模块的skynet_module结构体中的 create 函数指针变量 指向的函数
// 即 snlua模块中:snlua_create()函数
打开service_snlua.c文件,查看snlua_create()函数的实现
// 此段代码就是 snlua 结构体
struct snlua {
lua_State * L; // lalloc是个函数,用于检查服务的内存申请,见下面函数
struct skynet_context * ctx; // 服务本身的实例
size_t mem; // 记录服务占用的内存
size_t mem_report; // 设定单个服务内存占用的告警信息
size_t mem_limit; // 对单个服务的最大内存占用进行限制
lua_State * activeL; // 服务指向的虚拟机
ATOM_INT trap;
};
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);
l->activeL = NULL;
ATOM_INIT(&l->trap , 0);
return l;
}
// snlua_create()函数的实现很简单
// 根据 struct snlua 结构体,向系统申请了一块内存
// 并返回这块内存的地址给创建的人
// 此段代码用于检查、分配、限制单个服务,即单个lua虚拟机的内存
// ***********************************************************
static void *
lalloc(void * ud, void *ptr, size_t osize, size_t nsize) {
struct snlua *l = ud;
size_t mem = l->mem;
l->mem += nsize;
if (ptr)
l->mem -= osize;
if (l->mem_limit != 0 && l->mem > l->mem_limit) {
if (ptr == NULL || nsize > osize) {
l->mem = mem;
return NULL;
}
}
if (l->mem > l->mem_report) {
l->mem_report *= 2;
skynet_error(l->ctx, "Memory warning %.2f M", (float)l->mem / (1024 * 1024));
}
return skynet_lalloc(ptr, osize, nsize);
}
// ************************************************************
snlua_init
1,接着又进行了初始化操作:skynet_module_instance_init(mod, inst, ctx, param)
看看传入的参数是什么:
mod就是模块的句柄,inst就是实例的内存地址,ctx就是服务本身(即一个context结构体),para是一个参数(这里是需要启动的服务的脚本名称)
再看看实现:
int
skynet_module_instance_init(struct skynet_module *m, void * inst, struct skynet_context *ctx, const char * parm) {
return m->init(inst, ctx, parm);
}
这代表着又进入了snlua_init的函数,同时传入的参数是:
inst就是实例的内存地址,ctx就是服务本身(即一个snlua结构体),para是一个参数(这里是需要启动的服务的脚本名称)
// 同时给出前面的logger的结构体来进行对比
struct snlua {
lua_State * L; // lalloc是个函数,用于检查服务的内存申请,见下面函数
struct skynet_context * ctx; // 服务本身的实例
size_t mem; // 记录服务占用的内存
size_t mem_report; // 设定单个服务内存占用的告警信息
size_t mem_limit; // 对单个服务的最大内存占用进行限制
lua_State * activeL; // 服务指向的虚拟机
ATOM_INT trap;
};
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);
// 重点代码看这里:ctx的 cb 函数,被设置为 launch_cb
// // l就是inst
skynet_callback(ctx, l, launch_cb);
// 这一步的实现是:把服务ctx的handle转为无符号整数
const char * self = skynet_command(ctx, "REG", NULL); // skynet_command的实现请看源码
// strtoul 是一个标准库函数,用于将字符串转换为无符号长整数
// self+1 跳过字符串中的第一个字符(假设是 ":"),指向十六进制数字的第一个字符
// NULL 表示不需要返回指向未转换部分的指针
// 16 表示输入字符串是以基数16(即十六进制)进行解释的
//
// strtoul(self+1, NULL, 16) 将 self 字符串中的十六进制数转换为无符号整数
// 并存储在 handle_id 中
uint32_t handle_id = strtoul(self+1, NULL, 16);
// 为什么不直接使用 ctx->handle,而是要通过strtoul()转换一次?
// 因为 snlua_service.c 虽然引用了skynet.h头文件
// 但skynet.h头文件只声明了 struct skynet_context; 却并未定义
// struct skynet_context的定义是在 skynet_server.c中定义的
// 因此直接使用ctx->handle,是snlua会报错
// it must be first message
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY, 0, tmp, sz);
return 0;
}
// skynet_command(ctx, "REG", NULL)的说明
// REG 本质就是把 服务的handle注册在ctx的result字段
// 可以看看《Skynet源码之:服务实现》中的服务结构体
snlua_init()函数,首先把snlua这个的 cb 设置为 launch_cb() 函数
这意味着当snlua这个服务收到消息,并被worker线程处理的时候,消息的处理函数将会是 launch_cb()
2,我们回顾一下,worker线程是怎么处理snlua消息的
//
static void
dispatch_message(struct skynet_context *ctx, struct skynet_message *msg) {
assert(ctx->init);
CHECKCALLING_BEGIN(ctx)
pthread_setspecific(G_NODE.handle_key, (void *)(uintptr_t)(ctx->handle));
int type = msg->sz >> MESSAGE_TYPE_SHIFT;
size_t sz = msg->sz & MESSAGE_TYPE_MASK;
FILE *f = (FILE *)ATOM_LOAD(&ctx->logfile);
if (f) {
skynet_log_output(f, msg->source, type, msg->session, msg->data, sz);
}
++ctx->message_count;
int reserve_msg;
// *********************重要代码 begin**************************
if (ctx->profile) {
ctx->cpu_start = skynet_thread_time();
reserve_msg = ctx->cb(ctx, ctx->cb_ud, type, msg->session, msg->source, msg->data, sz);// 重点看这里
uint64_t cost_time = skynet_thread_time() - ctx->cpu_start;
ctx->cpu_cost += cost_time;
} else {
reserve_msg = ctx->cb(ctx, ctx->cb_ud, type, msg->session, msg->source, msg->data, sz);// 重点看这里
}
// *********************重要代码 end**************************
// 上面的2行代码是一样的
// 都是调用服务绑定的cb函数,并把消息的type,session,source,data,sz传给此cb函数
// 因此消息就由 snlua服务在init的 launch_cb 函数处理
if (!reserve_msg) {
skynet_free(msg->data);
}
CHECKCALLING_END(ctx)
}
这个时候snlua执行了一个操作:给自己发送了第一条信息,并且这个信息是不用回复的
// it must be first message
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY, 0, tmp, sz); // session 被置为0,所以不用回复
skynet_callback
先让我们看看skynet_callback(ctx, inst, logger_cb)做了什么事
void
skynet_callback(struct skynet_context * context, void *ud, skynet_cb cb) {
context->cb = cb;
context->cb_ud = ud;
}
// 可以看到,在前面记录了
ctx->mod = mod
ctx->instance = inst;
// 现在又记录
ctx->cb = cb
ctx->cb_ud = mod
// 实际 ctx->instance 和 ctx->cb_ud 记录都是 struct snlua 那块内存的地址,也就是实例内存
// 而 ctx->cb 记录的是 launch_cb
snlua_cb
那么snlua_cb是实现什么功能呢?
我们前面知道:snlua在初始化 init 的时候,就给自己发送了第一条信息
那么很快woker就会处理这条信息,并最终调用到服务snlua一开始绑定的cb函数:launch_cb
让我们来看看 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); // 第一条信息,首先把snlua服务的cb置为空
int err = init_cb(l, context, msg, sz); // 然后开始调用init_cb
if (err) {
skynet_command(context, "EXIT", NULL);
}
return 0;
}
这个操作就比较惊奇:
snlua服务在init的时候,把cb绑定为launch_cb,为什么在收到第一条信息之后又再次把cb置为null?
那snlua服务又是在什么时候重新绑定cb函数的呢?
原因分析:
snlua服务真正的cb,本来就应该绑定init_cb,而不是launch_cb
但是init_cb函数的操作比较多,并且需要启动lua虚拟机,在lua层需要执行某些操作,这些操作可能会阻塞
因此如果直接绑定 init_cb ,那么负责启动snlua的这个线程等待的时间将会比较漫长(调用skynet_context_new函数的时间很久)
所以snlua在init的时候,先绑定launch_cb,并自己给自己发一个信息,就立即返回了
当worker处理消息的时候,再由这个线程去绑定init_cb,并进行一些初始化
此时我们应该看看init_cb()函数的实现
static int
init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {
lua_State *L = l->L;
l->ctx = ctx;
lua_gc(L, LUA_GCSTOP, 0);
lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */
lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
luaL_openlibs(L);
luaL_requiref(L, "skynet.profile", init_profile, 0);
int profile_lib = lua_gettop(L);
// replace coroutine.resume / coroutine.wrap
lua_getglobal(L, "coroutine");
lua_getfield(L, profile_lib, "resume");
lua_setfield(L, -2, "resume");
lua_getfield(L, profile_lib, "wrap");
lua_setfield(L, -2, "wrap");
lua_settop(L, profile_lib-1);
lua_pushlightuserdata(L, ctx);
lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");
luaL_requiref(L, "skynet.codecache", codecache , 0);
lua_pop(L,1);
lua_gc(L, LUA_GCGEN, 0, 0);
const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");
lua_pushstring(L, path);
lua_setglobal(L, "LUA_PATH");
const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");
lua_pushstring(L, cpath);
lua_setglobal(L, "LUA_CPATH");
const char *service = optstring(ctx, "luaservice", "./service/?.lua");
lua_pushstring(L, service);
lua_setglobal(L, "LUA_SERVICE");
const char *preload = skynet_command(ctx, "GETENV", "preload");
lua_pushstring(L, preload);
lua_setglobal(L, "LUA_PRELOAD");
lua_pushcfunction(L, traceback);
assert(lua_gettop(L) == 1);
// *****************************************************重点代码
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);
// *****************************************************重点代码
if (r != LUA_OK) {
skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));
report_launcher_error(ctx);
return 1;
}
lua_settop(L,0);
if (lua_getfield(L, LUA_REGISTRYINDEX, "memlimit") == LUA_TNUMBER) {
size_t limit = lua_tointeger(L, -1);
l->mem_limit = limit;
skynet_error(ctx, "Set memory limit to %.2f M", (float)limit / (1024 * 1024));
lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, "memlimit");
}
lua_pop(L, 1);
lua_gc(L, LUA_GCRESTART, 0);
return 0;
}
上面的代码非常混乱,但那只是一些lua和c的交互操作,我们先不关心
我们关注重点代码:
const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
int r = luaL_loadfile(L,loader); // 加载loader.lua
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); // 这个args就是snlua在init时候,给自己发的第一条信息,args = "bootstrap"
r = lua_pcall(L,1,0,1); // 以 pcall 保护模式,使用loader.lua,加载 bootstrap 脚本
bootstrap
使用skynet的loader.lua,来记载 bootstrap 脚本,正式把代码从c层面转到lua层面
让我们看看 bootstrap 脚本的实现
skynet.start(function()
local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher)
-- 代码省略
pcall(skynet.newservice,skynet.getenv "start" or "main")
skynet.exit()
end)
skynet.start
bootstrap 脚本首先就是会执行skynet.start()函数
-- 由c层面的snlua中的 init_cb 函数,通过 snlua服务中的lua虚拟机:luaL_loadfile(L,loader)
-- 转移到lua层面的 skynet.start(start_func) 函数
-- 这里的skynet = require “skynet.lua",表明这是个lua文件了
function skynet.start(start_func)
c.callback(skynet.dispatch_message) -- 在这里重新绑定lua层的cb函数
init_thread = skynet.timeout(0, function()
skynet.init_service(start_func)
init_thread = nil
end)
end
接着我们看看所谓的c.callback是什么?为什么我们要把lua层的skynet.dispatch_message传递给c层呢?
local c = require "skynet.core"
// 这行代码又关涉到 lua层 到 c层 的调用
// 在项目文件lualib-src目录中,有lua-skynet.c文件
// 有函数:luaopen_skynet_core()
luaL_Reg l[] = {
{ "send" , lsend },
{ "genid", lgenid },
{ "redirect", lredirect },
{ "command" , lcommand },
{ "intcommand", lintcommand },
{ "addresscommand", laddresscommand },
{ "error", lerror },
{ "harbor", lharbor },
{ "callback", lcallback }, // 我们使用的c.callback,就是调用lcallback
{ "trace", ltrace },
{ NULL, NULL },
};
我们看看lua-skynet.c文件中lcallback函数的实现
// 此函数实现异常复杂,我们先不分析
// 直接看skynet_callback()函数
static int
lcallback(lua_State *L) {
struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
int forward = lua_toboolean(L, 2);
luaL_checktype(L,1,LUA_TFUNCTION);
lua_settop(L,1);
struct callback_context * cb_ctx = (struct callback_context *)lua_newuserdatauv(L, sizeof(*cb_ctx), 2);
cb_ctx->L = lua_newthread(L);
lua_pushcfunction(cb_ctx->L, traceback);
lua_setiuservalue(L, -2, 1);
lua_getfield(L, LUA_REGISTRYINDEX, "callback_context");
lua_setiuservalue(L, -2, 2);
lua_setfield(L, LUA_REGISTRYINDEX, "callback_context");
lua_xmove(L, cb_ctx->L, 1);
skynet_callback(context, cb_ctx, (forward)?(_forward_pre):(_cb_pre));
return 0;
}
第一,我们理清c.callback()的流程:c.callback() –> lcallback() –> skynet_callback()
也就是说:我们通过c.callback()函数,把lua层的skynet.dispatch_message函数,传给c层的lcallback()和skynet_callback()函数
最后由skynet_callback()函数把snlua的cb,重新绑定为 skynet.dispatch_message 函数
此后worker线程处理snlua服务的消息,就会调用skynet.dispatch_message函数来处理
第二,skynet.dispatch_message 函数的实现又是什么呢?
skynet.dispatch_message函数就是统一的信息分发接口
后面我们再详细解释
还有个疑问:
我们在调用skynet_context_new()函数时,调用了snlua_init函数,并给自己发了第一条信息
又有:在调用 snlua_init函数 之前,skynet_context_new()函数先一步建立了消息队列
因此:snlua_init函数,给自己发了第一条信息,是能放进snlua的消息队列的
特别注意:在snlua_init函数返回前,snlua的消息队列被没有并推送到 全局消息对列,所以并不会被worker执行
流程总结:建立snlua的消息队列 –> snlua_init函数开始 –> 给自己发第一条信息 –> 消息队列收到信息 –> snlua_init函数结束 –> 把消息队列放到全局
接着看skynet.start()代码
function skynet.start(start_func)
c.callback(skynet.dispatch_message) -- snlua会重新绑定cb函数,就是lua层传入的skynet.dispatch_message函数
init_thread = skynet.timeout(0, function() -- 给自己发消息,定时器的回调函数就是这个func
skynet.init_service(start_func)
init_thread = nil
end)
end
总的来说:snlua给自己发的第一条信息,解绑了launch_cb,导致了skynet.start()的执行(每个启动的服务都是一个脚本,加载脚本就会执行skynet.start() )
这也就是为什么我们在写skynet服务的时候,一定要有个skynet.start() 函数
在重新绑定了skynet.dispatch_message函数作为snlua服务的cb后,立马给自己设置一个定时器执行(其实也是给自己发消息)
最后定时器就会给snlua服务发一个消息,worker线程处理这个消息时,就会把它交给skynet.dispatch_message
skynet.dispatch_message会比较复杂,这里后面再详细说
最后skynet.dispatch_message会执行对应的函数(就是定时器设置的函数)
start_func
我们先来看看官方对于服务启动的解释
每个服务分三个运行阶段:
首先是服务加载阶段,当服务的源文件被加载时,就会按 lua 的运行规则被执行到。这个阶段不可以调用任何有可能阻塞住该服务的 skynet api 。因为,在这个阶段中,和服务配套的 skynet 设置并没有初始化完毕。
(这段话的意思是:调用任何name.lua服务脚本,lua脚本中能被执行的函数,例如skynet.start(),都不能是被阻塞的)
(像skynet.sleep()函数就是会被阻塞的,因此不能放入name.lua服务脚本中)
然后是服务初始化阶段,由 skynet.start 这个 api 注册的初始化函数执行。这个初始化函数理论上可以调用任何 skynet api 了,但启动该服务的 skynet.newservice 这个 api 会一直等待到初始化函数结束才会返回。
(这段话的意思是:skynet.start(start_func)中,用户注册的初始化函数start_func,此时可以执行了,也可以执行任意的skynet api)
(但是skynet.newservice却要在start_func执行完毕之后,才会返回)
(所以我们在skynet.newservice启动一个服务时,一开始尽量不要调skynet.sleep()这样的阻塞函数)
最后是服务工作阶段,当你在初始化阶段注册了消息处理函数的话,只要有消息输入,就会触发注册的消息处理函数。这些消息都是 skynet 内部消息,外部的网络数据,定时器也会通过内部消息的形式表达出来。
我们看看用户注册的初始化函数,是如何被执行的
这里的start就是 bootstrap 中传给 skynet.start() 的函数
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
回顾一下bootstrap 的代码
skynet.start(function() -- 这个function将会被传递给skynet.init_service(start)
local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher)
-- 代码省略
pcall(skynet.newservice,skynet.getenv "start" or "main")
skynet.exit()
end)
看看 bootstrap 实际做的事情
第一,启动另一个服务snlua,参数为launcher。实际就是跟 bootstrap 一样,把snlua绑定为launcher,成为launcher服务
launcher服务启动之后,所有需要启动的服务,都通过发消息给launcher
第二,最后还启动了由用户配置,需要第一启动的用户服务,start 或者 main
这个时候就是通过skynet.newservice接口启动的服务
我们可以看看skynet.launch(“snlua”,“launcher”) 是如何在lua层启动launcher服务的?
本质还是在c层面是通过 skynet_context_new(name, args)
function skynet.launch(...)
local addr = c.command("LAUNCH", table.concat({...}," ")) -- 通过lua-c的接口,给c层发送LAUNCH命令,从而启动服务
if addr then
return tonumber(string.sub(addr , 2), 16)
end
end
最终调用到c层的代码
// lua层命令 c.command函数 ---> c层命令 lcommand函数
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); // 重点代码: skynet_command命令查找 LAUNCH
if (result) {
lua_pushstring(L, result);
return 1;
}
return 0;
}
// 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;
}
}
又因为每次调用skynet.launch(“snlua”,“launcher”) 太麻烦了,所以封装了一个接口skynet.newservice
function skynet.newservice(name, ...)
return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end
这个就是最简单的操作了:直接给launcher服务发消息,命令是LAUNCH,服务是snlua,参数name就是用户需要启动的服务脚本
相关API总结
skynet.newservice
skynet.start
skynet.init
skynet.launch
skynet.pcall:skynet.init by hongling0 · Pull Request #1322 · cloudwu/skynet (github.com)
skynet.require
这部分需要独立开一个章节来了解,特别是:skynet.init 和 skynet.start 的执行顺序和影响
start(func) 用 func 函数初始化服务,并将消息处理函数注册到 C 层,让该服务可以工作。
init(func) 若服务尚未初始化完成,则注册一个函数等服务初始化阶段再执行;若服务已经初始化完成,则立刻运行该函数。
dispatch_message
我们来看看最重要的skynet.dispatch_message的实现
function skynet.dispatch_message(...)
local succ, err = pcall(raw_dispatch_message,...)
-- 代码省略
end
-- 最重要的实现函数
-- 直接看代码的注释,由注释进行分析
local function raw_dispatch_message(prototype, msg, sz, session, source)
-- skynet.PTYPE_RESPONSE = 1, read skynet.h
-- 首先判断消息的类型
-- 类型1表示此消息是 回复消息。如果是回复消息,那必然之前就有记录
-- 举例:定时器消息
-- 在定时器的实现中,调用skynet.timeout(ti,cb)会返回一个协程co
-- 这个协程co跟请求消息的session是一个对应关系
-- session_id_coroutine[session] = co
--
-- 具体看Skynet定时器的实现,以及skynet.timeout(ti,cb)的代码
-- 还有定时器在发送定时消息的时候,是如何设置消息的 type 的
--
-- 此时我们根据消息的session,就能找到当时的co
-- 从而执行当时的skynet.timeout(ti,cb)中的cb
if prototype == 1 then
local co = session_id_coroutine[session]
if co == "BREAK" then
session_id_coroutine[session] = nil
elseif co == nil then
unknown_response(session, source, msg, sz)
else
local tag = session_coroutine_tracetag[co]
if tag then c.trace(tag, "resume") end
session_id_coroutine[session] = nil
suspend(co, coroutine_resume(co, true, msg, sz, session))
end
else
-- 当消息类型为 请求消息 时,会比较复杂一些
-- 第一,我们知道snlua服务在init_cb函数中(c层面的代码,在service_snlua.c文件)通过loader.lua文件
-- 加载了脚本文件,例如 bootstrap,launcher等,这些脚本就代表着一个lua服务的启动
-- 而每个脚本中都要加载 local skynet = require "skynet"
-- 第二,正因为加载了skynet.lua,脚本文件才能执行skynet.start()
-- 但同时也执行了skynet.lua的代码,请看代码段1
-- 即把 请求消息 分为 不同类型,并进行注册
--
-- 于是我们能够通过 local p = proto[prototype] 找到消息对应的处理模块
-- 例如:
-- 请求消息的类型是 lua,对应的就是p1
-- 请求消息的类型是 text,对应的就是p2
local p = proto[prototype]
if p == nil then
if prototype == skynet.PTYPE_TRACE then
-- trace next request
trace_source[source] = c.tostring(msg,sz)
elseif session ~= 0 then
c.send(source, skynet.PTYPE_ERROR, session, "")
else
unknown_request(session, source, msg, sz, prototype)
end
return
end
-- 我们在 p 中,得到对应的 消息处理函数p.dispatch
-- 那么消息处理函数p.dispatch又是如何设置的呢?
-- 第一,在注册这个类型的消息时,就同时注册p.dispatch
-- 第二,如果注册这个类型的消息时没有指定p.dispatch,那么应该在开启此服务前,通过skynet.dispatch完成初始化
-- 请看代码段2
local f = p.dispatch
if f then
local co = co_create(f)
session_coroutine_id[co] = session
session_coroutine_address[co] = source
local traceflag = p.trace
if traceflag == false then
-- force off
trace_source[source] = nil
session_coroutine_tracetag[co] = false
else
local tag = trace_source[source]
if tag then
trace_source[source] = nil
c.trace(tag, "request")
session_coroutine_tracetag[co] = tag
elseif traceflag then
-- set running_thread for trace
running_thread = co
skynet.trace()
end
end
suspend(co, coroutine_resume(co, session,source, p.unpack(msg,sz)))
else
trace_source[source] = nil
if session ~= 0 then
c.send(source, skynet.PTYPE_ERROR, session, "")
else
unknown_request(session, source, msg, sz, proto[prototype].name)
end
end
end
end
代码段1:
----- register protocol
do
local REG = skynet.register_protocol
REG {
name = "lua",
id = skynet.PTYPE_LUA,
pack = skynet.pack,
unpack = skynet.unpack,
}
REG {
name = "response",
id = skynet.PTYPE_RESPONSE,
}
REG {
name = "error",
id = skynet.PTYPE_ERROR,
unpack = function(...) return ... end,
dispatch = _error_dispatch,
}
end
代码段2:
function skynet.dispatch(typename, func)
local p = proto[typename] -- 找到该类型消息的结构
if func then
local ret = p.dispatch
p.dispatch = func -- 把func绑定在 该类型消息的结构 的dispatch
return ret
else
return p and p.dispatch -- 返回默认的dispatch
end
end
这里分2种情况:
第一种
skynet.lua本身就注册了3种类型的消息:“lua”,“response”,“error” (看代码段1)
“lua"类型,并没有设置dispatch函数,只有name,id,pack和unpack。为什么没有设置dispatch函数呢?
“response"类型,也没有设置dispatch函数,只有name,id。这是可以理解的,因为"response"类型,代表回复消息,自己不用再回复别人了
“error"类型最齐全,设置dispatch函数,也有name,id。
为什么skynet.lua不帮我们设置dispatch函数呢?
这个是因为skynet一般都用lua消息为类型,所以处理lua消息的函数,应该是由用户自己定义,用来处理业务逻辑,代码如下:
skynet.start(function()
skynet.dispatch("lua", function (_, _, id)
-- do something
end)
end)
可以看到,在启动初始化服务的时候,就是重新注册lua类型的dispatch函数,从而把消息转到自己的业务逻辑上去
第二种
假设你自己需要注册其他类型的消息,例如:“myself”
-- 第一步你需要注册消息
skynet.register_protocol {
name = "myself",
id = skynet.PTYPE_SELF, -- 注意这个id的使用,要根据定义有序增长
unpack = skynet.tostring, -- 定义消息解包函数
pack = skynet.tostring,-- 定义消息压包函数
dispatch = function(_, address, msg) -- 定义消息处理函数
print(string.format(":%08x(%.2f): %s", address, skynet.time(), msg))
end
}
-- 启动服务即可
skynet.start(function() end)
或者
skynet.register_protocol {
name = "myself",
id = skynet.PTYPE_SELF, -- 注意这个id的使用,要根据定义有序增长
unpack = skynet.tostring, -- 定义消息解包函数
pack = skynet.tostring,-- 定义消息压包函数
-- 注意:这里不注册 dispatch 函数
}
-- 那么在启动服务时
-- 需要自己再注册 dispatch 函数
skynet.start(function()
skynet.dispatch("lua", function (_, address, msg)
print(string.format(":%08x(%.2f): %s", address, skynet.time(), msg))
end)
end)
service_snlua最重要的代码已经讲解完了
剩余的部分不是不重要,而是比较繁琐
主要侧重于c和lua的数据交互、skynet各类API的实现