Skynet服务器框架——C源码剖析启动流程
liuian 2024-12-25 14:00 73 浏览
引言:
在Linux下配置安装 skynet 的环境这里略过,为了从底层更好地理解整个框架的实现过程,我们有必要剖析一下源码,由于底层的源码都是用C语言写的,lua脚本基本是用来进行业务层开发,所以我们从C源码开始解读框架。打开下载包的 skynet-src 目录,这里是skynet框架的核心C源码,接下来我们就要来解读 skynet_main.c 和 skynet_start.c 这两个与skynet启动相关的C源码。
1.入口函数和初始化:
我们启动 skynet 使用的指令 ./skynet example/config 实际上就是调用 skynet-src/skynet_main.c 脚本的入口 main 函数,调用时将 config 配置文件地址传入到函数中,并在此函数中完成:设置环境 和 加载配置文件
//skynet_main.c
int main(int argc, char *argv[]) {
//保存config文件地址的变量
const char * config_file = NULL ;
if (argc > 1) {
//读取配置文件config的地址,保存在config_file变量中
config_file = argv[1];
} else {
//不传入config文件地址会提示错误并结束程序
fprintf(stderr, "Need a config file. Please read skynet wiki : https://github.com/cloudwu/skynet/wiki/Config\n"
"usage: skynet configfilename\n");
return 1;
}
//初始化操作
luaS_initshr();
//全局初始化,为线程特有数据使用pthread_key_create()函数创建一个key,然后使用pthread_setspecific()函数为这个key设置value值
skynet_globalinit();
//初始化lua环境,创建一个全局数据结构struct skynet_env *E,并初始化结构的值
skynet_env_init();
//设置信号处理函数,用于忽略SIGPIPE信号的处理
sigign();
//创建启动skynet所需的必要配置信息结构数据
struct skynet_config config;
//申请一个lua虚拟机
struct lua_State *L = luaL_newstate();
//链接一些必要的lua库到刚刚申请的lua虚拟机中
luaL_openlibs(L); // link lua lib
//执行config配置文件在lua中的读取
int err = luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");
assert(err == LUA_OK);
//把C读取的config配置文件内容串压入栈顶
lua_pushstring(L, config_file);
//执行栈顶的chunk,实际上就是加载config这个lua脚本字符串的内容
err = lua_pcall(L, 1, 1, 0);
if (err) {
fprintf(stderr,"%s\n",lua_tostring(L,-1));
lua_close(L);
return 1;
}
//初始化保存config信息的环境env
_init_env(L);
//通过skynet_getenv()接口从env中获取配置文件的信息(其实内部机制是通过lua_setglobal把之前压入栈顶的config_file转成lua中作为全局变量)
config.thread = optint("thread",8);
config.module_path = optstring("cpath","./cservice/?.so");
config.harbor = optint("harbor", 1);
config.bootstrap = optstring("bootstrap","snlua bootstrap");
config.daemon = optstring("daemon", NULL);
config.logger = optstring("logger", NULL);
config.logservice = optstring("logservice", "logger");
config.profile = optboolean("profile", 1);
//关闭上面创建的L(lua虚拟机)
lua_close(L);
//开始执行skynet的真是启动skynet服务程序的操作
skynet_start(&config);
//对应上面的skynet_globalinit(),用于删除 线程存储的Key。
skynet_globalexit();
//对应上面的luaS_initshr()
luaS_exitshr();
return 0;
}2.配置信息结构体:
必要的数据被定义在一个 skynet-src/skynet_imp.h 中的 skynet_config 结构体内:
//skynet_imp.h
struct skynet_config {
int thread; //启动工作线程数量,不要配置超过实际拥有的CPU核心数
int harbor; //skynet网络节点的唯一编号,可以是 1-255 间的任意整数。一个 skynet 网络最多支持 255 个节点。每个节点有必须有一个唯一的编号。如果 harbor 为 0 ,skynet 工作在单节点模式下。此时 master 和 address 以及 standalone 都不必设置。
int profile; //是否开启统计功能,统计每个服务使用了多少cpu时间,默认开启
const char * daemon; //后台模式:daemon = "./skynet.pid"可以以后台模式启动skynet(注意,同时请配置logger 项输出log)
const char * module_path; //用 C 编写的服务模块的位置,通常指 cservice 下那些 .so 文件
const char * bootstrap; //skynet 启动的第一个服务以及其启动参数。默认配置为 snlua bootstrap ,即启动一个名为 bootstrap 的 lua 服务。通常指的是 service/bootstrap.lua 这段代码。
const char * logger; //它决定了 skynet 内建的 skynet_error 这个 C API 将信息输出到什么文件中。如果 logger 配置为 nil ,将输出到标准输出。你可以配置一个文件名来将信息记录在特定文件中。
const char * logservice; //默认为 "logger" ,你可以配置为你定制的 log 服务(比如加上时间戳等更多信息)。可以参考 service_logger.c 来实现它。注:如果你希望用 lua 来编写这个服务,可以在这里填写 snlua ,然后在 logger 配置具体的 lua 服务的名字。在 examples 目录下,有 config.userlog 这个范例可供参考。
};启动skynet服务程序:
在 skynet-src/skynet_main.c 的 main 函数末尾,完成 环境设置 和 配置信息加载 之后,调用了 skynet_start(&config); 函数,这是在 skynet-src/skynet_start.c 中定义的,接下来我们来看一下实现的源码:
//skynet_start.c
void skynet_start(struct skynet_config * config) {
// register SIGHUP for log file reopen
struct sigaction sa;
sa.sa_handler = &handle_hup;
sa.sa_flags = SA_RESTART;
sigfillset(&sa.sa_mask);
sigaction(SIGHUP, &sa, NULL);
if (config->daemon) {
if (daemon_init(config->daemon)) {
exit(1);
}
}
skynet_harbor_init(config->harbor);
skynet_handle_init(config->harbor);
skynet_mq_init();
skynet_module_init(config->module_path);
skynet_timer_init();
skynet_socket_init();
skynet_profile_enable(config->profile);
struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
if (ctx == NULL) {
fprintf(stderr, "Can't launch %s service\n", config->logservice);
exit(1);
}
bootstrap(ctx, config->bootstrap);
start(config->thread);
// harbor_exit may call socket send, so it should exit before socket_free
skynet_harbor_exit();
skynet_socket_free();
if (config->daemon) {
daemon_exit(config->daemon);
}
}给大家推荐一个关于skynet项目实战的一个训练营 现在报名相当于免费,(后台私信“skynet”获取地址)主讲内容:
多核并发编程
消息队列。线程池
actor消息调度
网络模块实现
时间轮定时器实现
lua/c/接口编程
skynet编程精要
demo演示actor编程思维
更多skynet资料加群:812855908免费领取!
代码解析:
根据配置信息进行各个服务的初始化:
使用 -> 间接引用运算符,config 是指向 skynet_config 结构体的指针,config-> 是引用结构体成员变量:
//根据配置信息进行一系列初始化
if (config->daemon) {
//初始化守护进程
if (daemon_init(config->daemon)) {
exit(1);
}
}
//初始化节点模块,用于集群,转发远程节点的消息
skynet_harbor_init(config->harbor);
//初始化句柄模块,用于给每个Skynet服务创建一个全局唯一的句柄值
skynet_handle_init(config->harbor);
//初始化消息队列模块,这是Skynet的主要数据结构
skynet_mq_init();
//初始化服务动态库加载模块,主要用于加载符合Skynet服务模块接口的动态链接库(.so)
skynet_module_init(config->module_path);
//初始化定时器模块
skynet_timer_init();
//初始化网络模块
skynet_socket_init();
//加载日志模块
skynet_profile_enable(config->profile);创建第一个模块 logger 服务的实例,并启动这个服务:
使用 skynet_context_new(...) 函数实例化一个服务:
struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);这里传入两个参数:参数一是 加载模块的名称,参数二是初始化由模块生成的实例时所需的 传入设置参数,下面是创建一个服务的具体流程:
会从 logger.so 中把模块加载出来:
struct skynet_module * mod = skynet_module_query(name);让加载出来的模块自动生成一个新的实例:
void *inst = skynet_module_instance_create(mod);给新实例注册一个事件处理的handle:
ctx->handle = skynet_handle_register(ctx);创建这个实例的消息队列:
struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);调用模块的初始化方法
int r = skynet_module_instance_init(mod, inst, ctx, param);将实例的消息队列加到全局的消息队列中,这样才能收到消息回调
skynet_globalmq_push(queue);加载 bootstrap 引导模块:
bootstrap(ctx, config->bootstrap);安装默认 config 的配置内容,config->bootstrap 的内容就是一串字符串 bootstrap = "snlua bootstrap",下面来看一下 bootstrap 函数的具体实现过程:
static void bootstrap(struct skynet_context * logger, const char * cmdline) {
//获取字符串长度
int sz = strlen(cmdline);
char name[sz+1];
char args[sz+1];
//将传入的cmdline字符串按照格式分割成两部分,前部分模块名,后部分为模块初始化参数
sscanf(cmdline, "%s %s", name, args);
//创建并启动指定模块的一个服务
struct skynet_context *ctx = skynet_context_new(name, args);
//假如创建失败
if (ctx == NULL) {
//通过传入的logger服务接口构建错误信息假如logger消息队列
skynet_error(NULL, "Bootstrap error : %s\n", cmdline);
//输出消息队列中的错误信息
skynet_context_dispatchall(logger);
//结束程序
exit(1);
}
}同样使用 skynet_context_new() 与上面启动 logger 服务一样,先把 snlua.so 模块加载进来,然后调用此模块自身的实例化方法,去实例化一个 snlua 服务,并传入要实例化的 lua服务的脚本名称 为 bootstarp,bootstrap会根据config中 luaservice 配置的目录去获取指定名称的 lua脚本,按照默认目录最后会匹配到 service/bootstrap.lua 。snlua 是lua的沙盒服务,所有的 lua服务都是一个 snlua 的实例。
snlua 实例化的过程:
这里我们来看一下 snlua 模块的实例化方法,源码在 service-src/service_snlua.c 中的 snlua_create(void) 函数:
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;
//创建一个lua虚拟机(Lua State)
l->L = lua_newstate(lalloc, l);
return l;
}最后返回的是一个通过 lua_newstate 创建出来的 Lua vm(lua虚拟机),也就是一个沙盒环境,这是为了达到让每个 lua服务 都运行在独立的虚拟机中。
* lua服务 的初始化:*
上面的实例化步骤,只是生成了 lua服务 的运行沙盒环境,至于沙盒内运行的具体内容,是在初始化的时候才填充进来的,这里我们再来简单剖析一下初始化函数 snlua_init 的源码:
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
int sz = strlen(args);
//在内存中准备一个空间(动态内存分配)
char * tmp = skynet_malloc(sz);
//内存拷贝:将args内容拷贝到内存中的temp指针指向地址的内存空间
memcpy(tmp, args, sz);
//注册回调函数为launch_cb这个函数,有消息传入时会调用回调函数并处理
skynet_callback(ctx, l , launch_cb);
const char * self = skynet_command(ctx, "REG", NULL);
//当前lua实例自己的句柄id(转为无符号长整型)
uint32_t handle_id = strtoul(self+1, NULL, 16);
// it must be first message
// 给自己发送一条消息,内容为args字符串
skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
return 0;
}这个初始化函数主要完成了两件事:
- 给当前服务实例注册绑定了一个回调函数 launch_cb;
- 给本服务发送一条消息,内容就是之前传入的参数 bootstrap 。
当此服务的消息队列被push进全局的消息队列后,本服务收到的第一条消息就是上述在初始化中给自己发送的那条消息,此时便会调用回调函数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);
//设置各项资源路径参数,并加载loader.lua
int err = init_cb(l, context, msg, sz);
if (err) {
skynet_command(context, "EXIT", NULL);
}
return 0;
}这个方法里把服务自己在C语言层面的回调函数给注销了,使它不再接收消息,目的是:在lua层重新注册它,把消息通过lua接口来接收。
紧接着执行init_cb方法:
设置了一些虚拟机环境变量(紫瑶是资源路径类的):
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");加载执行了lualib\loader.lua文件:
const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");loader 的作用是以 cml 参数为名去各项代码目录 查找lua文件,找到后 loadfile 并执行(等效于 dofile)。
同时把真正要加载的文件(此时是 bootstrap.lua)作为参数传给它,最终 bootstrap.lua 脚本会被加载并执行脚本中的逻辑, 控制权就开始转到lua层。
Lua脚本逻辑起点:
完成上述的所有底层 C语言 逻辑之后,我们开始执行 lua层 的业务逻辑,起点就是上述最后加载和执行的 bootstrap.lua ,打开脚本,脚本内容如下:
local skynet = require "skynet"
local harbor = require "skynet.harbor"
require "skynet.manager" -- import skynet.launch, ...
local memory = require "memory"
skynet.start(function()
local sharestring = tonumber(skynet.getenv "sharestring" or 4096)
memory.ssexpand(sharestring)
local standalone = skynet.getenv "standalone"
local launcher = assert(skynet.launch("snlua","launcher"))
skynet.name(".launcher", launcher)
local harbor_id = tonumber(skynet.getenv "harbor" or 0)
if harbor_id == 0 then
assert(standalone == nil)
standalone = true
skynet.setenv("standalone", "true")
local ok, slave = pcall(skynet.newservice, "cdummy")
if not ok then
skynet.abort()
end
skynet.name(".cslave", slave)
else
if standalone then
if not pcall(skynet.newservice,"cmaster") then
skynet.abort()
end
end
local ok, slave = pcall(skynet.newservice, "cslave")
if not ok then
skynet.abort()
end
skynet.name(".cslave", slave)
end
if standalone then
local datacenter = skynet.newservice "datacenterd"
skynet.name("DATACENTER", datacenter)
end
skynet.newservice "service_mgr"
pcall(skynet.newservice,skynet.getenv "start" or "main")
skynet.exit()
end)源码剖析:
这里执行了 skynet.start 这个接口,这也是所有 lua服务 的标准启动入口,服务启动完成后,就会调用这个接口,传入的参数就是一个function(方法),而且这个方法就是此 lua服务 的在lua层的回调接口,本服务的消息都在此回调方法中执行。
skynet.start 接口:
关于每个lua服务的启动入口 skynet.start 接口的实现代码在 service/skynet.lua 中:
function skynet.start(start_func)
--重新注册一个callback函数,并且指定收到消息时由dispatch_message分发
c.callback(skynet.dispatch_message)
skynet.timeout(0, function()
skynet.init_service(start_func)
end)
end具体如何实现回调方法的注册过程,需要查看c.callback这个C语言方法的底层实现,源码在 lualib-src/lua-skynet.c:
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);
lua_rawsetp(L, LUA_REGISTRYINDEX, _cb);
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
lua_State *gL = lua_tothread(L,-1);
if (forward) {
skynet_callback(context, gL, forward_cb);
} else {
skynet_callback(context, gL, _cb);
}
return 0;
}与上面snlua初始化中的一致,使用 skynet_callback 来实现回调方法的注册。
总结:
跟随逻辑去查看源码,大致了解到skynet服务框架的启动实现流程大致为:
- 加载配置文件 -> 配置文件存入lua的全局变量evn -> 创建和启动C服务logger -> 启动引导模块并启动第一个lua服务(例如:bootstrap)。
第一个启动的lua服务其实都会由 config 配置文件中的 bootstrap 配置项所决定的,可以根据项目实际情况进行修改,当然也可以保持默认设置,保持使用 bootstrap 作为第一个lua服务,直接或间接地去启动其他的lua服务。
相关推荐
-
- 驱动网卡(怎么从新驱动网卡)
-
网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...
-
2026-01-30 00:37 liuian
- win10更新助手装系统(微软win10更新助手)
-
1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...
- windows11专业版密钥最新(windows11专业版激活码永久)
-
Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...
-
- 手机删过的软件恢复(手机删除过的软件怎么恢复)
-
操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...
-
2026-01-29 23:55 liuian
- 一键ghost手动备份系统步骤(一键ghost 备份)
-
步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。 步骤...
- 怎么创建局域网(怎么创建局域网打游戏)
-
1、购买路由器一台。进入路由器把dhcp功能打开 2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。 3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...
- 精灵驱动器官方下载(精灵驱动手机版下载)
-
是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...
- 一键还原系统步骤(一键还原系统有哪些)
-
1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。 2、在弹出的“准备安装”窗口中,可...
- 电脑加速器哪个好(电脑加速器哪款好)
-
我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...
- 任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)
-
是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...
- u盘怎么恢复文件(u盘文件恢复的方法)
-
开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...
- 系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)
-
1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...
-
- 剪贴板权限设置方法(剪贴板访问权限)
-
1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...
-
2026-01-29 21:37 liuian
- 平板系统重装大师(平板重装win系统)
-
如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...
- 联想官网售后服务网点(联想官网售后服务热线)
-
联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...
- 一周热门
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
