Skynet源码之:模块加载(8)

JavenLaw

skynet_module结构体

1,让我们看看首先在哪里调用的skynet_module_init(config->module_path)

在skynet_start.c文件中,第274行,skynet_module_init(config->module_path)

void 
skynet_module_init(const char *path) {
	struct modules *m = skynet_malloc(sizeof(*m)); // 分配内存
	m->count = 0;
	m->path = skynet_strdup(path); // 实际就是把 config->path 复制给 m->path
	
	SPIN_INIT(m)
	
	M = m;
}

static struct modules * M = NULL; // 声明的全局单例M

看看 modules 结构

struct modules {
	int count; // 模块的数量
	struct spinlock lock; // 自旋锁
	const char * path; // 模块的路径
	struct skynet_module m[MAX_MODULE_TYPE]; // 模块结构数量 MAX_MODULE_TYPE == 32
};

再看看 skynet_module 结构体

typedef void * (*skynet_dl_create)(void);
typedef int (*skynet_dl_init)(void * inst, struct skynet_context *, const char * parm);
typedef void (*skynet_dl_release)(void * inst);
typedef void (*skynet_dl_signal)(void * inst, int signal);

struct skynet_module {
	const char * name; // 模块名称
	void * module; // 指向模块的指针
	skynet_dl_create create; // create函数指针变量
	skynet_dl_init init; // init函数指针变量
	skynet_dl_release release; // release函数指针变量
	skynet_dl_signal signal; // signal函数指针变量
};


如何链接动态库

那这些skynet_dl_xxx又是什么呢?

typedef void * (*skynet_dl_create)(void) 定义了一个名为 skynet_dl_create 的函数指针类型,该函数指针可以指向一个没有参数并返回void指针的函数

这种类型定义常用于动态链接库(简称 DLL)或共享库(Shared Library)中,用于定义函数指针类型,以便在运行时动态加载和调用库中的函数

让我们先来复习一下函数指针

函数指针是指向函数的指针。在C语言中,函数被编译后在内存中占据一段连续的地址空间,函数指针就是用来存储这个地址的指针

函数指针的声明方式与函数的原型相似,但需要在函数名前面加上 * 表示它是一个指针。例如,下面是一个函数指针的声明示例:

int (*func_ptr)(int, int);

上述声明表示 func_ptr 是一个 指向 接受两个 int 类型参数,并返回 int 类型值的函数 的指针

通过函数指针,我们可以 将函数作为参数传递给其他函数,或者将 函数指针赋值给其他函数指针变量,以便在程序运行时动态地调用不同的函数

这种灵活性使得函数指针在实现回调函数、事件处理和动态函数调用等方面非常有用

下面是一个简单的示例,演示了函数指针的用法:

#include <stdio.h>

int add(int a, int b) {
 return a + b;
}

int sub(int a, int b) {
 return a - b;
}

int main() {
 int (*func_ptr)(int, int);

 func_ptr = add;
 printf("Add: %d\n", func_ptr(5, 3));

 func_ptr = sub;
 printf("Sub: %d\n", func_ptr(5, 3));

 return 0;
}

在上述示例中,我们声明了一个函数指针 func_ptr,并将它分别指向 add 和 sub 函数

通过调用函数指针,我们可以动态地选择调用哪个函数,并得到相应的结果

现在我们可以再来看看上面的代码:

// 声明了一个名为 skynet_dl_create 的函数指针
// 该函数指针 指向一个[不接受任何参数],且返回[void指针] 的函数
typedef void * (*skynet_dl_create)(void);

// 声明了一个名为 skynet_dl_init 的函数指针
// 该函数指针 指向一个[接收void指针,skynet_context结构体指针,const字符指针],且返回[int型] 的函数
typedef int (*skynet_dl_init)(void * inst, struct skynet_context *, const char * parm);

// 声明了一个名为 skynet_dl_release 的函数指针
// 该函数指针 指向一个[接收void指针],且返回[void] 的函数
typedef void (*skynet_dl_release)(void * inst);

// 声明了一个名为 skynet_dl_signal 的函数指针
// 该函数指针 指向一个[接收void指针,int值],且返回[void] 的函数
typedef void (*skynet_dl_signal)(void * inst, int signal);

struct skynet_module {
	const char * name;
	void * module;
    // 声明了一个变量create,变量的类型是skynet_dl_create函数指针,此时create等待着被赋值一个地址
    // 而这个地址,就是 一个[不接受任何参数],且返回[void指针] 的函数 的地址
	skynet_dl_create create;
    
    // 声明了一个变量init,变量的类型是skynet_dl_init函数指针,此时init等待着被赋值一个地址
    // 而这个地址,就是 一个[接收void指针,skynet_context结构体指针,const字符指针],且返回[int型] 的函数 的地址
	skynet_dl_init init;
    
    // 声明了一个变量release,变量的类型是skynet_dl_release函数指针,此时release等待着被赋值一个地址
    // 而这个地址,就是 一个[接收void指针],且返回[void] 的函数 的地址
	skynet_dl_release release;
    
    // 声明了一个变量signal,变量的类型是skynet_dl_signal函数指针,此时signal等待着被赋值一个地址
    // 而这个地址,就是 一个[接收void指针,int值],且返回[void] 的函数 的地址
	skynet_dl_signal signal;
};

// 把skynet_dl_create函数指针类型,看作是和int一样的类型就很好理解了
// int number = 10;即声明了一个int型变量number,并把整数值10赋值给number变量
// skynet_dl_create create = create_func;即声明了一个函数指针类型变量create,并把对应类型函数的地址create_func赋值给create变量

好了,现在已经建立了基础的modules结构

struct modules {
	int count; // 模块的数量
	struct spinlock lock; // 自旋锁
	const char * path; // 模块的路径
	struct skynet_module m[MAX_MODULE_TYPE] = {
        {
			const char * name; // 模块名称
			void * module; // 指向模块的指针
			skynet_dl_create create; // create函数指针变量
			skynet_dl_init init; // init函数指针变量
			skynet_dl_release release; // release函数指针变量
			skynet_dl_signal signal; // signal函数指针变量
		},
        
        {
			const char * name; // 模块名称
			void * module; // 指向模块的指针
			skynet_dl_create create; // 声明 函数指针create变量
			skynet_dl_init init; // 声明 函数指针init变量
			skynet_dl_release release; // 声明 函数指针release变量
			skynet_dl_signal signal; // 声明 函数指针signal变量
		},
            
        {
			const char * name; // 模块名称
			void * module; // 指向模块的指针
			skynet_dl_create create;
			skynet_dl_init init;
			skynet_dl_release release;
			skynet_dl_signal signal;
		},
        
        .... // 直到32个
    }; // 模块结构数量 MAX_MODULE_TYPE = 32
};


如何使用动态库

2,那我们建立这个modules结构,是怎么使用的呢?

程序如何使用外部的动态库,其实就是:

把动态库中要使用的函数的地址,赋值给本程序内某个函数指针变量,然后本程序直接使用函数指针变量,就能调用动态库中的函数了

modules结构体中的count记录动态库数量,path记录动态库所在的文件目录(path是一个const char *)

最重要的是modules结构体中的skynet_module,就是用来记录调用的动态库的信息,那第一步就是首先查找到动态库文件在那里

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 
    // 思考一下,为什么需要双重检查?
    // 因为动态库的数量是非常少的,所以很大概率skynet_module_query的时候,都能在上一步直接返回动态库的信息,因为大概率早就已经存在了
    // 那么就没必要一上来就立马加锁,加锁会导致效率降低
    // 只有在没有找到动态库信息的时候,才进行SPIN_LOCK(M)加锁
    // 又因为在上一步查找的_query(name)过程中,返回结果是null,但有可能在这期间刚好被引入了动态库
    // 所以在上锁后,再查找一次,确保没有重复
	
	if (result == NULL && M->count < MAX_MODULE_TYPE) {
		int index = M->count;
		void * dl = _try_open(M,name); // 尝试打开动态库文件
		if (dl) {
			M->m[index].name = name; // 记录动态库名字
			M->m[index].module = dl; // 绑定动态库文件句柄
	
			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;
}

看看_query(name)的实现:

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;
}

再看看_try_open(M,name):

// skynet_main.c文件156行:config.module_path = optstring("cpath","./cservice/?.so");
// skynet_start.c文件274行:skynet_module_init(config->module_path);
// skynet_module.c文件164行:m->path = skynet_strdup(path); 把 config->module_path 复制到 modules->path
// 即 m->path = "./cservice/?.so"
// 实际在example中的config,也就是skynet的启动文件中,是可以配置cpath的
// 那么skynet_main.c文件156行:config.module_path = optstring("cpath","./cservice/?.so");
// 就会读取配置中的 cpath
// 在配置中又有个规则,不同目录之间使用 ';' 号进行划分,最后串成一个字符串,即: 目录1;目录2;目录3
// 注意:目录后面跟 ';' 号,表示本条目录结束了
// 例如:./cservice/?.so;./lualib/?.so;/root/service/dll/?.so

// skynet_main.c文件161行:config.logservice = optstring("logservice", "logger");
// skynet_start.c文件279行:struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
// skynet_server.c文件126行:struct skynet_module * mod = skynet_module_query(name);
// 即mod->name = “logger"

static void *
_try_open(struct modules *m, const char * name) {
	const char *l;
	const char * path = m->path;
	size_t path_size = strlen(path); // 计算m->path的长度,不包括\0,path_size = 15
	size_t name_size = strlen(name); // 计算m->name的长度,不包括\0,name_size = 6

	int sz = path_size + name_size; // sz = 21
	//search path
	void * dl = NULL;
	char tmp[sz];
	do
	{
		memset(tmp,0,sz); // 把tmp初始化为0
		while (*path == ';') // ';' 意味着到了目录末尾,把path指针+1,指向下一个字符
        {
            path++;
        }
        
        // 实际在example中的config,也就是skynet的启动文件中,是可以配置cpath的
        // 那么skynet_main.c文件156行:config.module_path = optstring("cpath","./cservice/?.so");
        // 就会读取配置中的 cpath
        // 在配置中又有个规则,不同目录之间使用 ';' 号进行划分,最后串成一个字符串,即: 目录1;目录2;目录3
		// 注意:目录后面跟 ';' 号,表示本条目录结束了
        // 例如:./cservice/?.so;./lualib/?.so;/root/service/dll/?.so
        
		if (*path == '\0') // 符号 \0 意味着配置的所有目录都已经寻遍,需要break打破循环
        {
            break;
        }
        
        // strchr(path, ';') 是一个字符串处理函数,用于在字符串 path 中查找字符 ';'
        // strchr 函数会在字符串 path 中从左到右查找字符 ';'
        // 如果找到了该字符,函数会返回指向该字符的指针;如果没有找到,则返回空指针(NULL)
        // strchr 函数只会找到第一个匹配的字符,并停止搜索
        
		l = strchr(path, ';'); 
        // 从path往后找,找到第一个';'
        // 这个时候把 l 指向 配置中此项目录后面的 ';' 
        
		if (l == NULL)
        {
            // 如果从path没有找到 ';' 表明已经到了配置目录的最后面
            // 例如:配置目录 [./cservice/?.so;./lualib/?.so;/root/service/dll/?.so]
            //		中的最后一个目录 [/root/service/dll/?.so] 后面是不带 ';' 的
            // 又例如:只有一个目录 [./cservice/?.so] 后面也是不带 ';'
            
            l = path + strlen(path);
            // 这个时候是把 l 指向 配置的最后一个位置,即 \0
        }
        
        // 我们可以看到无论如何:l都会指向配置中某项目录的最后一位
        // 只不过如果到了 配置目录的最后面 比较特殊,需要 l = path + strlen(path) 处理一下而已
        // 如果 配置目录的最后一项后面也加 ';',根本就不需要 l = path + strlen(path) 处理
        
        // 下面以配置:[./cservice/?.so;./lualib/?.so;/root/service/dll/?.so] 为例
        
		int len = l - path; // 等于截取了配置中某项目录: [./lualib/?.so] 的长度
		int i;
		for (i=0; path[i]!='?' && i < len; i++) { // 开始一个一个字符对比,并把字符复制进tmp数组
			tmp[i] = path[i]; // tmp = [./lualib/]
		}
		memcpy(tmp+i, name, name_size); // 把 name = logger 也复制进数组 tmp
        // tmp = [./lualib/logger]
        
		if (path[i] == '?') { // 检测到path[i]是 '?' 
            // 此时数组是 [./lualib/logger], 即下一个能用的位置是:tmp+i+name_size
            // 因为遇到 path[i]是 '?' 则 '?' 下一个的位置是: path+i+1,即符号 '.' 的位置
            // 又因为前面说的 int len = l - path; 等于截取了配置中某项目录: [./lualib/?.so] 的长度
            // 则 len-i-1 的长度则计算为:符号 '?' 到 符号 ';' 的长度,即 '.so' 的长度
			strncpy(tmp+i+name_size, path+i+1, len-i-1);
            // 最终得到: [./lualib/logger.so] 
		} else {
			fprintf(stderr, "Invalid C service path\n");
			exit(1);
		}
        
        // dlopen 是一个函数,用于打开一个动态链接库(共享对象文件)并加载其中的符号
        // tmp:要打开的动态链接库的文件名或路径
        // RTLD_NOW 标志表示在 dlopen 函数调用期间立即解析所有符号
        // 这意味着,如果动态链接库中存在未解析的符号,将会在 dlopen 调用期间抛出错误
        // 这有助于在加载库时捕获符号解析错误,而不是在运行时出现错误
        // RTLD_GLOBAL 标志表示将动态链接库中的符号添加到全局符号表中,使得其他被加载的库也可以访问这些符号
        // 这对于符号的共享和跨库调用非常有用
        
        // 函数返回一个指向已加载库的句柄(handle)的指针,可以使用这个句柄来在库中查找和访问符号
		// 需要注意的是,dlopen 函数可能会返回 NULL,表示打开库失败
        
        // 配合下面的 dlsym() 函数使用
		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;
}

// 这个函数的实现比较麻烦和复杂,但只要功能就一个:
// 根据m(即前面建立的modules结构体)中初始化的动态库路径path,和动态库的名字,读取动态库文件
// 即:根据 存放动态库的目录 + 动态库名称,读取出动态库文件
// 返回的 dl = dlopen(tmp, RTLD_NOW | RTLD_GLOBAL) 是个系统调用
// 根据系统返回的 dl 文件指针,就能访问整个动态库文件

最后把动态库读取出来之后,回顾上面的代码:

//struct skynet_module {
//		const char * name; // 模块名称
//		void * module; // 指向模块的指针
//		skynet_dl_create create; // create函数指针变量
//		skynet_dl_init init; // init函数指针变量
//		skynet_dl_release release; // release函数指针变量
//		skynet_dl_signal signal; // signal函数指针变量
//};

if (result == NULL && M->count < MAX_MODULE_TYPE) {
		int index = M->count;
		void * dl = _try_open(M,name); // 系统返回能访问动态库文件的指针
		if (dl) {
			M->m[index].name = name; // 把动态库名字记录在skynet_module的name中,回忆一下skynet_module结构体中的name字段
			M->m[index].module = dl; // 把动态库地址记录在skynet_module的module中,回忆一下skynet_module结构体中的module字段

			if (open_sym(&M->m[index]) == 0) { // 那open_sym()函数又是什么呢?
                // 要注意:这个name,是传进来的const char *name, 并不属于skynet_module的name
                // 需要自己复制一份存着,自己使用
				M->m[index].name = skynet_strdup(name); 
				M->count ++; // 记录读取到的动态库数量+1
				result = &M->m[index];
                // return M->m[index];返回的是这个index下的值,即skynet_module结构体的值
                // return &M->m[index];返回的是这个index下数组的地址,即返回的是一个地址值
			}
		}
}

看看open_sym()的实现:

static void *
get_api(struct skynet_module *mod, const char *api_name) {
    // strlen 是一个 C 语言标准库函数,用于计算字符串的长度(不包括字符串末尾的空字符 \0)
    // strlen 函数是基于字符的,它通过逐个检查字符是否为 \0 来确定字符串的长度
    // 当遇到第一个空字符 \0 时,它会停止计数并返回当前的长度
    
    // skynet_main.c文件161行:config.logservice = optstring("logservice", "logger");
    // skynet_start.c文件279行:struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
    // skynet_server.c文件126行:struct skynet_module * mod = skynet_module_query(name);
    // 即传入的name = “logger"
    // 则:mod->name = “logger"
    
	size_t name_size = strlen(mod->name); // 计算mod->name长度,不包括\0,name_size = 6
	size_t api_size = strlen(api_name); // 计算api_name长度,不包括\0,api_size = 7
	char tmp[name_size + api_size + 1]; // 建立一个 6+7+1 长度的数组,剩余的1是存放\0
	memcpy(tmp, mod->name, name_size); // 把mod->name复制到数组前面,复制的长度是name_size
    // 把api_name复制到数组的tmp+name_size处(因为前面放的是mod->name),复制的长度是api_size+1
    // 复制长度是api_size+1是因为需要把api_name的\0也复制过来,当作tmp的\0结尾
	memcpy(tmp+name_size, api_name, api_size+1);
    
    // strrchr 是一个字符串操作函数,用于在一个字符串中查找指定字符的最后一个出现位置
    // strrchr 在 tmp 数组中查找字符 '.' 最后一个出现的位置,并将地址结果存储在指针 ptr 中
	// 需要注意的是,如果 ptr 的值为 NULL,则表示在 tmp 字符串中没有找到字符 '.'
    
    // 为什么要这么处理?
    // 猜测大概是有些 mod->name 传入的名字带有字符 '.'
    // 因为部分服务启动时,是在lua层开始的,而lua层传入的名字就可能带有类似:.logger 的样式
    // 因此如果带有字符 '.',就变为了 tmp = [.logger_create\0]
    // 需要把返回的 ptr 的位置 + 1,得到正确的[logger_create\0]
    
    // 目前tmp = [logger_create\0]
	char *ptr = strrchr(tmp, '.');
	if (ptr == NULL) {
		ptr = tmp;
	} else {
		ptr = ptr + 1;
	}
    
    // dlsym 是一个 POSIX 标准库函数,用于在指定的动态链接库中查找符号(symbol)
    // dlsym(mod->module, ptr) 表示在 mod->module 所代表的动态链接库中查找名为 ptr 的符号,并返回该符号的地址
	// 具体来说,mod->module 是一个指向动态链接库的句柄(handle)的指针,ptr 是一个字符串,表示要查找的符号的名称
	// 如果在动态链接库中找到了名为 ptr 的符号,dlsym 函数会返回该符号的地址,可以将其赋值给一个函数指针或者其他合适的类型指针
    // dlsym 函数只能用于动态链接库中的符号查找,而不是用于静态链接库或可执行文件中的符号查找
    
    // 配合上面的 dlopen() 函数使用
    
	return dlsym(mod->module, ptr);
}

static int
open_sym(struct skynet_module *mod) {
    // 从对get_api()实现代码的分析来看,get_api()函数的作用就是2个
    // 1,把传入mod中记录的mod->name,和 "_create",拼接在一起。例如传入的是:logger,最后拼接出来的就是
    //		logger_create,logger_init,logger_release,logger_signal
    // 
    // 2,再根据上面拼接的名字 + 前面存储的mod->module(动态库的文件句柄)
    // 		搜索到 拼接的名字 == 动态库内的函数名,则把这个函数的所在地址返回
    // 		把函数地址存储在 skynet_module 中对应的函数指针中
    // 		最后等待本程序调用 skynet_module 结构体对应的create,init,release,signal变量即可
	mod->create = get_api(mod, "_create");
	mod->init = get_api(mod, "_init");
	mod->release = get_api(mod, "_release");
	mod->signal = get_api(mod, "_signal");

	return mod->init == NULL;
}

最后,在经过漫长的流程下,动态库被查找,打开,并加载了对应的函数:

skynet_module_query() —> _try_open() (获得动态库文件jubing)—> open_sym() —> get_api()(加载对应的函数)


如何制作动态库

3,到了最后,还有一个疑问:动态库是如何制作的呢?为什么动态库里面的函数名就是会等于 模块名 + 函数功能名 的拼接呢?

例如:logger_create,logger_init,logger_release,logger_signal,甚至说:我们为什么需要这些动态库呢?

我们先看看云风的文章:云风的 BLOG: Skynet 设计综述 (codingnow.com)

有段话是这样的:

做为核心功能,Skynet 仅解决一个问题:

把一个符合规范的 C 模块,从动态库(so 文件)中启动起来,绑定一个永不重复(即使模块退出)的数字 id 做为其 handle 。模块被称为服务(Service),服务间可以自由发送消息。每个模块可以向 Skynet 框架注册一个 callback 函数,用来接收发给它的消息。每个服务都是被一个个消息包驱动,当没有包到来的时候,它们就会处于挂起状态,对 CPU 资源零消耗。如果需要自主逻辑,则可以利用 Skynet 系统提供的 timeout 消息,定期触发

那么什么是符合规范的 C 模块 呢?拥有 xxx_create,xxx_init,xxx_release,xxx_signal 函数的动态库,就是符合规范的 C 模块。所以在编写这些动态库的时候一定需要有这4个函数的实现,因此动态库里面的函数名就一定会等于 模块名 + 函数功能名 的拼接

那么这些动态库是如何制作的呢?关注目录 service-src,里面存放的只有4个动态库源文件:

service_gate.c + service_harbor.c + service_logger.c + service_snlua.c

编译成动态库后,存放在目录 cservice 中,变为:gate.so harbor.so logger.so snlua.so

这4个动态库 gate.so harbor.so logger.so snlua.so 非常重要,skynet中所有的服务都是以这4个底层的c模块构建的

gate.so模块负责网络,harbor.so模块负责节点, logger.so负责日志,snlua.so负责lua层所有的服务构建

以上4个模块是最最基础的部分,这也是我们需要这些动态库的原因


模块的具体实现

4,我们自己可以去看看gate.so harbor.so logger.so snlua.so的实现代码,去看看里面的 xxx_create,xxx_init,xxx_release,xxx_signal 函数是怎么实现的,这也是skynet所有服务的工作原理了。那程序加载了这些模块,通过:

把动态库中要使用的函数的地址,赋值给本程序内某个函数指针变量,然后本程序直接使用函数指针变量,就能调用动态库中的函数了

那本程序是如何调用的呢?代码如下:

void * 
skynet_module_instance_create(struct skynet_module *m) {
	if (m->create) {
		return m->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);
}

void 
skynet_module_instance_release(struct skynet_module *m, void *inst) {
	if (m->release) {
		m->release(inst);
	}
}

void
skynet_module_instance_signal(struct skynet_module *m, void *inst, int signal) {
	if (m->signal) {
		m->signal(inst, signal);
	}
}

以上的调用,都是本程序的一次封装,都是由以下拼接:

​ skynet_module_instance + _create

​ skynet_module_instance + _init

​ skynet_module_instance + _release

​ skynet_module_instance + _signal

通过模块内各自的函数具体实现

详细的例子在logger_service服务的实现中进行解释


总结

到这里模块加载modules的全部内容就在这里了

模块加载通过struct modules结构体,记录了加载的路径和已经完成加载的数量

并且把加载的动态库记录在struct modules结构体中的数组字段,总共32个

数组中元素的实质是struct skynet_module,用于记录不同的动态库数据

这意味着Skynet支持最多32种不同的模块

每种不同的模块,实现应该有xxx_create,xxx_init,xxx_release,xxx_signal 函数的实现

已提供给Skynet进行调用,否则就不是一个符合Skynet规范的模块,不能被启动

这些底层的c模块最后就是Skynet运行不同服务的基础

模块加载提供了模块搜索,实现函数的绑定,查找模块等功能

这些功能只是基本的程序调用外部动态库的操作,并非重要的实现代码

最重要的代码还是每个不同模块各自的实现

具体的例子看《Skynet源码之:service_logger》

清楚展示了动态库里xxx_create,xxx_init,xxx_release,xxx_signal 函数的实现


优化建议:

一般来说,我们都只会使用Skynet自带的4种服务模块:gate.so harbor.so logger.so snlua.so

上面说自带的4种服务模块其实都是在c层面实现的

但更多的普通服务都是在 snlua.so 的基础上,开启lua虚拟机,执行的lua代码

这势必意味着该服务的速度有一定影响

可能有些性能要求的功能,需要在C层面实现,此时就需要加载属于自己的模块

例如:网络收包解包的模块,我们有很高的性能要求,此时我们需要有一个c层面的服务

看看这篇文章:

云风的blog:云风的 BLOG: 在 skynet 中处理 TCP 的分包 (codingnow.com)

GitHub仓库:github.com

一般来说,我们不会加载很多的模块,我个人认为32的数组太大了

可以优化为在配置中由用户进行配置,像harbor一样把值传进去

这样每个进程启动时,都能读取到属于自己能加载的模块数量