skynet有4类线程:Monitor线程 + Timer线程 + Socket线程 + Worker线程
其中Monitor线程负责的事情最为简单,监控好struct monitor,并定时执行检查函数即可,代码量比较少
Timer线程负责定时器,做的事情一般,检查struct timer,并根据到期的定时器的信息,向某个服务投递定时消息即可,代码量一般
Socket线程负责网络,做的事情最专业,负责管理socket,跟系统API打交道,代码比较多,功能非常关键
最后一个是Worker线程,负责执行不同的任务,这些任务都是不同的服务
现在的问题是:这些服务到底是什么?服务有几个类型?不同的服务又是如何实现的?
底层的服务类型一共也有4类:logger服务,harbor服务,gate服务,snlua服务
logger服务
我们首先看logger服务,也是最简单的实现,也是整个Skynet最先开启的服务
在skynet_satrt.c文件中,第279行,首次调用了 skynet_context_new(config->logservice, config->logger) 服务
(当然,这是在前面消息队列,服务管理,监控器,定时器,模块加载已经完成初始化之后的调用)
同时,这个 skynet_context_new() 也是服务启动的接口,在c层的代码就是直接调用: skynet_context_new()
让我们看看实现吧:
// 传入的参数 name = logger,而这个logger是默认写死在Skynet底层c代码的
// 存储在config->logseivice中,此值不能被配置,因为必须要有最基础的日志模块
// 重点区分 config->logseivice(字段名是:logseivice,值是:logger)
// config->logger(字段名是:logger,值是:用户配置的参数)
// 如果用户配置了logger,config->logger的值就是用户配置的日志文件路径,param就会有值
// 用户没有配置logger,param就是空
struct skynet_context *
skynet_context_new(const char * name, const char *param) {
struct skynet_module * mod = skynet_module_query(name); // 重点代码:查找模块 logger
// skynet_module_query(name)实际返回的是:一个指向 struct skynet_module地址 的指针
// 并且这个地址存在 modules.skynet_module[32]数组中的
// 详细查看:《Skynet源码之:模块加载》
if (mod == NULL)// 假如没找到,就返回NULL,这个logger服务找不到实现的动态库
return NULL;
// *****************************************************************************
void *inst = skynet_module_instance_create(mod); // 重点代码:创建模块实例 inst
// 重点来了:读取出了模块的句柄,开始使用模块内的函数了
// 在模块初始化的时候,这些函数都是已经加载好了的
// 模块句柄是mod,具体的函数是create,init,release,signal
// 这个时候就是直接调用 logger动态库里面的 logger_create函数了
// *****************************************************************************
// .... 代码省略
ctx->mod = mod; // 记录模块地段
ctx->instance = inst; // 记录实列的内存地址
// .... 代码省略
// *****************************************************************************
int r = skynet_module_instance_init(mod, inst, ctx, param); // 重点代码:初始化模块
// 这个时候就是直接调用 logger动态库里面的 logger_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;
}
// .... 代码省略
}
logger_create
1,首先看看skynet_module_instance_create(mod)的实现
void *
skynet_module_instance_create(struct skynet_module *m) {
if (m->create) {
return m->create(); // 这里实质就是调用logger动态库中的 logger_create()
} else {
return (void *)(intptr_t)(~0);
}
}
// 最后我们知道,m->create() 实际就是调用了 logger模块的skynet_module结构体中的 create 函数指针变量 指向的函数
// 即 logger模块中:logger_create()函数
打开service_logger.c文件,查看logger_create()函数的实现
struct logger *
logger_create(void) {
struct logger * inst = skynet_malloc(sizeof(*inst)); // 分配内存
inst->handle = NULL; // 记录这个块内存的handle
inst->close = 0; // 记录标准输入输出是否被关闭
inst->filename = NULL;
return inst;
}
// logger_create()函数的实现很简单
// 根据 struct logger 结构体,向系统申请了一块内存
// 并返回这块内存的地址给创建的人
看看这个struct logger 结构体是怎么样的
struct logger {
FILE * handle; // 一个文件句柄
char * filename; // 一个文件名
uint32_t starttime; // 开始时间
int close; // 开关标识
};
接着看看返回后做了什么
ctx->mod = mod; // 其实就是把模块地址,绑定在服务ctx的mod字段
ctx->instance = inst; // 把刚才logger_create函数申请的内存,绑定在服务ctx的instance字段,作为实列
logger_init
2,接着又进行了初始化操作:skynet_module_instance_init(mod, inst, ctx, param)
看看传入的参数是什么:
mod就是模块的句柄,inst就是实例的内存地址,ctx就是服务本身(即一个context结构体),para是一个参数(这里是logger的日志文件)
再看看实现:
int
skynet_module_instance_init(struct skynet_module *m, void * inst, struct skynet_context *ctx, const char * parm) {
return m->init(inst, ctx, parm);
}
这代表着又进入了logger_init的函数,同时传入的参数是:
inst就是实例的内存地址,ctx就是服务本身(即一个logger结构体),para是一个参数(这里是logger的日志文件)
// 同时给出前面的logger的结构体来进行对比
struct logger {
FILE * handle; // 一个文件句柄
char * filename; // 一个文件名
uint32_t starttime; // 开始时间
int close; // 开关标识
};
int
logger_init(struct logger * inst, struct skynet_context *ctx, const char * parm) {
// 这一步的实现是:把skynet的启动时间记录在logger的starttime字段
const char * r = skynet_command(ctx, "STARTTIME", NULL); // skynet_command的实现请看源码
inst->starttime = strtoul(r, NULL, 10);
if (parm) {
// 这里parm就是:用户配置的logger文件的路径
// 如果用户传入了parm参数,就根据这个目录,打开或新建出日志文件
inst->handle = fopen(parm,"a");
if (inst->handle == NULL) {
return 1;
}
inst->filename = skynet_malloc(strlen(parm)+1);
strcpy(inst->filename, parm); // 把日志的目录记录下来
inst->close = 1; // 标记 日志是否需要关闭
// 如果用户配置了logger文件的路径,则把 inst->close 标记为1
// 表明在 logger_release 时需要fclose(inst->handle)关闭
// 若是 标准输出文件,则不会设置 inst->close = 1,保持 inst->close = 0
} else {
inst->handle = stdout;//没有配置日志目录,就把打印输出到标准输出
}
if (inst->handle) {
skynet_callback(ctx, inst, logger_cb); // 设置此服务的回调函数,这个是最重要的步骤!!
return 0;
}
return 1;
}
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 logger 那块内存的地址,也就是实例内存
// 而 ctx->cb 记录的是 logger_cb
logger_cb
那么logger_cb是实现什么功能呢?
static int
logger_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source, const void * msg, size_t sz) {
struct logger * inst = ud; // 看!这个ud其实就是logger的实例,就是一开始create时候分配的内存
// 这里处理消息!
// 这里就是worker线程最终会执行到,读取消息后,真正执行消息回调的地方
switch (type) {
case PTYPE_SYSTEM:
if (inst->filename) {
inst->handle = freopen(inst->filename, "a", inst->handle);
// 这个可以看《Skynet源码之:守护进程》和《Skynet源码之:信号》
}
break;
case PTYPE_TEXT: // 这边就是一些logger的打印规则了,就是把发给logger的消息都打印出来
if (inst->filename) {
char tmp[SIZETIMEFMT];
int csec = timestring(ud, tmp);
fprintf(inst->handle, "%s.%02d ", tmp, csec);
}
fprintf(inst->handle, "[:%08x] ", source);
fwrite(msg, sz, 1, inst->handle);
fprintf(inst->handle, "\n");
fflush(inst->handle);
break;
}
return 0;
}
现在我们知道了:对于logger模块来说,logger_cb就是打印信息的实现
但是要注意的是:每个模块的call_back实现是不一样的
最后,可以看到:
// 服务ctx的cb函数,就是该服务模块的call_back函数
ctx->cb = logger_cb
logger_release
最后还有一个在服务退出后的释放操作,代码如下:
void
logger_release(struct logger * inst) {
if (inst->close) {
fclose(inst->handle); // 关闭刚才标记的日志文件句柄
}
skynet_free(inst->filename); // 释放日志文件
skynet_free(inst); // 是否日志实列
}
总结
现在我们知道:
其他服务会把消息放入本服务的次级消息队列中,并把本服务的次级消息队列放入全局消息队列进行排队
当worker线程从全局消息队列中,取到本服务的次级消息队列时,从里面读取一条信息,交给服务模块的ctx->cb函数进行处理
而调用也是非常简单:ctx->cb()即可,这样就会直接执行logger动态库中的logger_cb()函数了
service_logger服务可以说是最为简单的一个模块
因为logger_cb()函数的功能和实现最为简单,只在 c 层面的代码中执行就行
而对于snlua的call_back函数的时候,即如何绑定ctx->cb(),是个比较复杂的流程,放到 service_snlua的实现中再详细分析
详细见《Skynet源码之:service_snlua》
skynet 源码阅读笔记 —— 引导服务 bootstrap 的启动 - 简书 (jianshu.com)
skynet学习笔记 源码之lua消息回调注册过程_skynet lua loader error-CSDN博客
我的疑问
logger服务为什么不独立开一个线程来执行呢?
xzhovo/skynet-logger: 自定义的 Skynet 日志文件服务,实现按日期建文件夹,单个服务可享有独立日志文件 (github.com)