Skynet服务器框架——C源码剖析启动流程
liuian 2024-12-25 14:00 64 浏览
引言:
在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服务。
相关推荐
- 腾达路由器手机设置教程(腾达路由器手机设置教程视频)
-
用手机设置腾达路由器的方法如下:1在手机上打开浏览器,输入路由器背面的管理IP和用户及对应的密码2一般第一次打开,默认会跳出设置向导,准备好宽带用户名和密码,3按向导提示输入相应内容4在无线设置的安全...
- 自配电脑配置推荐(自配电脑配置推荐百度)
-
首先,像这类软件最低要求不高。最高没上限。纯粹看你的工程量大小。CPU有双核,内存有4G,就可以运行。但是实际体验肯定比较差,卡是肯德。渲染时间也会超长,一个小作品渲染几小时是正常的。稍微大点的工程也...
- 2025年平板性价比排行(2020年值得买的平板)
-
推荐台电P30S好。 基本配置:10.1英寸IPS广视角屏幕,1280*800分辨率,16:10的黄金显示比例,K9高压独立功放,支持3.5mm耳麦接口,联发科MT8183八核处理器,4GB...
- 2020显卡天梯图10月(2020显卡天梯图极速空间)
-
排行球队名称积分已赛胜平负进球失球净胜球 1?诺维奇城974629107753639 2?沃特福德91462710...
-
- 笔记本电脑无线网络连接(笔记本电脑无线网络连接不上怎么办)
-
一、笔记本电脑怎么连接wifi---win7系统笔记本连接wifi1、要先创建无线网络连接,将鼠标移到Win7的开始菜单,然后点击“控制面板”。2、然后点击“网络和Internet”。3、再打开“网络和共享中心”,这是Win7系统必有的功...
-
2025-12-22 05:55 liuian
- wind数据库(wind数据库官网)
-
先购买wind数据库,安装好wind取得使用权后,按照wind所给提示,输入账户和密码可使用wind数据库。Wind资讯金融终端是一个集实时行情、资料查询、数据浏览、研究分析、新闻资讯为一体的金融数据...
- 如何关闭360家庭防火墙(如果关闭360家庭防火墙)
-
关闭方法如下:1.打开手机360主界面之后,点击“安全防护中心”。2.点击第三列“入口防护”下方的“查看状态”按钮。3.在列出的功能项中找到“局域网防护”,直接点击后面的“关闭”按钮,关闭所有的“局域...
- 笔记本电脑型号配置怎么看(怎么查自己电脑的型号)
-
查电脑的配置和型号方法:方法一:1、右键单击“此电脑”,点击属性2、这里可以看到操作系统,CPU等大致信息3、点击设备管理器4、这里可以查看具体硬件的详细信方法二:1、首先打开电脑上的“控制面板”2、...
- pscs6序列号是什么
-
AdobePhotoshopCS6就二个版本(测试版和正式版)1、AdobePhotoshopCS6是AdobePhotoshop的第13代,是一个较为重大的版本更新。2、Photoshop在前几...
- win7桌面图片怎么设置(win7如何设置桌面图片)
-
1、首先用鼠标右键单击桌面的空白处。然后在弹出的菜单上选择“个性化”选项。这样就弹出了的个性化窗口上能显示看到“桌面背景”按钮。点击它即可。2、继续打开了选择“桌面背景”选项,然后在上面选择你想要设置...
- windows安卓下载(win安卓版)
-
2265安卓网是安全的,2265安卓网成立于2012年初,网站一直努力为各位安卓爱好者提供最新、最全的安卓游戏软件资源下载。经过几个月的努力、和广大安卓用户的支持、2265安卓网截至到2012年6月已...
- 电脑系统网站排行榜(电脑系统网址还有哪些)
-
车架号查询网站:http://www.yiparts.com/vin通过车架号查询车辆信息。新国标电动车一般是15位纯数字的车架号,如175721508069087,1757前四位是企业代码,由企业申...
- 清理磁盘碎片怎么清理(清理磁盘碎片怎么清理不了)
-
清除磁盘碎片的方法是通过使用磁盘清理工具来进行操作。首先,明确结论是清除磁盘碎片可以提高电脑的性能和运行速度。其次,原因是磁盘碎片是指文件在硬盘上存储时被分割成多个碎片,导致读取速度变慢。清除磁盘碎片...
- 一周热门
- 最近发表
- 标签列表
-
- 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)
