框架源码

skynet_fly源码

框架如何启动第一个服务

该框架为了避免每个人部署的目录不同,使用相对路径的方式启动服务。使用脚本run.sh load_mods 是否启动守护进程

关键配置

load_mods,配置各个服务的启动的配置,例如第几个开启、启动的实例数量。然后运行的时候通过write_config转换成实际配置。

如何启动skynet_fly项目

  • 拉取代码git clone https://github.com/huahua132/skynet_fly.git以及拉取子模块代码
  • 切换到skynet目录下,拉取子模块git submodule update --init,如果更新子模块,则执行git submodule update --remote
  • ubuntu系统使用sh install_ubuntu.sh安装对应依赖软件和库
  • 切回根目录执行make linux编译
  • 在开发的项目中执行sh ../../binshell/make_server.sh ../../构建服务
  • bash make/script/run.sh load_mods_prod.lua 0运行服务

启动的流程

入口文件:main.lua
核心启动:容器

local skynet = require "skynet"
local container_launcher = require "skynet-fly.container.container_launcher"
local env_util = require "skynet-fly.utils.env_util"

skynet.start(function()
    skynet.error("start digitalbomb!!!>>>>>>>>>>>>>>>>>")
    env_util.setenv("test_proto", "pb")  -- 设置测试协议
    local delay_run = container_launcher.run()  -- 核心启动函数

    skynet.uniqueservice("room_game_login")  -- 启动登录服务(非热更服务)

    delay_run()  -- 执行延迟启动的模块
    skynet.exit()
end)

容器根据load_mods配置,分成立即启动和延迟启动的服务列表

function M.run()
    skynet.monitor('monitor_exit')  -- 启动退出监控服务
    local cmgr = skynet.uniqueservice('container_mgr')  -- 启动容器管理器
    skynet.uniqueservice("debug_console", env_util.getenv('debug_port'))  -- 启动调试控制台

    local before_run_list = {}  -- 立即启动列表
    local delay_run_list = {}   -- 延迟启动列表
    
    -- 按 launch_seq 排序模块
    for mod_name, mod_cfg in table_util.sort_ipairs(load_mods, function(a,b)
        return a.launch_seq < b.launch_seq
    end) do
        if not mod_cfg.delay_run then
            table.insert(before_run_list, mod_name)
        else
            table.insert(delay_run_list, mod_name)
        end
    end
    
    -- 启动第一批模块(按 launch_seq 顺序)
    skynet.call(cmgr, 'lua', 'load_modules', self_address, table.unpack(before_run_list))
    
    -- 返回延迟启动函数
    return function()
        if not delay_run_list or #delay_run_list <= 0 then return end
        skynet.call(cmgr, 'lua', 'load_modules', self_address, table.unpack(delay_run_list))
        delay_run_list = nil
    end
end

可热更服务是如何被加载的

通知容器管理器服务创建热更服务

-- 核心逻辑
for _, module_name in ipairs(module_name_list) do
    local m_cfg = load_mods[module_name]
    local isok, id_list, name_id_list = launch_new_module(module_name, m_cfg)
    -- ...
end

local function launch_new_module(module_name, config)
    local launch_num = config.launch_num
    local mod_args = config.mod_args or {}
    local default_arg = config.default_arg or {}
    
    for i = 1, launch_num do
        -- 创建 hot_container 服务实例
        local isok, server_id = pcall(skynet.newservice, 'hot_container', 
            module_name, i, cur_date, cur_time, version, is_record_on)
        
        -- 启动模块
        local args = mod_args[i] or default_arg
        local isok, ret = pcall(skynet_call, server_id, 'lua', 'start', args, auto_reload, record_backup)
    end
end
function CMD.start(cfg, auto_reload, record_backup)
    -- 1. 设置模块配置
    module_info.set_cfg(cfg)
    
    -- 2. 调用业务模块的 start 函数
    local ret = module_start(cfg)  -- module_start = CMD.start(业务模块定义)
    
    -- 3. 启动成功后的回调
    if ret then
        for _, func in ipairs(g_start_after_cb) do
            skynet.fork(func)
        end
        
        -- 4. 设置自动热更新定时器(如果配置了)
        if auto_reload and INDEX == 1 then
            -- ...
        end
        
        -- 5. 开启客户端访问
        container_client:open_ready()
        SERVER_STATE = SERVER_STATE_TYPE.starting
    end
    
    return ret
end

服务间如何通信

推荐方式:container_client
定位:

  • 主要为可热更新服务之间的通信提供支持
  • 同时支持普通服务访问可热更新服务
  • 不支持普通服务之间的通信

方式一:普通服务调用可热更服务


-- 获取模块实例
local client = container_client:instance("room_game_hall_m")

-- 1. 轮询负载均衡调用
local ret = client:balance_call("hello", arg1, arg2)

-- 2. 模除映射调用(固定服务)
local ret = client:set_mod_num(1):mod_call("hello", arg1, arg2)

-- 3. 广播调用(所有实例)
local ret = client:broadcast_call("hello", arg1, arg2)

-- 4. 发送(无返回)
client:balance_send("notify", data)
client:broadcast_send("broadcast", data)

方式二:可热更服务之间通信

-- 在 A_m.lua 中(可热更新服务)
local container_client = require "skynet-fly.client.container_client"

-- 加载阶段注册要访问的服务
container_client:register("B_m")

function CMD.start(cfg)
    -- 启动后获取客户端实例
    local client = container_client:instance("B_m")
    
    -- 调用服务
    local ret = client:balance_call("hello")
end

方式三:skynet原生通信

一般用于普通服务之间的调用

-- 在任何服务中调用普通服务
local skynet = require "skynet"

-- 直接调用,不需要 container_client
local ret = skynet.call("room_game_login", "lua", "login", account, password)

可热更容器

sh make/script/check_reload.sh load_mods.lua热更指令

如何实现热更

动态发现问题

在传统的静态配置中,服务地址是硬编码的:

-- 静态调用(无法应对动态变化)
local server_id = 12345
skynet.call(server_id, "lua", "cmd", ...)

问题场景:

  1. 服务扩容:新增服务实例时,调用方无法自动感知
  2. 服务缩容:服务实例下线时,调用方可能调用到已关闭的服务
  3. 热更新:服务更新重启后,旧地址失效,新地址需要被感知
  4. 故障恢复:服务崩溃重启后,地址可能变化

skynet_fly的方案——watch机制

在服务创建的时候,就会监听每一个服务

local function load_modules(...)
	-- 1. 过滤已关闭的模块
    for i = #module_name_list, 1, -1 do
        if g_close_load_map[module_name_list[i]] then
            tremove(module_name_list, i)
        end
    end
    
    -- 2. 加载配置并验证
    local load_mods = loadfile(loadmodsfile)()
    for _, module_name in ipairs(module_name_list) do
        assert(load_mods[module_name], "not m_cfg " .. module_name)
    end
    
    -- 3. 按启动顺序排序
    tsort(module_name_list, function(a,b)
        return load_mods[a].launch_seq < load_mods[b].launch_seq
    end)
    
    -- 4. 通知旧服务预告退出
    for _, module_name in ipairs(module_name_list) do
        call_module(module_name, "herald_exit")
    end
    
    -- 5. 启动新服务
    local tmp_module_map = {}
    for _, module_name in ipairs(module_name_list) do
        local m_cfg = load_mods[module_name]
        local isok, id_list, name_id_list = launch_new_module(module_name, m_cfg)
        tmp_module_map[module_name] = { id_list = id_list, name_id_list = name_id_list }
    end
    
    -- 6. 更新映射表并通知 watcher
    for module_name, m in pairs(tmp_module_map) do
        g_id_list_map[module_name] = m.id_list
        g_name_id_list_map[module_name] = m.name_id_list
        
        -- 唤醒所有 watcher
        for source, response in pairs(g_watch_map[module_name]) do
            response(true, m.id_list, m.name_id_list, g_version_map[module_name])
        end
    end
    
    -- 7. 通知旧服务退出
    for module_name, id_list in pairs(old_id_list_map) do
        call_id_list(id_list, "close")
    end
end

当手动触发热更新的时候,会调用到load_modules,此时就会他会退出旧服务,那么热更之后,其他服务要访问热更的服务时,就找不到最新的地址。所以服务创建之后,container_mgr会通过g_watch_map获取更新模块监听的名单,然后回复这些。

那么g_watch_map是如何注册的呢?关键角色就是container_client

-- 1. 注册要访问的服务(声明依赖)
container_client:register("B_m")

-- 2. 创建客户端实例(首次访问时触发监听注册)
local client = container_client:instance("B_m")
                 │
                 ▼
-- 触发 g_mod_svr_ids_map["B_m"] 的 __index
-- 执行:
--   1. query() 获取服务列表
--   2. skynet.fork(monitor, t, "B_m") 注册监听
--   3. register_visitor() 注册访问者
local function watch(source,module_name,oldversion)
	local id_list = g_id_list_map[module_name]
	local name_id_list = g_name_id_list_map[module_name]
	local newversion = g_version_map[module_name]
	local watch_map = g_watch_map[module_name]  -- 获取监听名单

	assert(not watch_map[source])
	if oldversion ~= newversion then
		return id_list,name_id_list,newversion
	end

	watch_map[source] = skynet.response()
	return skynet_util.NOT_RET
end
热更新对已有变量引用的影响
-- hotfix.lua:189-192
for _, info in ipairs(sort_list) do
    local name = info.name
    local info = hot_ret[name]
    local old_m = info.old_m
    local new_m = info.new_m
    
    -- 核心:将新模块的内容逐个复制到旧模块表中
    for k, v in pairs(new_m) do
        old_m[k] = v  -- 原地更新!不是替换整个表
    end
end

热更新不是替换整个模块表,而是将新模块的内容逐个键值对复制到旧模块表中。所以对于已经被函数引用的变量,是不会有影响,对于模块级的会有影响。

案例讲解
local config = { max_users = 100 }

function CMD.get_max_users()
    return config.max_users
end

-- 热更新后 config.max_users = 200

-- 调用结果
CMD.get_max_users()  -- 返回 200 ✅

config是模块级表,热更新时config.max_users被修改

-- 在某个协程中
local value = CMD.get_value()  -- value = 10(热更新前)

-- 执行热更新(CMD.get_value 现在返回 20)

print(value)  -- 仍然是 10 ❌

value 是函数内部的局部变量,已经被赋值,不受热更新影响。

-- 在其他模块中
local add_func = require("module").add

-- 热更新后,add_func 仍然指向旧函数?

add_func(1, 2)  -- 返回 3 ✅ 仍然使用旧函数!

这是一个陷阱! 如果在热更新前获取了函数引用,热更新后该引用仍然指向旧函数。

-- hotfix.lua:38-47
local g_mata = {
    __index = g_tb,
    __newindex = function(t, k, v)
        local oldv = rawget(g_tb, k)
        if v ~= oldv then
            log.warn_fmt("hotfix can`t change global k[%s] newv[%s] oldv[%s]", k, v, oldv)
        end
        return oldv  -- 阻止修改全局变量
    end,
}

框架主动保护全局变量,防止意外修改。

为啥热更需要退出旧服务,创建新服务的方式来实现呢?

热更实现思路

游戏开发基础设施

协议支持 Protobuf、JSON、Sproto 多协议

如何实现一个业务模块

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐