在 Skynet 框架中,服务(Service) 是最基本的运行单元。理解服务模型是掌握 Skynet 的关键。本教程将深入讲解 Skynet 服务的生命周期、服务类型、创建与管理方式,以及服务间的依赖关系。
服务的本质
什么是 Skynet 服务
Skynet 服务是一个运行在独立 Lua 虚拟机中的逻辑单元。每个服务都有:
- 独立的 Lua 虚拟机:服务之间不共享内存,避免数据竞争
- 独立的消息队列:每个服务有自己的消息队列,消息按顺序处理
- 唯一的服务地址:用于标识和定位服务
- 独立的状态:服务维护自己的内部状态
-- 服务的基本结构
local skynet = require "skynet"
-- 服务私有状态
local state = {
count = 0,
name = "my_service"
}
skynet.start(function()
-- 服务初始化
skynet.error("服务启动")
-- 注册消息处理器
skynet.dispatch("lua", function(session, source, cmd, ...)
-- 处理消息
end)
end)
服务 vs 线程
很多初学者会混淆服务和线程的概念。让我们明确它们的区别:
| 特性 | Skynet 服务 | 操作系统线程 |
|---|---|---|
| 内存模型 | 独立内存(Lua 虚拟机) | 共享内存 |
| 通信方式 | 消息传递 | 共享变量 + 锁 |
| 并发安全 | 天然安全(单线程执行) | 需要同步机制 |
| 创建开销 | 轻量(KB 级) | 重量(MB 级) |
| 调度方式 | Skynet 调度器 | 操作系统调度器 |
-- 错误理解:服务内部使用多线程
-- 这是错误的!服务内部是单线程的
-- 正确理解:服务 = 独立 Actor
-- 多个服务并发执行,每个服务内部顺序执行
服务生命周期
Skynet 服务有完整的生命周期,包括创建、初始化、运行、退出四个阶段。
1. 服务创建
服务通过 skynet.newservice 创建:
-- 创建服务的方式
local skynet = require "skynet"
skynet.start(function()
-- 方式 1:创建普通服务
local service1 = skynet.newservice("my_service")
-- 方式 2:创建服务并传递参数
local service2 = skynet.newservice("my_service", "arg1", "arg2")
-- 方式 3:创建唯一服务(全局单例)
local service3 = skynet.uniqueservice("singleton_service")
-- 方式 4:创建全局服务(集群可见)
local service4 = skynet.globalservice("global_service")
end)
2. 服务初始化
服务创建后,首先执行 skynet.start 中定义的初始化函数:
local skynet = require "skynet"
-- 接收启动参数
local arg1 = ...
local arg2 = select(2, ...)
skynet.start(function()
-- 初始化逻辑
skynet.error("初始化服务,参数:", arg1, arg2)
-- 加载配置
local config = load_config()
-- 连接数据库
connect_database()
-- 注册消息处理器
skynet.dispatch("lua", message_handler)
-- 启动定时器
skynet.fork(function()
while true do
skynet.sleep(100) -- 每秒执行一次
do_periodic_task()
end
end)
end)
3. 服务运行
初始化完成后,服务进入运行状态,等待并处理消息:
local skynet = require "skynet"
local CMD = {}
function CMD.query(key)
return cache[key]
end
function CMD.update(key, value)
cache[key] = value
return true
end
skynet.start(function()
skynet.dispatch("lua", function(session, source, cmd, ...)
local f = assert(CMD[cmd], "Unknown command: " .. cmd)
if session ~= 0 then
-- call 调用,需要返回结果
skynet.ret(skynet.pack(f(...)))
else
-- send 调用,不需要返回
f(...)
end
end)
end)
4. 服务退出
服务可以通过以下方式退出:
local skynet = require "skynet"
skynet.start(function()
-- 方式 1:主动退出
skynet.exit()
-- 方式 2:处理退出消息
skynet.dispatch("lua", function(session, source, cmd, ...)
if cmd == "shutdown" then
-- 清理资源
cleanup()
skynet.exit()
end
end)
end)
-- 退出时的清理函数
skynet.register_exit_handler(function()
skynet.error("服务即将退出")
-- 保存状态
save_state()
-- 关闭连接
close_connections()
end)
生命周期图
创建 (newservice)
↓
初始化 (start)
↓
运行 (dispatch) ←──┐
↓ │
处理消息 ──────────┘
↓
退出 (exit)
↓
销毁
服务类型
Skynet 提供了多种内置服务类型,每种类型有不同的用途。
snlua 服务
snlua 是最常用的服务类型,运行 Lua 脚本:
-- 创建 snlua 服务
local service = skynet.newservice("my_lua_service")
-- 等价于
local service = skynet.newservice("snlua", "my_lua_service")
snlua 服务的特点:
- 运行在 Lua 虚拟机中
- 支持所有 Skynet Lua API
- 适合编写业务逻辑
C 服务
C 服务用 C 语言编写,性能更高:
// 示例:logger 服务(C 实现)
#include "skynet.h"
struct logger {
FILE * handle;
int close;
};
// 服务创建函数
struct logger * logger_create(struct skynet_context * context, const char * param) {
struct logger * inst = malloc(sizeof(*inst));
inst->handle = fopen(param, "w");
inst->close = 1;
return inst;
}
// 消息处理函数
int logger_cb(struct skynet_context * context, void *ud, int type,
int session, uint32_t source, const void * msg, int sz) {
struct logger * inst = ud;
fprintf(inst->handle, "[%08x] ", source);
fwrite(msg, sz, 1, inst->handle);
fprintf(inst->handle, "\n");
fflush(inst->handle);
return 0;
}
C 服务的特点:
- 性能极高
- 适合底层功能(日志、网络)
- 开发难度较大
常用内置服务
logger 服务
日志服务,负责记录系统日志:
-- logger 服务自动启动,无需手动创建
-- 可以通过配置指定日志文件
logpath = "./logs"
loglevel = "debug"
-- 使用 skynet.error 输出日志
skynet.error("这是一条日志消息")
launcher 服务
服务启动器,负责创建新服务:
-- launcher 服务在系统启动时自动创建
-- skynet.newservice 实际上调用了 launcher 服务
-- 查看 launcher 服务状态
local launcher = skynet.localname(".launcher")
skynet.error("Launcher 服务地址:", launcher)
gate 服务
网关服务,负责管理客户端连接:
-- 创建网关服务
local gate = skynet.newservice("gate")
-- 配置网关
skynet.call(gate, "lua", "open", {
address = "0.0.0.0",
port = 8888,
maxclient = 1024,
nodelay = true,
})
-- 处理客户端连接
skynet.dispatch("lua", function(session, source, cmd, ...)
if cmd == "socket" then
local command, id, addr = ...
if command == "open" then
skynet.error("客户端连接:", id, addr)
-- 为该客户端创建代理服务
local agent = skynet.newservice("agent", id)
skynet.call(gate, "lua", "forward", id, agent)
end
end
end)
datacenter 服务
数据中心服务,提供全局数据共享:
-- 写入数据
skynet.call(".datacenter", "lua", "set", "config", "max_players", 100)
-- 读取数据
local max_players = skynet.call(".datacenter", "lua", "get", "config", "max_players")
skynet.error("最大玩家数:", max_players)
service_mgr 服务
服务管理器,负责管理唯一服务:
-- service_mgr 在系统启动时自动创建
-- skynet.uniqueservice 实际上调用了 service_mgr 服务
-- 创建唯一服务
local service = skynet.uniqueservice("singleton")
-- 再次调用会返回同一个服务
local service2 = skynet.uniqueservice("singleton")
assert(service == service2)
服务创建方式对比
newservice vs uniqueservice vs globalservice
local skynet = require "skynet"
skynet.start(function()
-- newservice:每次创建新实例
local s1 = skynet.newservice("counter")
local s2 = skynet.newservice("counter")
assert(s1 ~= s2) -- 两个不同的服务实例
-- uniqueservice:全局唯一实例
local u1 = skynet.uniqueservice("singleton")
local u2 = skynet.uniqueservice("singleton")
assert(u1 == u2) -- 同一个服务实例
-- globalservice:集群全局唯一
local g1 = skynet.globalservice("global")
local g2 = skynet.globalservice("global")
assert(g1 == g2) -- 同一个服务实例(跨节点)
end)
使用场景
-- newservice:适合需要多实例的服务
-- 例如:玩家代理服务、房间服务
local agents = {}
for i = 1, 100 do
agents[i] = skynet.newservice("agent", i)
end
-- uniqueservice:适合全局单例服务
-- 例如:配置管理器、全局计时器
local config_mgr = skynet.uniqueservice("config_manager")
-- globalservice:适合集群共享服务
-- 例如:跨服聊天、全局排行榜
local global_chat = skynet.globalservice("chat_server")
服务地址
每个 Skynet 服务都有唯一的地址,用于标识和通信。
地址格式
服务地址是一个 32 位无符号整数,通常以十六进制表示:
local skynet = require "skynet"
skynet.start(function()
-- 获取当前服务地址
local self_addr = skynet.self()
skynet.error("当前服务地址:", string.format(":%08x", self_addr))
-- 输出类似::01000003
-- 获取其他服务地址
local service = skynet.newservice("my_service")
skynet.error("新服务地址:", string.format(":%08x", service))
end)
地址组成
服务地址由两部分组成:
高 8 位:节点 ID(集群中使用)
低 24 位:服务 ID(本节点内唯一)
例如::01000003
01 = 节点 ID
000003 = 服务 ID
服务别名
可以为服务设置别名,方便访问:
local skynet = require "skynet"
skynet.start(function()
-- 注册别名
skynet.register(".my_service")
-- 其他服务可以通过别名访问
local service = skynet.localname(".my_service")
skynet.call(service, "lua", "hello")
end)
-- 在另一个服务中
local service = skynet.localname(".my_service")
if service then
skynet.send(service, "lua", "message")
end
服务间通信
skynet.send
发送消息,不等待响应:
local skynet = require "skynet"
skynet.start(function()
local target = skynet.newservice("target_service")
-- 发送消息(异步)
skynet.send(target, "lua", "hello", "world")
-- 继续执行,不等待响应
skynet.error("消息已发送")
end)
skynet.call
发送消息并等待响应:
local skynet = require "skynet"
skynet.start(function()
local target = skynet.newservice("target_service")
-- 调用并等待响应(同步)
local response = skynet.call(target, "lua", "query", "data")
-- 处理响应
skynet.error("收到响应:", response)
end)
消息类型
Skynet 支持多种消息类型:
local skynet = require "skynet"
skynet.start(function()
local target = skynet.newservice("target_service")
-- lua 消息:Lua 数据
skynet.send(target, "lua", "command", arg1, arg2)
-- text 消息:纯文本
skynet.send(target, "text", "hello world")
-- client 消息:客户端数据(通常来自网关)
skynet.send(target, "client", binary_data)
-- system 消息:系统消息(内部使用)
-- skynet.send(target, "system", ...) -- 一般不使用
end)
消息处理
local skynet = require "skynet"
skynet.start(function()
-- 处理 lua 消息
skynet.dispatch("lua", function(session, source, cmd, ...)
skynet.error("Lua 消息:", cmd, ...)
if session ~= 0 then
skynet.ret(skynet.pack("response"))
end
end)
-- 处理 text 消息
skynet.dispatch("text", function(session, source, msg)
skynet.error("Text 消息:", msg)
end)
-- 处理 client 消息
skynet.dispatch("client", function(session, source, data)
skynet.error("Client 消息:", #data, "bytes")
end)
end)
服务状态管理
服务监控
local skynet = require "skynet"
skynet.start(function()
-- 获取服务信息
local info = skynet.call(".launcher", "lua", "info")
for addr, data in pairs(info) do
skynet.error(string.format("服务 %s: %d 消息, %d 字节内存",
addr, data.message, data.memory))
end
-- 获取服务列表
local services = skynet.call(".launcher", "lua", "list")
for addr, name in pairs(services) do
skynet.error(string.format("%s: %s", addr, name))
end
end)
服务热更新
local skynet = require "skynet"
skynet.start(function()
-- 方式 1:重新加载服务代码
skynet.call(service, "lua", "reload")
-- 方式 2:创建新服务替换旧服务
local new_service = skynet.newservice("my_service_v2")
-- 迁移状态
local state = skynet.call(old_service, "lua", "get_state")
skynet.call(new_service, "lua", "set_state", state)
-- 停止旧服务
skynet.send(old_service, "lua", "shutdown")
end)
实战:构建服务管理系统
让我们构建一个完整的服务管理示例:
主服务
-- service/main.lua
local skynet = require "skynet"
skynet.start(function()
skynet.error("=== Skynet 服务管理示例 ===")
-- 创建配置服务
local config = skynet.uniqueservice("config_manager")
skynet.call(config, "lua", "load", "config.json")
-- 创建多个工作服务
local workers = {}
for i = 1, 5 do
workers[i] = skynet.newservice("worker", i)
skynet.error("创建工作服务", i)
end
-- 分发任务
for i = 1, 10 do
local worker = workers[(i % 5) + 1]
skynet.send(worker, "lua", "process", "task_" .. i)
end
-- 等待一段时间
skynet.sleep(500)
-- 收集结果
for i, worker in ipairs(workers) do
local result = skynet.call(worker, "lua", "get_result")
skynet.error("工作服务", i, "结果:", result)
end
-- 关闭所有工作服务
for _, worker in ipairs(workers) do
skynet.send(worker, "lua", "shutdown")
end
skynet.error("=== 示例结束 ===")
skynet.exit()
end)
配置管理服务
-- service/config_manager.lua
local skynet = require "skynet"
local cjson = require "cjson"
local config = {}
local CMD = {}
function CMD.load(filename)
local file = io.open(filename, "r")
if file then
local content = file:read("*all")
file:close()
config = cjson.decode(content)
skynet.error("配置已加载:", filename)
return true
end
return false
end
function CMD.get(key)
return config[key]
end
function CMD.set(key, value)
config[key] = value
return true
end
skynet.start(function()
skynet.dispatch("lua", function(session, source, cmd, ...)
local f = assert(CMD[cmd])
if session ~= 0 then
skynet.ret(skynet.pack(f(...)))
else
f(...)
end
end)
end)
工作服务
-- service/worker.lua
local skynet = require "skynet"
local worker_id = ...
local results = {}
local CMD = {}
function CMD.process(task)
skynet.error(string.format("工作服务 %s 处理任务: %s", worker_id, task))
-- 模拟处理
skynet.sleep(100)
results[#results + 1] = task .. "_done"
end
function CMD.get_result()
local result = table.concat(results, ", ")
results = {}
return result
end
function CMD.shutdown()
skynet.error(string.format("工作服务 %s 关闭", worker_id))
skynet.exit()
end
skynet.start(function()
skynet.error(string.format("工作服务 %s 启动", worker_id))
skynet.dispatch("lua", function(session, source, cmd, ...)
local f = assert(CMD[cmd])
if session ~= 0 then
skynet.ret(skynet.pack(f(...)))
else
f(...)
end
end)
end)
最佳实践
1. 服务粒度
-- 不好:服务过大,职责不清晰
-- service/monolith.lua
local CMD = {}
function CMD.handle_login() ... end
function CMD.handle_chat() ... end
function CMD.handle_battle() ... end
function CMD.handle_payment() ... end
-- 好:服务职责单一
-- service/login.lua
-- service/chat.lua
-- service/battle.lua
-- service/payment.lua
2. 错误处理
local skynet = require "skynet"
local CMD = {}
function CMD.safe_command(...)
local ok, result = pcall(function()
-- 可能抛出异常的代码
return do_something(...)
end)
if not ok then
skynet.error("命令执行失败:", result)
return {error = result}
end
return {success = true, data = result}
end
skynet.start(function()
skynet.dispatch("lua", function(session, source, cmd, ...)
local f = CMD[cmd]
if not f then
if session ~= 0 then
skynet.ret(skynet.pack({error = "unknown command"}))
end
return
end
local ok, result = pcall(f, ...)
if session ~= 0 then
if ok then
skynet.ret(skynet.pack(result))
else
skynet.ret(skynet.pack({error = result}))
end
end
end)
end)
3. 资源管理
local skynet = require "skynet"
local resources = {}
skynet.start(function()
-- 初始化资源
resources.db = connect_database()
resources.cache = connect_redis()
skynet.dispatch("lua", function(session, source, cmd, ...)
-- 处理消息
end)
end)
-- 注册退出清理
skynet.register_exit_handler(function()
-- 释放资源
if resources.db then
resources.db:close()
end
if resources.cache then
resources.cache:close()
end
end)
总结
Skynet 的服务模型是整个框架的核心。通过本教程,你应该掌握了:
- 服务的本质:独立 Lua 虚拟机 + 消息队列
- 生命周期:创建 → 初始化 → 运行 → 退出
- 服务类型:snlua、C 服务、内置服务
- 创建方式:newservice、uniqueservice、globalservice
- 通信机制:send、call、消息类型
- 最佳实践:服务粒度、错误处理、资源管理
在下一节教程中,我们将深入学习 Skynet 的消息传递机制,了解消息队列、消息序列化和异步 IO 的实现原理。
参考资料
- Skynet 服务模型文档:https://github.com/cloudwu/skynet/wiki/Service
- Skynet 源码分析:service_snlua.c
- Actor 模型理论:Carl Hewitt 的论文
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。