深度学习 skynet —— 总体架构

前言

服务端高级架构—云风的skynet
用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数组
… …
};

扫描二维码关注公众号,回复: 12160126 查看本文章

服务模块

先来看一下创建服务的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也不例外:

在这里插入图片描述

推荐自己的技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、大厂面试题、有趣的项目和热门技术教学视频资料共享在里面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等等.),有需要的可以自行添加哦!~
技术交流群
四小时玩转skynet
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_52622200/article/details/112276959