本篇自我总结
本篇主要讲了数据包的分包和重组问题, 到底数据包多大才好呢?是不是越大越好呢?包太大了怎么办呢?
请看总结, 不明之处再看文中具体讲解.
为什么需要做这个可靠UDP协议
网络协议在动作游戏类型(FPS)中的典型特征就是一个持续发送的数据包,在两个方向上以稳定的速度如20或30包每秒发送。这些数据包都包含有不可靠的无序数据例如t时间内的世界状态;所以,当一个数据包丢失,重新发送它并不是特别有用。当重新发送的数据包到达时,时间t已经过去了。
所以这就是我们将要实现可靠性的现状。对于我们90%的数据包,仅丢弃并不再重新发送它会更好。对于10%或更少(误差允许范围内)的情况,我们确实需要可靠性,但这样的数据是非常罕见的,很少被发送而且比不可靠的数据的平均大小要小得多。这个使用案例适用于所有过去十五年来发布的AAA级的FPS游戏。
应答系统是实现可靠UDP的最重要的部分
为实现数据包层级的应答,在每个包的前面添加如下的报头:
1 | struct Header |
这些报头元素组合起来以创建应答系统:
- sequence 是一个数字,随每个数据包发送而增长(并且在达到65535后回往复)。
- ack 是从另一方接收到的最新的数据包序列号。
- ack_bits 是一个位字段,它编码与ack相关的收到的数据包组合:如果位n已经设置,即 ack– n 数据包被接收了。
ack_bits 不仅是一个节省带宽的巧妙的编码,它同样也增加了信息冗余来抵御包的丢失。每个应答码要被发送32次。如果有一个包丢失了,仍然有其他31个包有着相同的应答码。从统计上来说,应答码还是非常有可能送达的。
但突发的传送数据包的丢失还是有可能发生的,所以重要的是要注意:
- 如果你收到一个数据包n的应答码,那么这个包肯定已经收到了。
- 如果你没有收到应答码,那么这个包就很有可能 没有被收到。但是…它也许会是,仅是应答码没有送达。这种情况是极其罕见的。
以我的经验,没有必要设计完善的应答机制。在一个极少丢应答码的系统上构建一个可靠性系统并不会增加什么大问题。
发送方如何追踪数据包是否已经被应答
为实现这个应答系统,我们在发送方还需要一个数据结构来追踪一个数据包是否已经被应答,这样我们就可以忽略冗余的应答(每个包会通过 ack_bits
多次应答)。我们同样在接收方也还需要一个数据结构来追踪那些已经收到的包,这样我们就可以在数据包的报头填写ack_bits
的值。
1 | const int BufferSize = 1024; |
你在这可以看到的窍门是这个滚动的缓冲区是以序列号来作为索引的:
1 | const int index = sequence % BufferSize; |
当条目被顺序添加,就像一个被发送的队列,对插入所需要做的就是把这个序列缓冲区的值更新为新的序列号并且在该索引处重写这个数据:
1 | PacketData & InsertPacketData( uint16_t sequence ) |