zoukankan      html  css  js  c++  java
  • skynet总体架构

    前言

    skynet是我们游戏服务端的底层框架,当初在技术选型的时候仔细阅读过它的源码,发现它是一个C语言的工程典范。大多数游戏服务端,要么使用C++,要么使用java,使用C是非常少见的。但是skynet通过C和Lua的结合,实现了一个高效的游戏框架,C层没有多余的一堆三方库,只有紧凑的核心结构,提供最核心的消息处理框架;Lua层用来写游戏逻辑,降低了开发门槛。

    目前skynet在阿里游戏大量使用,据我所闻风之大陆,时下很火的三国志使用的都是skynet,而我们游戏当然也用这个框架,已经稳定运营了一年有余。

    说起来skynet并不能算是一个游戏服务端框架,它只是提供了一些游戏服务端必须的基础设施,可以用这套设施去设计符合要求的上层逻辑。按照云风的说法,skynet实现了类似Erlang 的 Actor 模型,它本质上是一个高并发的消息处理框架,消息从底层派发给上层的“服务”去处理,这里的服务可以用C编写,当然大部分时候都是用Lua编写,每个Lua服务是一个独立的Lua虚拟机,这就保证了服务之间的环境隔离,Lua服务使用协程处理消息,当需要向其他服务通讯时,协程可以挂起等其他服务返回再继续,这让我们一方面能像写同步代码一样“顺序执行”,另一方面当协程挂起时,该服务可以处理其他消息,这就保证了消息的高并发。

    由于skynet内核的精简,很多人抱着开箱即用的想法,后面发现门槛其实并不低,它仍然要求你对游戏服务器的业务很熟悉,知道自己想要实现什么,然后自己动手。但是正是由于它的精简,使得他的可定制性很高。

    skynet的核心功能

    如果要用一句话描述skynet核心功能是什么:它仍然是一个基于事件的高并发消息处理框架。事件主要来源于网络,定时器和信号通知等,当事件触发时,skynet将这些事件统一编码成消息结构,派发给感兴趣的服务处理;而服务在处理消息时,也可以主动向其他服务发送消息。因此他是事件来驱动的,如果没有前面说的那些事件,skynet就没法做任何事情。

    skynet的核心数据结构是 skynet_context ,我对Erlang不熟悉,所以没法说出它对应于Erlang的什么结构;但它实际上也像操作系统中的进程的概念,在这里我们把它称之为服务,一个服务包含了下面几个东西:

    • 服务句柄:和进程ID类似,用于唯一标识服务。
    • 服务模块:模块以动态库的形式提供。在创建skynet_context的时候,必须指定模块的名字,skynet把模块加载进来,创建模块实例,实例向服务注册一个回调函数,用于处理服务的消息。
    • 消息队列:每个服务都有一个消息队列,当队列中有消息时,会主动挂到全局链表。skynet启动了一定数量的工作线程,不断从全局链表取出消息队列,派发消息给服务的回调函数去处理。

    下面的结构图展示了skynet最核心的结构:

    服务句柄

    每个服务都关联一个句柄,句柄的实现在 skynet_handle.h|c 中,句柄是一个32位无符号整型,最高8位表示集群ID(已不推荐使用),剩下的24位为服务ID。

    handle_storage 用于存储ID和skynet_context的映射:

    // 句柄存储结构
    struct handle_storage {
        struct rwlock lock;             // 读写锁
        uint32_t harbor;                // 集群ID
        uint32_t handle_index;          // 当前句柄索引
        int slot_size;                  // 槽位数组大小
        struct skynet_context ** slot;  // skynet_context数组
        ... ...
    };

    服务模块

    先来看一下创建服务的API:

    // 创建一个服务:name为服务模块的名字,parm为参数,由模块自己解释含义
    struct skynet_context * skynet_context_new(const char * name, const char * parm);

    这里的name参数就是模块名,skynet根据这个名字加载模块,并调用约定好的导出函数。这个过程大概是这样的:

    • 得到模块后,调用skynet_module_instance_create函数创建模块实例。
    • 然后调用skynet_module_instance_init初始化实例,通常实例在初始化时调用skynet_callback向skynet设置回调函数,以后消息处理由该回调函数处理。

    消息队列

    创建服务时也会新建一个消息队列,消息队列在 skynet_mq.c|h 中实现,消息队列用下面的结构表示:

    // 消息队列
    struct message_queue {
        struct spinlock lock;
        uint32_t handle;                // 关联的服务句柄
        int cap;                        // 队列容量
        int head;                       // 队列头的位置
        int tail;                       // 队列尾的位置
        struct skynet_message *queue;   // 消息结构数组
        struct message_queue *next;     // 指向下一个消息队列 
        ... ...
    };

    next指向下一个消息队列,也就是说message_queue会形成一个链表,然后由global_queue持有,global_queue就这样的:

    struct global_queue {
        struct message_queue *head;
        struct message_queue *tail;
        struct spinlock lock;
    };

    global_queue持有的链表是需要处理消息的消息队列,这个过程是这样的:

    • 调用skynet_mq_push向消息队列压入一个消息。
    • 然后,调用skynet_globalmq_push把消息队列链到global_queue尾部。
    • 从全局链表弹出一个消息队列,处理队列中的消息,如果队列的消息处理完则不压回全局链表,如果未处理完则重新压入全局链表,等待下一次处理。

    描述得比较简单,具体的细节还是要查看skynet_context_message_dispatch这个函数。

    skynet启动及消息处理

    上面把服务的三个重要组成部分介绍完,现在可以来看看skynet_context的内容了:

    struct skynet_context {
        void * instance;        // 服务模块的实例指针
        struct skynet_module * mod;   // 服务模块指针
        void * cb_ud;                 // 回调函数的用户数据
        skynet_cb cb;                 // 服务处理消息的回调函数
        struct message_queue *queue;  // 消息队列
        uint32_t handle;      // 服务句柄
        ... ...
    };

    其实包含的最核心的部分就是上面介绍的三个,那么skynet是怎么样启动起来,并不断地处理消息呢?答案就是skynet_start这个函数:

    • 第一步初始化各个功能模块,比如句柄,消息队列,模块,定时器,socket等等。
    • 然后创建一个logger服务。创建一个bootstrap服务。
    • 接着创建一定数量的工作线程,这个数量可由配置指定,工作线程的责任就是派发消息。
    • 创建定时器线程,用于记录时间以及实现timeout事件;
    • 创建sokcet线程,用于处理sokcet消息,socket和timeout事件最终都会转化成消息,交给工作线程派发给服务处理。
    • 创建monitor线程,这个线程的作用是监控服务有没有出现死循环。

    前面说过,skynet是由事件驱动运行的,这里的事件主要就是两个,一个是socket,另一个是timeout。分别由两个线程驱动运行。

    工作线程的核心逻辑就是调用skynet_context_message_dispatch去派发消息,派发完成后,它会进入睡眠状态,等待另外两个线程来唤醒。这就是非常典型的生产消费者模型,绝大多数服务器程序的核心功能就是这个,skynet也不例外:

    from:https://zhuanlan.zhihu.com/p/84634254

  • 相关阅读:
    《JavaScript高级程序设计》读书笔记(十):本地对象Date
    JavaScript计算字符串中每个字符出现的次数
    JavaScript单元测试ABC
    ASP.NET MVC3 AJAX 上传图片示例
    canvas标签的width和height以及style.width和style.height的区别
    分享一个自定义的 console 类,让你不再纠结JS中的调试代码的兼容
    《JavaScript高级程序设计》阅读笔记(十四):继承机制的实现
    从此不再惧怕URI编码:JavaScript及C# URI编码详解
    Levenshtein算法的JavaScript实现
    《JavaScript高级程序设计》阅读笔记(十五):浏览器中的JavaScript
  • 原文地址:https://www.cnblogs.com/lidabo/p/14266251.html
Copyright © 2011-2022 走看看