文前导读
skynet 是一个由云风所写的轻量级在线游戏服务器框架。本文为 skynet 框架源码剖析系列的第七篇文章,探讨了 skynet 框架下,同一 skynet 节点内不同的lua 服务之间是如何通过消息来进行交互,主要包含了以下内容:
- lua 服务的消息协议
- lua 服务如何注册自己的消息及对应的回调函数
- lua 服务是如何接受消息的?
- lua 服务是如何发送消息的?
lua 服务的消息协议
skynet 使用 proto 来描述不同的消息协议。在最开始的时候,proto 是一个空表,需要由 skynet.register_protocol
进行消息协议的注册。skynet 在启动 lua 服务的初期会默认注册 lua,response 以及 error 类型的消息协议,这个过程通常在 require "skynet"
语句中执行。skynet.register_protocol
函数如下:
1 | function skynet.register_protocol(class) |
从 skynet 默认注册的消息类型来推断,我们知道一个消息协议应当包含有以下的一些字段:
- name:表明了该消息协议的类型名称
- id:表明该消息协议的类型编号,包括了以下几种不同的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16local 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, --lua 服务类型的消息
PTYPE_SNAX = 11,
PTYPE_TRACE = 12, -- use for debug trace
} - pack:发送消息时所用到的打包函数
- unpack:接收消息时调用的解包函数
- dispatch:由消息提供方指定对应类型消息的处理函数,如果没有指定,则最终会调用
skynet.dispatch(typename, func)
函数来处理
说完基本的消息协议,我们来看看 skynet 定义的三种不同类型的消息都有什么作用:
- lua 型消息:采用 skynet.pack 和 skynet.unpack 进行消息的打包和解包, 默认调用
skynet.dispatch(typename, func)
进行消息的派发- response 型消息:response 消息主要用于处理skynet.call调用和定时器的返回。当源服务向目的服务发送请求,会附带一个 session,目的服务在处理完请求后,会将 session 加入 response 消息中一起通过
skynet.ret
返回给源服务- error 型消息:当调用
skynet.call
发送错误消息时,源服务可以接收到一个 error 类型的消息
lua 服务如何注册自己的消息及对应的回调函数
讲完了 lua 服务的消息服务的定义,我们以 example/simplemonitor.lua 中的服务来说明一下,lua 服务之间是如何相互收发信息的。而在这之前,我们需要看看 simplemonitor.lua 定义:
1 | local skynet = require "skynet" |
如以往的文章所提到的那样,当使用 skynet.newservice
函数启动一个新的 lua 服务时,会执行相应的脚本来完成服务的初始化。在 simplemonitor.lua 脚本中,先执行了 require "skynet"
,这不仅会将相应的函数导入到当前 lua 脚本当中,还会执行 skynet.register_protocol
为 simplemonitor 注册三种默认消息协议。随后,simplemonitor.lua 又调用了 skynet.register_protocol
注册了一个 client 类型的 lua 消息协议,并指定了对应的 dispatch 函数。随后调用 skynet.start
来启动 simplemonitor 服务。在上一篇文章《skynet 源码阅读笔记 —— 如何在 lua 服务中启动另一个 lua 服务》 中提到了 skynet.start
会将 simplemonitor 服务的消息回调函数设置为 skynet.dispatch_message
,然后执行 skynet.dipatch("lua", monitor)
进行服务的初始化。
lua 服务是如何接受消息的?
讨论完 lua 服务是如何注册自己的消息类型及定义消息对应的回调函数后,我们来看看 lua 服务是如何接受消息的。我们先来看看 skynet.dispatch
函数的实现:
1 | --skynet.lua |
从上述代码可以看出,当 simplemonitor 服务启动完毕后,对应的 lua 消息协议的 dispatch 函数实际上就是 monitor
函数。接着,我们再来看看 skynet.dispatch_message
1 | function skynet.dispatch_message(...) |
结合上述带注释的代码,我们描述一下整体的过程:当服务 A 向 simplemonitor 发送一条消息时,会将这条消息放入到 simplemonitor 对应的 snlua 服务所属的次级消息队列当中(skynet当中有多个 snlua 类型的服务,分别对应不同的 lua 服务)。worker 线程会将其取出并消费,在消费的过程当中会调用该消息所指定的 callback 函数。而 skynet.start
已经通过 c.callback(skynet.dispatch_message)
将 simplemonitor 的消息的回调函数设置为 skynet.dispatch_message
。此时,worker线程最终就会调用到 raw_dispatch_message
函数。这个函数会获得一个新的空的协程来执行消息协议中指定的 dispatch 函数。对应协程一旦执行起来完毕,会调用 coroutine_yield
函数将自身挂起,并返回挂起的原因。suspend
会根据这个原因做不同的处理
lua 服务是如何发送消息的?
讲完了当 simplemonitor 收到消息的行为,我们再来看看发送消息的行为。假设现在有一个服务 A 需要向另一个服务 B 发送一条消息,那么他需要调用 skynet.send
函数。我们来看看 skynet.send
函数的定义:
1 | function skynet.send(addr, typename, ...) |
skynet.send
会调用 c.send(addr, p.id, 0 , p.pack(...))
函数来发送消息,其中 c.send
函数的参数从左至右分别是目标地址,消息协议类型,session ID,自定义参数列表。
我们再来看看 c.send
所对应的函数 lsend
是如何实现的:
1 | //lua-skynet.c |
结合上述代码及注释,当一个 lua 服务向另一个 lua 服务发送消息时,会调用skynet.send
函数,这个函数最终会调用 C 层的 send_message
函数,通过对调用参数的解析,为消息添加上 type 和 session 字段,并最终调用 skynet_send
函数,这个函数在之前的skynet 源码阅读笔记 —— 消息调度机制说明了它的作用,这里就不多做说明。skynet_send
函数将消息压入到指定服务的次级消息队列中,发送的过程就结束了。接下来只需要等待 worker 线程从全局消息队列中取出对应的次级消息队列,并消费相应的消息即可。
- 本文作者: Phoenix
- 本文链接: http://hacker-cube.com/2020/11/04/skynet-源码阅读笔记-——-lua-服务间是如何交互的/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!