Skynet源码之:service_snlua(17)

JavenLaw

根据前面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的实现