skynet与actor

skynet是一个为网络游戏服务器设计的轻量框架,采用单进程,多线程架构。底层是c,中间层和上层都是lua。基于actor模型,使用消息队列进行内部通信。

总体思路

skynet是一个很好的actor实例。通过给每个worker分配lua vm,有效地实现了actor的思路。

简单说,可以把skynet理解为一个简单的操作系统,它可以用来调度数千个lua虚拟机,让它们并行工作。每个lua虚拟机都可以接收处理其它虚拟机发送过来的消息,以及对其它虚拟机发送消息。每个lua虚拟机,可以看成skynet这个操作系统下的独立进程,你可以在skynet工作时启动新的进程、销毁不再使用的进程、还可以通过调试控制台监管它们。skynet同时掌控了外部的网络数据输入,和定时器的管理;它会把这些转换为一致的(类似进程间的消息)消息输入给这些进程。
120056311-e459f100-c06d-11eb-8117-af58ad15e2bc

上图其实就是 Actor 模型,Actor 模型是一个概念模型,用于处理并发计算。它定义了一系列系统组件应该如何动作和交互的通用规则,即 Actor 的参与者 = {消息队列, 处理逻辑(服务)}。

Actor实现

那在 skynet 底层中,如何描述一个 Actor 参与者呢? 通过上面的结构体关系图可知,其实在 skynet 底层使用了结构体 struct skynet_context 来描述一个 Actor 参与者,这个结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct skynet_context {
void * instance; // 它是一个指针,指向了一个服务对象,它是由服务模板 struct skynet_module * mod 创建出来的
struct skynet_module * mod; // 动态库
void * cb_ud; // 一个被回调函数真实调用的服务实例对象的指针
skynet_cb cb; // 服务的回调函数,是消息被服务对象执行的唯一通道,这个回调函数可以重新设置
struct message_queue *queue; // 它是一个指针,指向了消息队列对象,这个对象中的 q->queue 字段才真正存放了消息数组
FILE * logfile; // for 服务日志
uint64_t cpu_cost; // in microsec,for cpu 性能指标
uint64_t cpu_start; // in microsec,for cpu 性能指标 & 当前消息处理耗时
char result[32]; // 存放性能指标的查询结果
uint32_t handle; // 服务的id
int session_id; // 消息的session id分配器
int ref; // 服务引用计数
int message_count; // 已处理过的消息总数
bool init; // 初始化成功的标识
bool endless; // 死循环标识
bool profile; // cpu 性能指标开启开关

CHECKCALLING_DECL
};

instance,cb,queue,cb_ud。这四个字段,构成了一个 Actor 核心骨架,消息队列内的消息被服务的回调函数 cb 执行,这也是为什么说 skynet 是消息驱动的。一个大致的流程是:一个消息通过服务的回调函数,然后传递给服务实例,最终被消费处理掉。

1
2
// 回调函数类型
typedef int (*skynet_cb)(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz);

这里需要注意下,在服务实例第一次初始化并设置回调后,cb_ud 与 instance 是同一个东西,即ctx->instance = ctx->cb_ud,但是对于 snlua 服务启动后,会重新设置一次回调,这里就产生了两个区别的:

1.cb_ud 指向由原来的 ctx->instance 变成了 lua vm(lua 主线程),它们存在这样的一个关系:ctx->instance->L = ctx->cb_ud = luavm;
2.cb 回调函数变成了一个封装了 lua 调用的函数;

具体的更改流程的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
// cb_ud=gL
// cb = forward_cb,消息派发后不free掉,用于转发消息
skynet_callback(context, gL, forward_cb);
} else {
// cb = _cb,消息派发后free掉
skynet_callback(context, gL, _cb);
}

return 0;
}

El amor eterno dura aproximadamente tres meses.