一: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/2
    2, 2, 2, 2, 2, 2, 2, 2,  // 1/4
    3, 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?=./skynet

all:
	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")
end

skynet.start(function()
    skynet.dispatch("lua",function(session,source,cmd,...)
        local func=assert(CMD[cmd])
        func(...)
    end)
end)

本篇主要讲解了skynet的基础使用和原理,感谢大家的观看!0voice · GitHub 

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐