发送数据处理模式的概念:
相信每一个第一次写游戏服务器的人都会在发送数据处理这里卡主,因为相对于简单易处理的接收消息处理,发送消息的时机和驱动更加难以把握。为什么呢?我们看下套接字可读的条件:
1: 该套接字接收缓冲区中的数据字节数大于接收低水位标记
2: 该连接的读关闭
3: 该套接字是一个监听套接字,并且有新的连接
4: 该套接字上有错误处理
以上所有的条件,都可以通过注册事件来完成,并且因为都是被动触发,所以处理起来比较轻松。
那我们看看套接字可写的条件:
1: 该套接字发送缓冲区中可用空间大于发送高水位标记
2: 该套接字的写关闭
3: 该套接字上有错误处理
看到套接字可写的条件我们为难了,因为我们需要发送的时候,套接字并不一定可写;而套接字可写的时候,我们未必有数据要发送,这就造成了事件的浪费,也就造成了发送数据比接收数据更难。
前言: 我们在这里是将网络数据的发送和接收放在单独的线程里面处理,称之为网络IO线程;而在其他的线程总处理玩家逻辑,称之为玩家逻辑线程。
解决方案1: 定时发送。
定时发送算是一个比较通用的处理,也是用起来比较方便的方式。对于每一个来自客户端的连接,我们只是注册可读事件,而不会注册可写事件,对于发送处理,我们采用定时器触发的模式,比如每一个连接上绑定一个30ms的定时器,每次定时器触发的时候,也就是写饥饿的时候,对每个连接都去做一次试图发送的处理。
当大家看到思路的时候,我想麻烦也就跟着来了。因为是30ms的定时器,所以每条消息的延迟都是N*30ms;可能很多的套接字并没有数据要发送,但是定时器到了,造成了很多浪费;因为定时器的触发也是轮训的模式,大家不是都说“轮训就是**吗”。
解决方案2: 按需注册write事件
如果我们真正了解write事件,就应该按需注册write事件。每次发送玩家数据的时候,如果只发送了部分数据,则把剩余的数据存放到自定义缓冲区,并且注册write写事件;这个事件会在下一次select的时候触发,触发的时候发送剩余的数据,如果发送完毕,就关闭write事件。
这里我们分开网络io线程和游戏逻辑线程,如果是网络io线程内部发送数据,那么很简单,只需要调用提供send函数就可以了。如果是游戏逻辑线程里发送数据,就会稍微麻烦一点。为了保证线程的安全性以及数据的完整性,在游戏逻辑线程里面调用发送数据的接口,实际的工作是在网络线程里面完成。简单来说就是将这个函数或者task,投递到网络io线程里面去执行。
我们可以参考下muduo的做法,如果是在非网络io线程里面发送数据,就将要发送的套接字和数据封装成Functor,投递到网络io线程中去,并且唤醒网络IO线程,去处理这些需要执行的Functors(std::vector)。
我在修改自己的网络发送模式的时候,也是这个思路,不过是用的是java为多线程提供的FutureTask,如果是在非网络io线程中发送数据,就将需要发送的连接和数据封装成FutureTask,投递到网络Io线程中去,唤醒io线程,处理需要执行的FutureTasks(Vector)。
好吧,最后还是承认,其实在设计的时候,参考了一下mina的设计,不过看到muduo网络库居然和mina 的设计如此类似,相比muduo的设计也是参考了mina的设计。
可能很多人认为方案2的方案,会增加系统调用。因为我们不是总说要减少系统调用吗。这也是定时器发送数据模式的依据,可是我们的tcp已经提供了negle算法,大部分send的函数,只是将数据写入到缓冲区,并没有那么大的消耗。大家可以测试看下!