轻量级游戏服务器框架:skynet的原理讲解
一:skynet的介绍
首先他是一个轻量级的游戏服务器框架,但是他的作用并不只是在游戏中。那么他的轻量级体现在什么地方?
1:实现了 actor 模型,以及相关的脚手架(工具集):
actor 间数据共享机制;
c 服务扩展机制;
2:实现了服务器框架的基础组件:
实现了 reactor 并发网络库,并提供了大量连接的接入方案;
基于自身网络库,实现了常用的数据库驱动(异步连接方案),并融合了 lua 数据结构;
实现了网关服务;
时间轮用于处理定时消息;
二:多核并发编程
多线程:
在一个进程中开启多线程,为了充分利用多核,一般设置工作线程的个数为 cpu 的核心数;
多线程在一个进程当中,所以数据共享来自进程当中的虚拟内存;这里会涉及到很多临界资源的访问,所以需要考虑加锁;
多进程:
在一台机器当中,开启多个进程充分利用多核,一般设置工作进程的个数为 cpu 的核心数;
nginx 就是采用这种方式(master 进程 和多个 worker进程);
nginx 当中的 worker 进程,通过共享内存来进行共享数据;也需要考虑使用锁;
CSP:
以 go 语言为代表,并发实体是协程(用户态线程、轻量级线程);
内部也是采用多少个核心开启多少个线程来充分利用多核;
Actor:
erlang 从语言层面支持 actor 并发模型,并发实体是 actor(在skynet 中称之为服务);
skynet 采用 c + lua 来实现 actor 并发模型;
底层也是通过采用多少个核心开启多少个内核线程来充分利用多核;
总结:我们要尽量不通过共享内存来通信,而应该通过通信来共享内存。通过通信来共享数据,其实是一种解耦合的过程;并发实体之间可以分别开发并单独优化,而它们唯一的耦合在于消息;这能让我们快速地进行开发;同时也符合我们开发的思路,将一个大的问题拆分成若干个小问题;
三:Actor 并发模型
1:定义
Actor用于并行计算,并且是最基本的计算单元。Actor基于消息计算,并且通过消息进行通信。
2:组成
隔离的环境:主要通过 lua 虚拟机来实现;
消息队列:用来存放有序(先后到达)的消息;
回调函数:用来运行 Actor;从 Actor 的消息队列中取出消息,并作为该回调函数的参数来运行 Actor;在skynet.start 中会设置回调函数,一个消息执行的时候,会获取一个协程执行它;
Actor是skynet在用户层进行抽象的一个进程,为什么要进行抽象进程呢?
我们知道在 lua 中有虚拟机(拥有隔离的环境),而加载这个虚拟机的代价较小,而在同一个进程中的多个lua虚拟机可以共享很多lua资源。当我们抽象一个进程之后,那么我们就可以提供一个隔离的运行环境,避免多线程的资源竞争(避免多个抽象进程消费同一资源)。
3:Actor 的公平调度
首先我们一个skynet中含有多个 actor ,而这些actor全都是对等的,并且他们每个actor中都含有消息队列。线程池的并发实体是线程,nginx的并发实体是进程,而skynet的并发实体是actor,所以我们需要公平调度actor。
对于很多的actor来说,我们需要采用两级队列来进行公平调度:首先我们需要找到其中含有消息的actor(活跃队列),将这些actor连接在一起形成一级队列,然后开始调度队列,调度的时候,我们轮到哪个actor,查看他的消息队列,从中取出一个消息任务,这些消息队列组成的就是二级队列。我们这个公平调度是每一个actor进行pop出这个消息队列中的一个任务后,然后将他pushback到一级队列的末尾,然后继续下一个。
但是在我们用户定义的actor中,并不是每一个消息队列的消息是一样多的,一般都是不均匀的,因此skynet在工作线程中赋予了权重来解决这个问题
// 工作线程权重图 32个核心
static int weight[] = {-1, -1, -1, -1, 0, 0, 0, 0,1, 1, 1, 1, 1, 1, 1, 1, // 1/22, 2, 2, 2, 2, 2, 2, 2, // 1/43, 3, 3, 3, 3, 3, 3, 3, }; // 1/8
比如一次pop出一半的任务,四分之一的任务,这样对于消息很多的队列来说就比较友好。
四:skynet的具体使用
1:skynet的简单程序
首先我们需要在skynet存在的目录中创建一个config文件,在这个文件中我们要指定一些参数:
thread=4 --线程的数量
logger=nil --日志产生的位置
harbor=0 --设置集群,这里不设置
start="main" --启动的应用程序
lua_path="./skynet/lualib/?.lua;./skynet/lualib/?/init.lua;" --lua的路径
luaservice="./skynet/service/?.lua;./5.1-skynet/?.lua" --lua服务的路径
lualoader="./skynet/lualib/loader.lua" --lua加载器
cpath="./skynet/cservice/?.so" --c服务的路径
lua_cpath="./skynet/luaclib/?.so" --lua的c服务的路径
--其中留意一下分号,分号后也是一个路径
然后我们开始写main.lua的代码:
local skynet = require "skynet"skynet.start(function()print("hello skynet")
end)
然后创建一个Makefile文件:
SKYNET_PATH?=./skynetall:cd $(SKYNET_PATH) && $(MAKE) PLAT='linux'clean:cd $(SKYNET_PATH) && $(MAKE) clean
2:skynet网络消息
在skynet中含有一个socket的线程,专门接收消息,并且采用了reactor模型,对于众多的actor中,我们怎么知道这个网络消息是发送给谁的呢?我们在这个接收的时候,会进行绑定,这样就不会丢失了。
local skynet = require "skynet"
local socket=require "skynet.socket"skynet.start(function()print("hello skynet1")local listenfd=socket.listen("0.0.0.0",8081);socket.start(listenfd,function(clientfd,addr)print("receive a client: ",clientfd,addr);end)print("hello skynet2")
end)
3:定时消息
创建的定时任务被推送给定时线程,定时线程检测完时间后往消息队列推送一个消息,然后actor开始执行callback函数。
local skynet = require "skynet"skynet.start(function()print("hello skynet")skynet.timeout(100,function()print("已经过了 1s");end)end)
4:actor之间的消息
在actor之间的消息,是通过发送消息进行数据交换的,发送的消息将存放到对方的消息队列中。
local skynet = require("skynet")skynet.start(function()print("hello skynet")local slave=skynet.newservice("slave")local response=skynet.call(slave,"lua","ping")print("main: ",response)
end)
local skynet = require "skynet"local CMD={}function CMD.ping()skynet.retpack("pong")
endskynet.start(function()skynet.dispatch("lua",function(session,source,cmd,...)local func=assert(CMD[cmd])func(...)end)
end)
本篇主要讲解了skynet的基础使用和原理,感谢大家的观看!0voice · GitHub