学习到的东西(因特网就是个坑)第一课:如果所有的玩家都拨打同一个电话号码,那么你就不是在测试网络,你实际上是在测试调制解调器和POP服务器,无论如何你确实不是在测试网络。如果你仔细想一下的话,就会发现这是显而易见。你的数据包通过调制解调器到达POP服务器,然后POP服务器把他们转发给其他玩家。数据包从未被POP服务器处理。 当我们最终把我们的游戏放到真实的网络环境中去,它在几秒钟之内就挂了。我们都困惑不解。它在局域网工作的如此之好,甚至能够允许500毫秒的延迟,为什么一上因特网就挂了呢。当我们进行一些检查后,发现了一些难以置信的事实,5到10秒的延迟是常见现象,并且我们发现了一些延迟甚至达到了50秒!在这种延迟的情况下,我们的游戏当然无法运行了。 甚至有一些包出现了丢失。TCP协议规定数据包一定会被接收,并且它们会被按顺序发送。TCP协议使用一个检查系统来验证数据包是否被成功发送了,并在发生丢失的时候进行重发。按顺序发送的意思是如果前一个数据包需要重发,那么后一个数据包将会被延迟发送,直到前一个数据包被接收到。但问题是,一旦因特网连接开始丢包,那么接下来的包很大概率仍然会发生丢包。这就意味着一个数据包可能需要好几秒钟才能够到达目的地。 第二课:TCP就是恶魔。不要在游戏里用TCP协议。你将会用尽你生命剩余的时间看到满载13岁少女的泰坦尼克号的悲剧一遍遍的重演。首先,TCP在等待发送下一个数据包之前不会发送任何额外的数据包。这就是为什么我们会看到有5秒延迟的包出现。其次,如果有某个数据包没有到达目的地,TCP协议不会立刻重发这个数据包。这个方式的考虑是如果一个数据包因为阻塞而发生了丢失,那么,就没有必要去重发这个数据包,因为这样只会增加阻塞。所以这个时候TCP就会停止发包,而开始发送一些临时的非常小的检查网络通畅的包。一旦这些测试包通过了,那么TCP就会重新开始发送真正的数据包。这种重发算法解释了为什么我们发现了一些延迟达50秒的数据包。 第三课:使用UDP。应对TCP这个恶魔的办法看起来也非常简单。不用TCP,用UDP代替就可以了。不像TCP,UDP是一个不可信的协议。它不做任何事情来保证数据包会被接收到,并且不关心数据包是否按照顺序发送。换句话说,它什么都不做。所以如果你非常需要发送一个数据包,你就需要自己去控制重发和检查机制。关于UDP还有一件烦恼的事。调制解调器的连接使用一种叫做PPP的协议进行连接。当你使用TCP协议通过PPP协议的时候,PPP协议会非常聪明的压缩数据包的因特网包头内容,使它从22字节减少到3字节(甚至更小)。当你发送UDP包通过PPP连接的时候,它不会像TCP那样进行聪明的压缩,而直接发送22字节的包头。这样,你使用UDP的时候,你就不应该一次发送很少的数据,因为这样会非常浪费带宽。 我们的网络系统当然需要每一个数据包都能够被接收到。如果TCP协议能够使用,这就不是问题。但是用TCP是彻底没希望的,所以我们必须自己去写我们的协议来处理检测和重发机制。不幸的是,我们并没有马上意识到这点,我们花了很长时间才明白这点的重要性。 我们的第一步就是换掉TCP,使用UDP。对于DirectPlay,只需要传递一个标记给它,就可以换用UDP了。但是,我们的游戏在第一个数据包丢失之后就会悲剧的挂掉。所以我们实现了一个简单的重发机制去处理丢包。这样会好了一点,但是一旦发生一些意外,游戏就跟之前一样彻底悲剧了。我们的第一个猜测是DirectPlay忽略了我们的UDP标记,而依然在使用TCP协议。但是检查后发现,这个罪魁祸首比微软更加邪恶:是因特网自身的问题。 第四课:UDP比TCP要好,但是UDP也是个坑。虽然开始时我们假设了丢包会偶尔发生,但是因特网的情况更加糟糕。在某一些连接下,有五分之一通过以太网的包会被丢失。当他们说UDP是不可靠的时候,他们并没有在开玩笑啊!我们那个简单的重发系统在这种情况下工作的并不好。重发的包也非常轻易就丢失掉了,并且我们看到了在一些情况下,原包以及重发的4,5个包都一起被丢掉了。因为我们重发了太多的数据包,超出了我们的带宽限制,然后延迟就开始上升,最后所有的噩梦就开始了。 我们的解决办法非常简单而且有效。每一个包都包含上一个包的拷贝,这样如果有一个包丢失了,那么下一个到达的包就会送来上一个包的信息。我们就又可以愉快的玩耍了:)。这需要大约之前带宽的两倍,幸运的是,我们本来所需要的带宽就非常小,所以增加一倍我们还能够接受。这个方法在连续的两个包同时丢失的时候也会失败,但是看起来不会发生这样的事。如果它真的发生了,我们就用重发的机制。 这个办法看起来非常奏效!我们最终让游戏在因特网上成功的跑起来了!当然因特网的情况比我们想象中更糟,但是我们还能够处理它。 第五课:当你认为因特网的情况不会更坏时,它就真的变的更坏了。更加广泛的测试显示我们还有很多严重的问题。显然,我们的重发机制代码里还有一些bug,因为偶尔有一些玩家会出现丢失连接而且任何数据都无法被发送的情况。在花费了无数个小时想从我们的代码里找到bug的时候,最后发现我们的代码是没问题的,反而是因为因特网断开连接了。 有的时候因特网变得非常差,根本无法发送任何数据包!我们记录下在10秒到20秒之内只有3或4个包能够被收到。难怪TCP这时会不再发送数据,而是发送检查包!你在这种情况下怎么玩游戏?现在我们确实有一个大问题了。断开连接这种问题我们确实没有准备。 幸运的是,这种情况通常非常短,大约几秒钟左右。通过调整重发机制的代码就能够处理这种情况。当玩家出现这种情况时,游戏就停止下来直到重新连接了游戏,一旦这种情况过去了,他就可以继续游戏了。 不幸的是,这种失去连接的状态可能会持续很长世间,如果真的那样,我们就无能为力了,最后我们只能把玩家踢出游戏。这不算是一个真的解决办法,但是至少一个坏的连接不会毁了所有玩家。 对于我们游戏的最后一个改良是处理因特网带来的对游戏预测不准确的问题。由于延迟能够变得非常高,需要一种方法来处理预测的游戏世界拷贝与真实不符的情况。 我们的第一条线索就是之前为了提高性能,做出的存在主服务器的设计。我们意识到如果每一个玩家都无法顺畅的把数据发送给主服务器,那么所有玩家都会延迟,因为主服务器在没有收到所有玩家的数据之前是不会向所有玩家发送压缩的数据包的。我们最终决定如果一个玩家超过一段时间还没有把数据发给主服务器,那么主服务器就丢弃掉这个玩家的数据,而把已经获得的数据通过压缩发送给所有玩家。 如果你仔细考虑这个办法的每一步,你将会意识到这使得游戏变得非常糟糕。玩家总是非常精确的知道他们自己飞船的位置。毕竟,他们知道他们实际上进行了哪些操作,所以他们准确的知道他们应该飞到哪里去。但是如果主服务器的官方游戏世界拷贝里丢失了他们的输入操作,而那些操作又关乎他们在游戏世界位置的准确性,最后,为了保持和其他玩家的同步,服务器不得不改变这些丢失了操作的玩家的位置。最后的结果就是玩家本地的位置被改变了,这使得他们游戏世界里的所有东西的位置都发生了变化,包括星空也会变化位置。 这个被服务器位移了的效果,被我们戏称为“星空跃迁”,它非常的令人不安,因为它使得游戏完全无法进行下去了。最终我们只能妥协,除非一个玩家的数据确实有非常高的延迟,否则我们不会丢掉它,这使得“星空跃迁”非常少见了。但是,后来我们发现,如果这里也使用我们后来对别的玩家的处理方式的话,也许会更好。 这种位置的瞬间变化,或者叫做“星空跃迁”,在绘制别的玩家身时也经常出现,因为他们的位置总出现预测偏差。在延迟非常低的时候(比如少于200毫秒)这种变化就看不出来,但是随着延迟的提高,游戏世界预测的就越不准确,然后这种“跃迁”就变得非常明显。 为了解决这个问题,我们实现了一种“平滑”的效果。这个平滑算法记录我们上一次预测的每一个玩家的位置。它把当前预测的位置更加靠近上一次预测的位置。这使玩家飞船的动作非常平滑,然后看起来非常好,即使是有点不准确也没关系。 |