skynet 是一个 actor 模型的消息框架,每个 actor 在 skynet 中就是一个服务。服务可以说是 skynet 中最重要的概念,本篇会介绍 skynet 中的服务。
概述
skynet 中的服务是基于上文中前文中提到的 module 在创建的,module 为服务提供了私有数据的存储位置。服务作为一个 acotr,拥有自己的消息队列,可以向别的服务发送消息,也可以接收别的服务的消息并把消息交给消息回调函数进行处理。
结构
|
|
虽然 skynet_context 中的数据项很多,但是其实核心的数据就是跟消息处理有关的那几个。worker 线程会不断为消息队列中每个消息调用回调函数进行处理。
创建服务
|
|
不管是从 C 层还是从 Lua 层创建一个新的服务,最后的创建函数都是 skynet_context_new,这个函数接受一个 module 名字和一个传给 module 的 init 函数的额外参数,返回一个 struct skynet_context 变量指针。
服务在创建的时候有几个关键步骤:
- 通过 module 名字去查找 module 的地址,然后调用 module 的 create 接口创建一个数据副本,这两个值会保存在 skynet_context 的 mod 和 instance 字段中。
- 注册服务的 handle,服务的 handle 是用来完成 名字 → handle 和 handle → 服务 的映射。
- 创建服务的消息队列。
- 调用 module 的 init 函数初始化之前创建好的 module 数据副本。
- 把服务的消息队列 push 到全局的消息队列中去。
引用计数
每个服务都有一个原子类型的引用计数变量 ref,当有地方引用一个服务时,调用 skynet_context_grab 来增加服务的引用计数,在引用结束以后需要手动调用 skynet_context_release 来减少服务的引用计数。
|
|
ref 在服务创建完毕以后会的值为 1,正常的引用和解引用是不会使 ref 变为 0 的,如果需要删除一个服务,可以在服务的逻辑中调用 skynet.exit 或者是通过后台命令来减少服务的引用次数,然后等别的地方的引用全部结束了,ref 会变为 0 来触发删除服务的函数 delete_context.
|
|
之所以要进入一个引用计数而不是调用一个删除函数直接进行删除,主要原因是因为要删除的目标服务可能正在被别处引用,比如它的消息队列正在被 worker 线程处理,直接删除会有问题,所以虽然维护引用计数比较麻烦,但是其实已经是一个比较巧妙的解法了。
消息队列
|
|
每个服务都有属于自己的一个 struct message_queue 结构的消息队列。消息队列是有锁结构,实现方式是一个动态环形数组,使用 head 表示目前队列中最早的一条消息的索引值,tail 表示目前队列中最晚的一条消息的索引值。在 push 消息进来的时候会检查是否还有剩余位置,如果没有了会调用 expand_queue 扩容这个数组,容量初始的时候是 64,每次扩容会把当前容量直接翻倍。
handle
上文提到了,handle 的作用是用来完成 名字 → handle 和 handle → 服务 的映射。首先,skynet 中同进程不同服务之间发送消息的本质就是把消息 push 到目标服务的消息队列中,这个操作中最重要的一个步骤就是拿到目标服务的消息队列的地址,又因为有了目标服务的地址就能拿到其消息队列的地址,所以这个步骤变为了拿到目标服务的地址。
skynet 中发送消息的时候,指定目标可以通过服务的名字或是 handle,如果目标的参数是名字,需要两步转换,首先要根据服务的名字拿到服务的 handle,然后再根据 handle 拿到服务的地址。如果参数是 handle 则直接根据 handle 拿到服务的地址即可。
这个过程能不能简化呢?在 C 语言中很难实现,如果是 C++ 的话,可以考虑强制要求服务一定要有名字,然后使用一个 unordered_map 把服务的名字和地址对应起来,发送消息的时候也只能通过名字来指定目标服务,这样可以简化掉 handle 这个转化过程,但是也加了不小的限制。