Skynet服务器框架 C源码剖析启动流程

2024-05-07 05:18

本文主要是介绍Skynet服务器框架 C源码剖析启动流程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引言:

之前我们已经完成了在Linux下配置安装 skynet 的环境,并成功启动了 skynet 服务框架,为了从底层更好地理解整个框架的实现过程,我们有必要剖析一下源码,由于底层的源码都是用C语言写的,lua脚本基本是用来进行业务层开发,所以我们从C源码开始解读框架。打开下载包的 skynet-src 目录,这里是skynet框架的核心C源码,接下来我们就要来解读 skynet_main.cskynet_start.c 这两个与skynet启动相关的C源码。
启动流程
启动流程
1.skynet-src/skynet_main.c 这个是main()函数所在,主要就是设置一下lua的环境、默认的配置、打开config配置文件,并修改默认配置。最后调用skynet_start()函数,这个函数在skynet_start.c文件中。
2.skynet-src/skynet_start.c这个文件主要是初始化Skynet的各个模块,包括harbor节点、handle服务ID、mq消息队列、module加载动态链接库、timer时钟、socket套接字以及加载一些服务logger日志服务、master主服务、harbor节点服务、snlua 加载lua模块的服务;以及最后启动几种线程包括_moitor、_timer、_socket和根据线程数启动n个工作线程。

1.入口函数和初始化:

我们启动 skynet 使用的指令 ./skynet example/config 实际上就是调用 skynet-src/skynet_main.c 脚本的入口 main 函数,调用时将 config 配置文件地址传入到函数中,并在此函数中完成:设置环境加载配置文件

//skynet_main.cint 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.hstruct 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.cmain 函数末尾,完成 环境设置配置信息加载 之后,调用了 skynet_start(&config); 函数,这是在 skynet-src/skynet_start.c 中定义的,接下来我们来看一下实现的源码:

//skynet_start.cvoid skynet_start(struct skynet_config * config) {// register SIGHUP for log file reopenstruct 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_freeskynet_harbor_exit();skynet_socket_free();if (config->daemon) {daemon_exit(config->daemon);}
}
代码解析:
  • 根据配置信息进行各个服务的初始化:
    使用 -> 间接引用运算符,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.luasnlua 是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.luaint 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 thenassert(standalone ==  nil)standalone = trueskynet.setenv("standalone", "true")local ok, slave = pcall(skynet.newservice, "cdummy")if not ok thenskynet.abort()endskynet.name(".cslave", slave)elseif standalone thenif not pcall(skynet.newservice,"cmaster") thenskynet.abort()endendlocal ok, slave = pcall(skynet.newservice, "cslave")if not ok thenskynet.abort()endskynet.name(".cslave", slave)endif standalone thenlocal datacenter = skynet.newservice "datacenterd"skynet.name("DATACENTER", datacenter)endskynet.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服务。

这篇关于Skynet服务器框架 C源码剖析启动流程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/966421

相关文章

将Java项目提交到云服务器的流程步骤

《将Java项目提交到云服务器的流程步骤》所谓将项目提交到云服务器即将你的项目打成一个jar包然后提交到云服务器即可,因此我们需要准备服务器环境为:Linux+JDK+MariDB(MySQL)+Gi... 目录1. 安装 jdk1.1 查看 jdk 版本1.2 下载 jdk2. 安装 mariadb(my

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

Redis在windows环境下如何启动

《Redis在windows环境下如何启动》:本文主要介绍Redis在windows环境下如何启动的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Redis在Windows环境下启动1.在redis的安装目录下2.输入·redis-server.exe

解决SpringBoot启动报错:Failed to load property source from location 'classpath:/application.yml'

《解决SpringBoot启动报错:Failedtoloadpropertysourcefromlocationclasspath:/application.yml问题》这篇文章主要介绍... 目录在启动SpringBoot项目时报如下错误原因可能是1.yml中语法错误2.yml文件格式是GBK总结在启动S

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

基于Python打造一个可视化FTP服务器

《基于Python打造一个可视化FTP服务器》在日常办公和团队协作中,文件共享是一个不可或缺的需求,所以本文将使用Python+Tkinter+pyftpdlib开发一款可视化FTP服务器,有需要的小... 目录1. 概述2. 功能介绍3. 如何使用4. 代码解析5. 运行效果6.相关源码7. 总结与展望1

使用Python开发一个简单的本地图片服务器

《使用Python开发一个简单的本地图片服务器》本文介绍了如何结合wxPython构建的图形用户界面GUI和Python内建的Web服务器功能,在本地网络中搭建一个私人的,即开即用的网页相册,文中的示... 目录项目目标核心技术栈代码深度解析完整代码工作流程主要功能与优势潜在改进与思考运行结果总结你是否曾经

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu