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", ...)
问题场景:
- 服务扩容:新增服务实例时,调用方无法自动感知
- 服务缩容:服务实例下线时,调用方可能调用到已关闭的服务
- 热更新:服务更新重启后,旧地址失效,新地址需要被感知
- 故障恢复:服务崩溃重启后,地址可能变化
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 多协议
如何实现一个业务模块
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)