初探 Skynet:轻量级分布式游戏服务器框架实战

++在游戏服务器开发领域,高效、稳定且易于扩展的框架一直是开发者追求的目标。Skynet 作为一款轻量级、高性能的分布式游戏服务器框架,凭借其独特的设计理念和强大的功能,赢得了众多开发者的青睐++

一.Skynet底层架构支持

1.Actor

erlang 从语言层面支持 actor 并发模型,并发实体是 actor (在 skynet 中称之为 服务 ); skynet 采用 c + lua 来实现 actor 并发模型;底层也是通过采用多少个核心开启多少个内核线程来充分利用多核;

有消息的 actor 为活跃的 actor,没有消息为非活跃的 actor;

2.config

我们要配置config文件来支持skynet的运行

Lua 复制代码
thread=8    --8个工作线程
logger=nil  --不打日志
harbor=0 
start="main" --main文件作为入口函数
lua_path="./skynet/lualib/?.lua;./skynet/lualib/?/init.lua;"
-- lua 抽象进程
luaservice="./skynet/service/?.lua;./app/?.lua;"
lualoader="./skynet/lualib/loader.lua"
cpath="./skynet/cservice/?.so"
lua_cpath="./skynet/luaclib/?.so"
3.core-eg

我们通过一个网络消息的代码案例来看actor是怎么运行的

Lua 复制代码
local skynet = require "skynet"
local socket = require "skynet.socket"

skynet.start(function()
    local listenfd = socket.listen("192.168.217.148",8888)
    socket.start(listenfd,function (clientfd,addr)
        print("recive a client:",clientfd,addr)
    end)

    skynet.timeout(100,function()
    print("after 1s ,do here")
    end)

    print("hello skynet")

    -- local slave = skynet.newservice("slave")
    -- local response = skynet.call(slave,"lua","ping")
    --[[
    main ->  ping ->slave  //协程挂起
    slave -> pong ->main   //协程唤醒
    c/c++ 异步流程 callbcak 协程来实现
    通过协程的方式消除异步产生的回调我们就能写同步的代码
    ]]
    -- print(response)
end)

skynet.start 是服务的入口点,Skynet 会为这个服务创建一个独立的 Actor服务启动时执行的代码(如打印 "hello skynet")在 Actor 初始化阶段同步执行
socket.listensocket.start 注册了一个网络监听事件

当有新客户端连接时,Skynet 会将这个事件封装成消息发送给当前服务 Actor

服务 Actor 在处理消息时执行回调函数(打印客户端信息)
skynet.timeout 注册了一个定时器事件

Skynet 的定时器系统会在指定时间后向服务 Actor发送一个超时消息

服务 Actor收到超时消息后执行对应的回调函数

main

Lua 复制代码
local skynet = require "skynet"
local socket = require "skynet.socket"

skynet.start(function()
    
    local slave = skynet.newservice("slave")
    local response = skynet.call(slave,"lua","ping")
    --[[
    main ->  ping ->slave  //协程挂起
    slave -> pong ->main   //协程唤醒
    c/c++ 异步流程 callbcak 协程来实现
    通过协程的方式消除异步产生的回调我们就能写同步的代码
    ]]
    print(response)
end)

slave

Lua 复制代码
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)

我们上述代码通过callback回调的方式来驱动actor运行

4.公平调度

actor 是抽象的用户态进程,相对于 linux 内核 ,有进程调度,那么 skynet 也要实现 actor 调度

  1. 将活跃的 actor 通过 全局队列 组织起来; actor 当中的 消息队列 有消息就是活跃的 actor ;
  2. 线程池去 全局队列中取出 actor 的消息队列,接着运行 actor ;

二.lua/c****接口编程

skynet 、 openresty 都是深度使用 lua 语言的典范;学习 lua 不仅仅要学习基本用法,还要学会使用 c 与 lua 交互,这样才学会了 lua 作为胶水语言的精髓;

1.虚拟栈

①栈中只能存放 lua 类型的值,如果想用 c 的类型存储在栈中,需要将 c 类型转换为 lua 类型;
②lua 调用 c 的函数都得到一个 的栈,独立于之前的栈;
③c 调用 lua ,每一个协程都有一个栈;
④c 创建虚拟机时,伴随创建了一个主协程,默认创建一个虚拟栈;
⑤无论何时 Lua 调用 C , 它都只保证至少有 LUA_MINSTACK 这么多的堆栈空间可以使用。
⑥LUA_MINSTACK 一般被定义为 20 , 因此,只要你不是不断的把数据压栈, 通常你不用关心堆栈 大小。

2.C调用lua接口
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

//lua嵌入在C语言里面的好处--->热更新 不需要重新编译lua代码 重启即可更新or文件变化重新加载

/*
 * 调用Lua全局函数,向其传递一个整数值1
 * L: Lua虚拟机指针
 * funcname: 要调用的Lua函数名称
 */
static void
call_func_0(lua_State *L, const char* funcname)
{
     // 从全局环境中获取指定名称的函数
    lua_getglobal(L, funcname);
    //向栈顶压入整数参数1
    lua_pushinteger(L, 1);
    // 调用函数,1个参数,0个返回值
    lua_call(L, 1, 0);

    //C调lua必须维护栈的平衡 eg:--->lua_pop(L);
}

int main(int argc, char** argv)
{
    //创建新的lua虚拟机
    lua_State *L = luaL_newstate();
    //打开lua标准库
    luaL_openlibs(L);

    if (argc > 1) {

        lua_pushboolean(L, 1);
        lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");

        if ( LUA_OK != luaL_dofile(L, argv[1]) ) {
            const char* err = lua_tostring(L, -1);
            fprintf(stderr, "err:\t%s\n", err);
            return 1;
        }

        // 依次调用Lua脚本中的Init、Loop、Release函数
        call_func_0(L, "Init");
        call_func_0(L, "Loop");
        call_func_0(L, "Release");
        lua_close(L);
        return 0;
    }
    return 0;
}
Lua 复制代码
package.cpath = "luaclib/?.so"
local so = require "tbl.c"



--a=1
--全局可见

--local a =1
--当前文件可见

-- 加了local相当于C/C++中的static

function Init(args)
    print("call [init] function", args)
end

function Loop()
    print("call [loop] function")
    for k, v in ipairs({1,2,3,4,5}) do
    --so.echo(v)  -- 等于 调用 5 次  因为每次调用都是一个新的虚拟栈,所以没有必要维护 --->C调用lua然后调C
   print("value = " .. v)  --->C调用lua
    end
    return 1
end

function Release()
    print("call [release] function")
end

c->lua每个协程一个栈 我们需要维护栈的空间 弹出不需要的栈空间

3.C调用lua接口-->lua再调用C接口
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

//lua嵌入在C语言里面的好处--->热更新 不需要重新编译lua代码 重启即可更新or文件变化重新加载

/*
 * 调用Lua全局函数,向其传递一个整数值1
 * L: Lua虚拟机指针
 * funcname: 要调用的Lua函数名称
 */
static void
call_func_0(lua_State *L, const char* funcname)
{
     // 从全局环境中获取指定名称的函数
    lua_getglobal(L, funcname);
    //向栈顶压入整数参数1
    lua_pushinteger(L, 1);
    // 调用函数,1个参数,0个返回值
    lua_call(L, 1, 0);

    //C调lua必须维护栈的平衡 eg:--->lua_pop(L);
}

int main(int argc, char** argv)
{
    //创建新的lua虚拟机
    lua_State *L = luaL_newstate();
    //打开lua标准库
    luaL_openlibs(L);

    if (argc > 1) {

        lua_pushboolean(L, 1);
        lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");

        if ( LUA_OK != luaL_dofile(L, argv[1]) ) {
            const char* err = lua_tostring(L, -1);
            fprintf(stderr, "err:\t%s\n", err);
            return 1;
        }

        // 依次调用Lua脚本中的Init、Loop、Release函数
        call_func_0(L, "Init");
        call_func_0(L, "Loop");
        call_func_0(L, "Release");
        lua_close(L);
        return 0;
    }
    return 0;
}
Lua 复制代码
package.cpath = "luaclib/?.so"
local so = require "tbl.c"



--a=1
--全局可见

--local a =1
--当前文件可见

-- 加了local相当于C/C++中的static

function Init(args)
    print("call [init] function", args)
end

function Loop()
    print("call [loop] function")
    for k, v in ipairs({1,2,3,4,5}) do
    so.echo(v)  -- 等于 调用 5 次  因为每次调用都是一个新的虚拟栈,所以没有必要维护 --->C调用lua然后调C
    --print("value = " .. v)  --->C调用lua
    end
    return 1
end

function Release()
    print("call [release] function")
end
cpp 复制代码
#include <lua.h>

#include <lauxlib.h>

#include <lualib.h>

#include <stdio.h>

static int
lecho (lua_State *L) {
    const char* str = lua_tostring(L, -1);
    fprintf(stdout, "%s\n", str);
    return 0;
}

static const luaL_Reg l[] = {// 导出给lua使用数组
	{"echo", lecho},
	{NULL, NULL},
};

int
luaopen_tbl_c(lua_State *L) { // local tbl = require "tbl.c"
    // 创建一张新的表,并预分配足够保存下数组 l 内容的空间
	// luaL_newlibtable(L, l);
	// luaL_setfuncs(L, l, 0);
    luaL_newlib(L, l);
	return 1;
}
4.运行lua/ src/lua 可执行程序让lua调用C接口

cpp 复制代码
#include <lua.h>

#include <lauxlib.h>

#include <lualib.h>

#include <stdio.h>

static int
lecho (lua_State *L) {
    const char* str = lua_tostring(L, -1);
    fprintf(stdout, "%s\n", str);
    return 0;
}

static const luaL_Reg l[] = {// 导出给lua使用数组
	{"echo", lecho},
	{NULL, NULL},
};

int
luaopen_tbl_c(lua_State *L) { // local tbl = require "tbl.c"
    // 创建一张新的表,并预分配足够保存下数组 l 内容的空间
	// luaL_newlibtable(L, l);
	// luaL_setfuncs(L, l, 0);
    luaL_newlib(L, l);
	return 1;
}
Lua 复制代码
package.cpath = "luaclib/?.so" --c库的路径

local so = require "tbl.c"

so.echo("hello world") -- 新的虚拟栈
so.echo("hello world1")-- 新的虚拟栈
so.echo("hello world2")-- 新的虚拟栈


--[[
    1. c调用lua  c有多个协程 每个协程一个虚拟栈
    2. lua调用c  每次调用都有一个虚拟栈
]]

cpp 复制代码
#include <lua.h>

#include <lauxlib.h>

#include <lualib.h>

#include <stdio.h>

// 闭包实现:  函数 + 上值  luaL_setfuncs
// lua_upvalueindex(1)
// lua_upvalueindex(2)
static int
lecho (lua_State *L) {
	lua_Integer n = lua_tointeger(L, lua_upvalueindex(1));
    n++;
    const char* str = lua_tostring(L, -1);
    fprintf(stdout, "[n=%lld]---%s\n", n, str);
    lua_pushinteger(L, n);
    lua_replace(L, lua_upvalueindex(1));
    return 0;
}

static const luaL_Reg l[] = {
	{"echo", lecho},
	{NULL, NULL},
};

int
luaopen_uv_c(lua_State *L) { // local tbl = require "tbl.c"
	luaL_newlibtable(L, l);// 1
    lua_pushinteger(L, 0);// 2
	luaL_setfuncs(L, l, 1);// 上值
    // luaL_newlib(L, l);
	return 1;
}
Lua 复制代码
package.cpath = "luaclib/?.so"

local so = require "uv.c"

so.echo("hello world1")
so.echo("hello world2")
so.echo("hello world3")
so.echo("hello world4")
so.echo("hello world5")
so.echo("hello world6")
so.echo("hello world7")
so.echo("hello world8")
so.echo("hello world9")

--./lua/src/lua test-uv.lua

cpp 复制代码
#include <lua.h>

#include <lauxlib.h>

#include <lualib.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

struct log {
    int count;
};

static int
lagain(lua_State *L) {
    struct log *p = (struct log *)luaL_checkudata(L, 1, "mk.ud.log");
    lua_getuservalue(L, -1);
    const char* str = lua_tostring(L, -1);
    fprintf(stdout, "ud[n=%d]----%s\n", p->count, str);
    return 0;
}

static int
lecho(lua_State *L) {
    struct log *p = (struct log *)luaL_checkudata(L, 1, "mk.ud.log");
    const char* str = lua_tostring(L, -1);
    p->count++;
    lua_setuservalue(L, -2);
    fprintf(stdout, "ud[n=%d]----%s\n", p->count, str);
	return 0;
}

static int
lnew (lua_State *L) {
    struct log *q = (struct log*)lua_newuserdata(L, sizeof(struct log));
    q->count = 0;
    lua_pushstring(L, "");
    lua_setuservalue(L, -2);
	if (luaL_newmetatable(L, "mk.ud.log")) {
        luaL_Reg m[] = {
			{"echo", lecho},
            {"again", lagain},
			{NULL, NULL},
		};
		luaL_newlib(L, m);
		lua_setfield(L, -2, "__index");
        lua_setmetatable(L, -2);
    }
    return 1;
}

static const luaL_Reg l[] = {
	{"new", lnew},
	{NULL, NULL},
};

int
luaopen_ud_c(lua_State *L) {
    luaL_newlib(L, l);
	return 1;
}
Lua 复制代码
package.cpath = "luaclib/?.so"

local so = require "ud.c"

local ud = so.new()

ud:echo("hello world1")
ud:again()
ud:echo("hello world2")
ud:again()
ud:echo("hello world3")
ud:again()
ud:echo("hello world4")
ud:again()
ud:echo("hello world5")
ud:again()
ud:echo("hello world6")
ud:again()
ud:echo("hello world7")
ud:again()
ud:echo("hello world8")
ud:again()
ud:echo("hello world9")
ud:again()

--./lua/src/lua test-ud.lua

lua->c不需要维护栈空间 因为每次调用都会生成一个新栈

相关推荐
搞不懂语言的程序员6 小时前
Redis的Pipeline和Lua脚本适用场景是什么?使用时需要注意什么?
数据库·redis·lua
花落已飘14 小时前
LVGL(lv_btnmatrix矩阵按钮)
ui·c·lvgl
编程百晓君19 小时前
C语言速成之07switch语句详解:多分支选择的高效实现
c
DBWYX20 小时前
Linux proc文件系统 内存影射
linux·c
·云扬·2 天前
【PmHub后端篇】PmHub中基于Redis加Lua脚本的计数器算法限流实现
redis·算法·lua
一丝晨光2 天前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
Aric_Jones2 天前
lua入门语法,包含安装,注释,变量,循环等
java·开发语言·git·elasticsearch·junit·lua
Petrichorzncu3 天前
Lua再学习
开发语言·学习·lua
mikey棒棒棒3 天前
lua脚本+Redission实现分布式锁
redis·分布式·lua·看门狗·redission