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一样把值传进去
这样每个进程启动时,都能读取到属于自己能加载的模块数量