Skynet 服务模型详解

深入理解 Skynet 的服务模型,包括服务生命周期、服务类型、服务创建与管理、服务间依赖关系等核心概念

在 Skynet 框架中,服务(Service) 是最基本的运行单元。理解服务模型是掌握 Skynet 的关键。本教程将深入讲解 Skynet 服务的生命周期、服务类型、创建与管理方式,以及服务间的依赖关系。

服务的本质

什么是 Skynet 服务

Skynet 服务是一个运行在独立 Lua 虚拟机中的逻辑单元。每个服务都有:

  1. 独立的 Lua 虚拟机:服务之间不共享内存,避免数据竞争
  2. 独立的消息队列:每个服务有自己的消息队列,消息按顺序处理
  3. 唯一的服务地址:用于标识和定位服务
  4. 独立的状态:服务维护自己的内部状态
-- 服务的基本结构
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 的服务模型是整个框架的核心。通过本教程,你应该掌握了:

  1. 服务的本质:独立 Lua 虚拟机 + 消息队列
  2. 生命周期:创建 → 初始化 → 运行 → 退出
  3. 服务类型:snlua、C 服务、内置服务
  4. 创建方式:newservice、uniqueservice、globalservice
  5. 通信机制:send、call、消息类型
  6. 最佳实践:服务粒度、错误处理、资源管理

在下一节教程中,我们将深入学习 Skynet 的消息传递机制,了解消息队列、消息序列化和异步 IO 的实现原理。

参考资料

  1. Skynet 服务模型文档:https://github.com/cloudwu/skynet/wiki/Service
  2. Skynet 源码分析:service_snlua.c
  3. Actor 模型理论:Carl Hewitt 的论文

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页