skynet.dispatch与skynet.register_protocol
- 源码:
- skynet.register_protocol(class)
- [skynet.dispatch(typename, func)](#skynet.dispatch(typename, func))
- 协同工作流程:一个完整的例子
- 一些总结
这两个方法是 Skynet 服务间消息传递和处理机制的基石,理解它们,就理解了 skynet的"神经系统"是如何工作的,这两个方法共同构筑了 Skynet 服务间通信的桥梁:register_protocol定义了桥的材质和结构(如何打包/解包数据),而 dispatch 则定义了桥另一端的目的地(如何处理解包后的数据)
源码:
lua
function skynet.dispatch(typename, func)
local p = proto[typename]
if func then
local ret = p.dispatch
p.dispatch = func
return ret
else
return p and p.dispatch
end
end
lua
local proto = {}
local skynet = {
-- read skynet.h
PTYPE_TEXT = 0,
PTYPE_RESPONSE = 1,
PTYPE_MULTICAST = 2,
PTYPE_CLIENT = 3,
PTYPE_SYSTEM = 4,
PTYPE_HARBOR = 5,
PTYPE_SOCKET = 6,
PTYPE_ERROR = 7,
PTYPE_QUEUE = 8, -- used in deprecated mqueue, use skynet.queue instead
PTYPE_DEBUG = 9,
PTYPE_LUA = 10,
PTYPE_SNAX = 11,
PTYPE_TRACE = 12, -- use for debug trace
}
-- code cache
skynet.cache = require "skynet.codecache"
skynet._proto = proto
function skynet.register_protocol(class)
local name = class.name
local id = class.id
assert(proto[name] == nil and proto[id] == nil)
assert(type(name) == "string" and type(id) == "number" and id >=0 and id <=255)
proto[name] = class
proto[id] = class
end
skynet.register_protocol(class)
这个方法的作用是向 skynet框架注册一种新的消息协议 ,所谓"协议",在这里的定义远不止于网络协议,它更广泛地指代一种消息的格式、编码方式以及默认的解包方法
它接受一个 class 表,这个表必须包含以下字段:
- name: string 类型,协议的唯一名称,例如 "lua", "text", "client"。这是一个便于阅读的标识符
- id: number 类型,协议的唯一 ID,范围必须是 0-255。这是消息在网络上或服务间传输时使用的数字标识符,非常紧凑。框架已经预定义了许多 ID(如 skynet.PTYPE_LUA = 10)
- pack: function 类型(可选),消息的编码函数,它接受一个消息(通常是 Lua 对象),将其序列化为一个字符串(可能包含 C 指针和长度,在 Lua 中表现为 lightuserdata + length),以便通过网络发送或放入消息队列,例如,对于 "lua" 协议,它使用 skynet.pack 来序列化 Lua 值
- unpack: function 类型(可选),消息的解码函数,它接受一个由 pack 生成的字符串(或 lightuserdata + length),将其还原成 Lua 能够处理的对象,对于 "lua" 协议,它使用 skynet.unpack
- dispatch: function 类型(可选),该协议的默认消息分发函数,这是一个非常重要的回调函数,当服务收到一条此类型的消息,但又没有通过 skynet.dispatch 为其注册自定义处理函数时,就会调用这个默认的 dispatch 函数
工作流程与内部机制
- 注册存储:当调用 skynet.register_protocol(class) 时,框架会将这个 class 表分别以 name 和 id 为键,存储到内部的 proto 表中(即代码开头的 local proto = {})
- proto["lua"] = class
- proto[10] = class
- 消息处理基础:当 skynet 的核心 C 模块从网络或消息队列中接收到一个消息包时,它首先会读取包头的协议 ID,然后根据这个 ID 在 proto 表中找到对应的协议类 class
- 解包:接着,核心模块会调用该协议类的 unpack 函数,将消息包体(一串二进制数据)解包成 Lua 值。如果没有提供 unpack,则消息会以原始字符串形式传递
- 分发:最后,核心模块会尝试将解包后的消息分发给相应的处理逻辑,这时,class.dispatch 或通过 skynet.dispatch 注册的函数就开始起作用了
使用场景
- 扩展框架:如果想定义一种全新的消息类型(例如,使用 Protobuf 进行序列化),就需要注册一个自定义协议
lua
local sprotoloader = require "sprotoparser"
local proto = sprotoloader.parse(...)
skynet.register_protocol {
name = "my_protobuf",
id = 128, -- 选择一个未被占用的 ID
pack = proto.pack, -- 你的 protobuf 打包函数
unpack = proto.unpack, -- 你的 protobuf 解包函数
}
- 使用内置协议:绝大多数情况下,不需要自己调用这个函数。skynet 在启动时已经为你注册好了所有内置协议(如 text, lua, client 等)
skynet.dispatch(typename, func)
这个方法的作用是为当前服务注册一个针对特定协议类型的消息处理函数,它建立了这样一种映射:"当本服务收到类型为 typename的消息时,请调用函数 func 来处理它"
- typename: string 类型,协议的名称,如 "lua",必须是一个已经通过skynet.register_protocol 注册过的协议名
- func: function 类型,消息处理函数,其签名通常为 function(session, source, ...)。
处理函数 (func) 的参数详解
处理函数是消息处理的核心,它的参数由框架自动传入:
- session: number 类型,会话号
- 如果为 0:表示这是一个请求,不需要返还,对应send
- 如果为正整数:表示这是一个请求,并且对方期望一个回复,这个数字是本次请求的唯一标识,需要用这个 session 来回应对方,对应call
- 如果为负数:表示这是一个响应,这是对方对你之前发出的某个请求(其会话号为 -session)的回复,通常这种情况会被 skynet 的协程机制内部处理,你在自己的业务处理函数中很少直接见到负数的 session
- source: number 类型,发送方服务的地址(一个 32bit 的整数句柄),可以通过skynet.response() 或 skynet.send() 等 API 向这个地址发送消息
- ... (可变参数): 任何值,这是消息经过协议类的 unpack 函数解码后的内容,对于最常见的 "lua" 协议,这就是调用 skynet.send 时传递的参数
工作流程与内部机制
- 注册存储:skynet.dispatch("lua", my_func) 会在当前服务的内部数据结构中,将协议名 "lua" 映射到处理函数 my_func
- 覆盖默认 :这个操作会覆盖该协议类本身提供的默认 dispatch 函数
- 消息触发:当一条 "lua" 协议的消息到达当前服务时,框架会:
- 用 "lua" 协议的 unpack 函数解包消息
- 查表找到通过 skynet.dispatch 注册的 my_func
- 最终调用 my_func(session, source, ...)
使用场景
这是每个 skynet 服务都必须做的事情,你需要在服务启动的入口函数(通常是被 skynet.start 包裹的函数)中,处理协议类型注册的处理函数
lua
local skynet = require "skynet"
-- 服务的启动函数
skynet.start(function()
-- 注册对 "lua" 类型消息的处理函数
skynet.dispatch("lua", function(session, source, cmd, ...)
-- 消息被解包后,第一个参数我们约定为命令名 'cmd'
local f = assert(CMD[cmd], "Unknown command: " .. tostring(cmd))
-- 调用对应的命令处理函数
f(...)
end)
-- 可能还会注册其他协议的处理
-- skynet.dispatch("client", client_message_handler)
end)
协同工作流程:一个完整的例子
场景:服务 A (地址 :0x100) 向服务 B (地址 :0x200) 发送一个请求,调用其 ping 方法
- 发送方 (Service A):
lua
-- Service A
skynet.send(0x200, "lua", "ping", "hello")
- skynet.send 内部会查找 "lua" 协议(因为第三个参数是 "lua")
- 使用 "lua" 协议的 pack 函数(即 skynet.pack)将参数 "ping", "hello" 序列化成一个字符串
- 将目标地址 (0x200)、协议 ID (10)、序列化后的数据等组装成 Skynet 内部消息,投入消息队列
- 传输层:
- Skynet 的核心调度器从队列中取出这个消息
- 发现目标地址是服务 B (0x200),将消息投递到服务 B 的消息队列中
- 接收方 (Service B):
- 服务 B 的协程被调度器唤醒,处理这条新消息
- 核心层读取消息头中的协议 ID (10)
- 根据 ID=10 找到协议类 proto[10](即 "lua" 协议)
- 调用 "lua" 协议的 unpack 函数(即 skynet.unpack),将二进制数据还原成 Lua 值: "ping", "hello"
- 核心层检查服务 B 是否通过 skynet.dispatch 为 "lua" 协议注册了自定义处理函数,假设它注册了
- 于是,核心层调用服务 B 注册的那个处理函数:
-- 这是服务B当初注册的函数
lua
function(session, source, cmd, ...)
-- session = 12345 (一个由框架生成的正整数,因为skynet.send默认生成一个session)
-- source = 0x100 (服务A的地址)
-- cmd = "ping", ... = "hello"
local f = CMD.ping
f(...) -- 相当于 CMD.ping("hello")
end
- CMD.ping 函数执行它的逻辑
- 如果没有自定义 dispatch:
如果服务 B 没有调用 skynet.dispatch("lua", ...),那么框架就会fallback到调用 "lua" 协议类本身的默认 dispatch 函数,这个默认函数的行为可能是记录一个错误或忽略该消息
一些总结
1. 分离关注点:
- register_protocol 关心的是 "消息是什么"------如何编码、解码,这是全局的、通用的定义
- dispatch 关心的是 "收到消息后怎么办" ------业务逻辑,这是每个服务私有的、特定的行为
这种设计极大地提高了灵活性,可以定义一种通用的序列化格式(如 Protobuf),然后不同的服务可以根据自己的需求用不同的方式处理同一种格式的消息
2. 协议与处理的解耦:
消息的格式和处理逻辑被完全分开了,网络层或消息队列只负责传递二进制数据。它不需要理解数据的内容,只需要根据协议 ID 找到正确的解包方式即可,解包后的数据才交给上层的业务逻辑处理,这是一种清晰的分层架构
3. 性能优化:
- 使用数字 ID 而非字符串名称在消息头中标识协议,极大地减少了传输开销
- pack/unpack 的机制允许使用高效的序列化方案(如 "lua" 协议使用的自定义序列化器),避免了在 C 层和 Lua 层之间传递不必要的临时对象,提升了性能
4. 默认行为与自定义行为:
协议类自带一个默认的 dispatch 函数,这提供了一个安全网,但更重要的是,它允许框架为某些特殊协议提供内置的、高效的处理逻辑。最典型的例子是 PTYPE_RESPONSE(响应消息),它的默认 dispatch 函数与 Skynet 的协程调度机制紧密合作,用于唤醒正在等待响应的协程。对于这种消息,通常不需要也不应该自己去注册 dispatch 函数