skynet 的网关模块的一点修改
skynet 有一个叫做 gate 的模块,用来解决外部连接数据读取的问题。它最初是用我随手为 ringbuffer 示例 而写的一段代码改造成的。
最初我认为,用 epoll 去处理读事件足够了。至于写数据,完全可以用阻塞写的方式进行。因为 skynet 可以将事务分到多线程中,所以特定几个 socket 发生阻塞,也并不会把系统阻塞住。也可以简单的理解为,单线程读,多线程写。
随着我们的游戏的开发,这样做的弊端逐渐显露出来。大量玩家聚集的场景里,广播数据会突然同时塞满多条连接。skynet 的工作线程数据固定,这样就有可能因为同时写数据而阻塞住所有的工作线程。
这个问题发现有一段时间了。当时蜗牛同学顺手改了一下,把外部连接设为非阻塞状态,并调大了缓冲区。一旦发生缓冲区慢的情况,就关闭连接。
这个改法并不彻底,而且在关闭连接后未能发送消息通知 agent ,导致业务逻辑不正确。上周仔细考虑了一下,决定把当初偷懒而没有完成的代码重新实现一下。
一开始,我在发送数据的模块中增加了写缓冲区。一旦系统缓冲区满,就放进应用层的缓冲区。等下次发起写请求时,再将以前没有发生完的数据发完。对于网络游戏来说,这样做基本够了。但隐患是,有可能最后一小片数据永远没有发送出去。(在 MMORPG 中,这种可能性几乎为零,即使发生,对用户也没有太坏的影响)
为了堵住这点纰漏,我设置了超时继续的 timer 。一旦应用层缓冲区有数据,timer 会在一定时间后处理它。
但这个做法引起了晓靖同学的不满。他觉得不统一用 epoll 来解决大量写事件而用 timer 这种事情是不可接受的。OK ,我也认同这点,但我这不是不想去动已经跑的好好的代码么?
周末还是忍不住动手了。
一开始,我想另外写一个模块整合所有的写事件。不过,读写模块分开有个问题:当用户想关闭 socket 时,它无法知道应用层写缓冲区里是否还有没发完的数据。对于 MMORPG 应用来说,有数据没发完就断开连接也不是啥大问题,不过对于 skynet 这个层次的东西来说,这样做粗暴了点。
我也想过另一个方案:重新做一个模块,利用 epoll 监听读写事件,并把事件发送给需要的服务,而自己并不真正读写数据。这样可以完美的把系统的 socket 读写 api 转换为 skynet 友好的接口。这样做的坏处是,需要更大的内存缓冲区(因为每个连接独立了);对现有代码改动较大;多了一个间接层,可能对性能有些影响。
我认为这可能是最佳方案,如果这样做,还可以把原来 skynet 中别的 IO 操作的部分也整合在一起。但最后我还是放弃了。
昨天,我决定把所有的 socket 写操作都统一交给 gate 模块处理,并保留原有分开处理的方式做兼容(等新代码跑一段时间,再去掉旧代码)。以前的发送模块继续保留,为一个连接设立一个服务负责发送数据到客户端。但它们不再直接写 socket ,而是将数据打包转发给 gate 。
这样修改后数据的处理流会多一次复制的环节,但考虑到日后还需要对数据加密和压缩,这个环节倒不算冗余。
代码写好后,已经提到到 github 。kqueue 的部分暂时还没有环境测试,希望有测试环境的同学可以帮我看看 :)