Skynet源码之:service_logger(16)

JavenLaw

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)