handle 作为服务的句柄,在 skynet 的消息发送和消息处理中都起到了不可或缺的作用。handle 的管理模块主要提供了 “通过 handle 查询服务地址” 和 “通过服务名字查询 handle” 这两个功能。本篇来讨论一下 skynet 是如何对 handle 进行管理的。
管理器结构
|
|
每个 skynet 进程都有一个全局的 handle 管理器,会在进程启动时被初始化。它是个带有读写锁的结构,因为大部分的操作都是查询操作,所以读写锁保证了查询的效率。保存 handle的哈希→服务 键值对的 slot 是一个动态数组,不够了会扩容,初始长度是 4,每次扩容会在原长度上翻倍。name 用来保存全部的 handle 和名字的对应关系,是一个有序的动态数组,按 handle_name 的 name 字段的字符顺序进行排序,初始长度是 2,每次扩容在原长度上翻倍。
通过 handle 查询服务地址
注册 handle
想要查询首先要注册数据。服务在创建的时候会调用 skynet_handle_register 为服务注册一个 handle 并且插入到 slot 中。
|
|
插入数据的部分值得讲一讲,slot 的 key 是通过 handle 对 slot_size 取余数的来的。handle 有一个 handle_index 做标记量,是从 1 开始的。如果不考虑服务退出的话,其实 handle 就一直累加就好了,也不需要取余数了,不够了就扩容。实际在服务退出的时候,handle 不会回收,但是 slot 中占据的位置要清掉。这就造成了 slot 的空洞问题,插入数据就变得麻烦了起来,因为要找到空洞安放数据。
- 以写模式锁住读写锁
- 把 handle 赋值为 handle_index 的值
- 用 handle 对 slot_size 取余,拿到一个位置
- 如果这个位置是空的,则找到了合适的位置,保存在 slot 中,解开读写锁,返回 handle 即可
- 如果这个位置不为空,则累加 handle,再检查,一直累加整个 slot_size 的数值,此时所有 slot 的位置都被检查了一遍了
- 如果还没找到,那就要扩容了,把 slot 扩容到原先的两倍,然后重复遍历检查的步骤
这里有两个小限制,首先 slot_size 不能超过 16777215,还有就是当 handle 超过 16777215 以后,handle 会从 1 再次开始,理论上来说有重复的风险。
释放 handle
skynet 会在服务退出后移除掉 slot 中对应的项。在 slot 中形成空洞,这也是为什么在插入的时候要遍历整个 slot 找空位置的原因。
查询
可以通过 handle 直接查询到服务的地址。
|
|
有了上面的注册以后,这个查询就很简单了,算出 handle 的哈希,直接从 slot 里面取出来服务的地址返回即可。因为不涉及到修改,所以查询只锁读锁就行了。
通过服务名字查询 handle
注册名字
可以通过调用 skynet.name 来注册一个指定服务的名字。该接口最终会调用到 C 层的
|
|
这个函数会锁上管理器的写锁,并且把自己的名字和 handle 插入到管理器的 name 数组中去。因为 name 是一个有序的动态数组,所以在插入的时候同样需要维持顺序。实际实现是通过二分查找搜索目标的名字,如果查到了,则说明名字重复了,返回了 NULL,不会覆盖旧名字。如果没有查到,则结果位置就是合适的插入位置,然后把名字和 handle 组合一下插入到 name 的合适位置中去。插入过程有可能引起数组的扩容,每次把容量扩充到当前容量的两倍。
注册名字是如果是本地名字则应该在前面加上一个点号,类似 “.xxx” 来区别本地名字和 harbor 的全局名字。本地名字没限制,harbor 的服务名字不能超过 16 个字符。
查询
因为 name 是个有序的数组,所以查询这一步同样使用二分查找搜索数组即可。因为不涉及到修改,所以只锁上读锁即可。