15

我非常熟悉TCP的输入/输出完成端口的用途。

但是,例如,如果我正在编写 FPS 游戏,或者任何需要低延迟的东西可能会破坏交易 - 我希望立即响应玩家以提供最佳的游戏体验,即使以丢失一些空间数据为代价去。很明显,我应该使用UDP,除了经常发送坐标更新之外,我还应该实现一种半可靠的协议(afaik TCP 在 UDP 中会导致丢包,所以我们应该避免将这两者混为一谈)来处理诸如聊天消息之类的事件,或丢包可能至关重要的枪声。

假设我的目标是适用于 MMOFPS 游戏的性能,该游戏允许在一个持久的世界中遇到数百名玩家,除了与枪支战斗之外,它还允许他们通过聊天消息等进行交流 - 这样的事情确实存在并且运行良好 - 查看 PlanetSide 2。

网上的许多文章(例如来自 msdn 的这些文章)都说重叠套接字是最好的,而 IOCP 是神级概念,但它们似乎没有区分我们使用 TCP 以外的其他协议的情况。

所以几乎没有关于开发这种服务器时使用的 I/O 技术的可靠信息,我看过这个,但这个话题似乎很有争议,我也看过这个,但考虑到第一个链接中的讨论,我不知道我是否应该遵循第二个假设,我是否应该将 IOCP 与 UDP 一起使用,如果不是,那么对于 UDP,什么是最可扩展和最有效的 I/O 概念

或者,也许我只是在进行另一个过早的优化,目前不需要提前考虑?

考虑将其发布在 gamedev.stackexchange.com 上,但我认为这个问题更适用于通用网络。

4

7 回答 7

15

我不建议使用它,但从技术上讲,接收 UDP 数据报的最有效方法是阻塞recvfrom(或者WSARecvFrom如果你愿意的话)。当然,您需要一个专用线程,否则在您阻塞时不会发生太多事情。

除了 TCP,您没有内置到协议中的连接,也没有没有定义边界的流。这意味着您可以通过每个传入的数据报获得发件人的地址,并且您会收到完整的消息或什么也没有。总是。没有例外。
现在,阻止recvfrom意味着一个上下文切换到内核,一个上下文在收到东西时切换回来。通过在飞行中进行多次重叠读取也不会更快,因为只有一个数据报可以同时到达线路,这是迄今为止最大的限制因素(CPU时间不是瓶颈!)。使用 IOCP 意味着至少有 4 个上下文切换,两个用于接收,两个用于通知。或者,使用完成回调的重叠接收也不会好多少,因为您必须NtTestAlertSleepEx运行 APC 队列,因此您至少有 2 个额外的上下文切换(尽管,所有通知一起只有 +2,您可能会顺便反正已经睡了)。

然而:
使用 IOCP 和重叠读取仍然是最好的方法,即使它不是最有效的方法。完成端口与使用 TCP 无关,它们也适用于 UDP。只要您使用重叠读取,您使用什么协议(甚至是网络或磁盘,或其他一些可等待或可警报的内核对象)都无关紧要。
是否为完成端口额外燃烧几百个周期对于延迟或 CPU 负载也无关紧要。我们在这里谈论的是“纳米”与“毫”,相差一到一百万。另一方面,完井端口总体上是一个非常舒适、健全和高效的系统。

例如,当您没有及时收到 ACK 时,您可以简单地实现重新发送的逻辑(当需要某种形式的可靠性时,您必须这样做,UDP 不会为您这样做),以及保持活动状态。
对于 keepalive,添加一个可等待计时器(可能在 15 或 20 秒后触发),每次收到任何内容时都会重置该计时器。如果您的完成端口告诉您此计时器已关闭,您就知道连接已死。
对于重新发送,您可以设置一个超时时间GetQueuedCompletionStatus,并且每次您醒来时都会发现所有超过某某旧且尚未被确认的数据包。
整个逻辑发生在一个地方,这非常好。它用途广泛、高效且不易出错。

您甚至可以在完成端口上阻塞多个线程(实际上,线程数比 CPU 的内核数多)。许多线程听起来像是一个不明智的设计,但实际上这是最好的做法。

完成端口以后进先出的顺序唤醒 N 个线程,N 是内核数,除非你告诉它做一些不同的事情。如果这些线程中的任何一个被阻塞,另一个线程会被唤醒以处理未完成的事件。这意味着在最坏的情况下,一个额外的线程可能会运行很短的时间,但这是可以容忍的。在平均情况下,只要有一些工作要做,它就会使处理器使用率接近 100%,否则为零,这非常好。LIFO 唤醒有利于处理器缓存并保持低切换线程上下文。

这意味着您可以阻止并等待传入​​的数据报并对其进行处理(解密、解压缩、执行逻辑、从磁盘读取某些内容等),另一个线程将立即准备好处理可能在下一微秒内出现的下一个数据报。您也可以使用具有相同完成端口的重叠磁盘 IO。如果您有计算工作(例如 AI)可以拆分为任务,您也可以PostQueuedCompletionStatus在完成端口上手动发布()这些任务,并且您可以免费获得并行任务调度程序。您所要做的就是将 an 包装OVERLAPPED到一个后面有一些额外数据的结构中,并使用您将识别的密钥。不用担心线程同步,它只是神奇地工作(你甚至不需要严格地OVERLAPPED在发布您自己的通知时,在您的自定义结构中,它适用于您传递的任何结构,但我不喜欢对操作系统撒谎,你永远不知道......)。

是否阻塞甚至都没有多大关系,例如从磁盘读取时。有时这只是发生,你无能为力。那又怎样,一个线程阻塞了,但您的系统仍然接收消息并对其做出反应!必要时,完成端口会自动从其池中拉出另一个线程。

关于 TCP 在 UDP 上导致丢包的问题,​​我倾向于称之为都市神话(尽管它有些正确)。然而,这种常见的口头禅的措辞方式具有误导性。曾几何时,路由器可能会丢弃 UDP转而使用TCP,从而导致数据包丢失,这可能是真的(存在关于该问题的研究,但已有近十年的历史)。然而,今天的情况肯定不是这样。
更真实的观点是,任何您发送会导致丢包。TCP 在 TCP 上导致丢包,而 UDP 在 TCP 上导致丢包,反之亦然,这是正常情况(顺便说一下,这就是 TCP 实现拥塞控制的方式)。如果另一个插头上的电缆“静默”,路由器通常会转发一个传入数据包,它将在硬期限内将几个数据包排队(缓冲区通常故意很小),可选地它可以应用某种形式的 QoS,它会简单而默默地放下一切
现在,许多具有相当苛刻的实时要求的应用程序(VoIP、视频流,如你所愿)都使用 UDP,虽然它们可以很好地处理一两个丢失的数据包,但它们根本不喜欢严重的、反复出现的数据包丢失。尽管如此,它们在具有大量 TCP 流量的网络上仍然可以正常工作。我的电话(就像数百万人的电话一样)完全通过 VoIP 工作,数据通过同一个路由器作为互联网流量传输。无论我多么努力,我都无法通过 TCP 引起辍学。
从日常观察中,可以确定 UDP 绝对不会被 TCP 抛弃。如果有的话,QoS 可能有利于 UDP 而不是 TCP,但它肯定不会惩罚它。
否则,一旦你打开一个网站,像 VoIP 这样的服务就会断断续续,如果你下载 DVD ISO 文件大小的东西,就会完全不可用。

编辑:
为了让您了解 IOCP 的生活有多简单(有些精简,缺少实用功能):

for(;;)
{
    if(GetQueuedCompletionStatus(iocp, &n, &k, (OVERLAPPED**)&o, 100) == 0)
    {
        if(o == 0) // ---> timeout, mark and sweep
        {
            CheckAndResendMarkedDgrams();  // resend those from last pass
            MarkUnackedDgrams();           // mark new ones
        } 
        else
        {   // zero return value but lpOverlapped is not null:
            // this means an error occurred
            HandleError(k, o);
        }
        continue;
    }

    if(n == 0 && k == 0 && o == 0)
    {
        // zero size and zero handle is my termination message
        // re-post, then break, so all threads on the IOCP will
        // one by one wake up and exit in a controlled manner
        PostQueuedCompletionStatus(iocp, 0, 0, 0);
        break;
    }
    else if(n == -1) // my magic value for "execute user task"
    {
        TaskStruct *t = (TaskStruct*)o;
        t->funcptr(t->arg);
    }
    else
    {
        /* received data or finished file I/O, do whatever you do */
    }
}

注意处理完成消息、用户任务和线程控制的整个逻辑是如何发生在一个简单的循环中的,没有晦涩的东西,没有复杂的路径,每个线程只执行相同的、相同的循环。
相同的代码适用于为 1 个套接字提供服务的 1 个线程,或为 50 个为 5,000 个套接字提供服务的池中的 16 个线程、10 个重叠的文件传输以及执行并行计算。

于 2012-07-09T10:03:22.517 回答
7

我见过许多使用 UDP 作为网络协议的 FPS 游戏的代码。

标准解决方案是在一个大的 UDP 数据包中发送更新单个游戏帧所需的所有数据。该数据包应包括帧号和校验和。数据包当然应该被压缩。

通常,UDP 数据包包含玩家附近每个实体的位置和速度、发送的任何聊天消息以及所有最近的状态变化。(例如创建新实体、销毁实体等)

然后客户端监听UDP数据包。它将仅使用具有最高帧号的数据包。因此,如果出现乱序数据包,旧数据包将被忽略。

任何校验和错误的数据包也会被忽略。

每个数据包都应包含所有信息,以使客户端的游戏状态与服务器同步。

聊天消息通过多个数据包重复发送,并且每条消息都有一个唯一的消息 id 例如,您重新传输相同的聊天消息一整秒的帧。如果客户在收到 60 次后错过了一条聊天消息 - 那么网络频道的质量太低而无法玩游戏。客户端将显示他们在 UDP 数据包中获得的任何消息,这些消息具有尚未显示的消息 ID。

对于正在创建或销毁的对象也是如此。所有创建或销毁的对象都有一个由服务器设置的唯一对象 ID。如果对象对应的对象 id 之前没有被操作过,则对象将被创建或销毁。

所以这里的关键是冗余发送数据,并将所有状态转换为服务器设置的唯一ID。

@edit:另一张海报提到,对于聊天消息,您可能希望在不同的端口上使用不同的协议。他们可能是正确的,这可能是最佳的。这适用于延迟不重要但可靠性更重要的消息类型,您可能希望打开不同的端口并使用 TCP。但我会把它留作以后的练习。对于你的游戏来说,一开始只使用一个通道当然更容易和更清晰,然后再弄清楚多个端口、多个通道的变幻莫测以及它们的各种故障模式。(例如,如果 UDP 通道正常工作,但聊天通道出现故障会发生什么?如果您成功打开一个端口而不打开另一个端口怎么办?)

于 2012-07-09T13:02:58.157 回答
4

当我为客户端执行此操作时,我们使用ENet作为基本可靠的 UDP 协议,并从头开始重新实现,以将 IOCP 用于服务器端,同时将免费提供的 ENet 代码用于客户端。

IOCP 可以很好地与 UDP 配合使用,并且可以很好地与您可能正在处理的任何 TCP 连接集成(我们有 TCP、WebSocket 或 UDP 客户端连接以及服务器节点之间的 TCP 连接,并且如果我们能够将所有这些插入到同一个线程池中)想要很方便)。

如果绝对延迟和 UDP 数据包处理速度是最重要的(实际上不太可能),那么使用新的 Server 2012 RIO API 可能是值得的,但我还不相信(请参阅此处了解一些初步性能测试和一些示例服务器)。

您可能希望查看使用 GetQueuedCompletionStatusEx() 处理入站数据,因为它减少了每个数据报的上下文切换,因为您可以通过一次调用将多个数据报拉回。

于 2012-07-10T12:13:00.727 回答
4

几件事:

1)作为一般规则,如果您需要可靠性,最好只使用 TCP。基于 UDP 的竞争性甚至可能更出色的解​​决方案是可能的,但要正确并使其正常运行是极其困难的。人们在 UDP 之上实现可靠性的主要问题是适当的流量控制。如果您打算发送大量数据并希望它优雅地利用当前可用的带宽(随着路由条件不断变化),您必须具有流量控制。在实践中,除了 TCP 使用的本质上相同的算法之外,实施任何其他协议也可能对网络上的其他协议不友好。在实现该算法方面,您不太可能比 TCP 做得更好。

2)至于并行运行 TCP 和 UDP,现在它并不像其他人所说的那么大。有一次,我听说沿途过载的路由器偏向于在 TCP 数据包之前丢弃 UDP 数据包,这在某些方面是有道理的,因为无论如何都会重新发送丢弃的 TCP 数据包,而丢失的 UDP 数据包通常不会。也就是说,我怀疑这是否真的发生。特别是,丢弃 TCP 数据包会导致发送方减速,因此丢弃 TCP 数据包可能更有意义。

TCP 可能会干扰 UDP 的一种情况是 TCP 本质上它的算法会不断地尝试越来越快,除非它到达丢失数据包的点,然后它会节流并重复该过程。由于 TCP 连接不断地碰到带宽上限,它导致 UDP 丢失的可能性与 TCP 丢失一样,理论上 TCP 流量似乎偶尔会导致 UDP 丢失。

但是,即使您将自己的可靠机制放在 UDP 之上(假设您正确进行流量控制),您也会遇到这个问题。如果您想避免这种情况,您可以有意地在应用层限制可靠数据。通常在游戏中,可靠数据速率仅限于客户端或服务器实际需要发送可靠数据的速率,这通常远低于管道的带宽能力,因此无论是否存在干扰都不会发生。基于 TCP 或 UDP 可靠。

如果您正在制作流媒体资产游戏,事情会变得更加困难。对于像 FreeRealms 这样的游戏,资产是通过 HTTP/TCP 从 CDN 下载的,它会尝试使用所有可用带宽,这将增加主游戏通道(通常是 UDP)上的丢包率。我通常发现干扰足够低,我认为你不应该太担心它。

3)至于 IOCP,我对它们的经验非常有限,但过去做过广泛的游戏网络,我怀疑它们在 UDP 的情况下是否增加了价值。通常,服务器将有一个处理所有传入数据的 UDP 套接字。在连接了数百个用户的情况下,数据进入服务器的速率非常高。正如其他人所建议的那样,让后台线程在套接字上进行阻塞调用,然后将数据快速移动到队列中以供主应用程序线程获取是一个合理的解决方案,但有些不必要,因为实际上数据是这样进来的在负载下快速,当线程阻塞时睡眠没有多大意义。

让我换一种说法,如果阻塞套接字调用轮询单个数据包,然后让线程休眠直到下一个数据包进来,那么当数据速率变高时,它将每秒数千次上下文切换到该线程. 无论是这样,还是在未阻塞线程执行并清除数据时,也已经准备好处理其他数据。相反,我更喜欢将套接字置于非阻塞模式,然后让后台线程以大约 100fps 的速度旋转处理它(根据需要在轮询之间休眠以达到帧速率)。以这种方式,套接字缓冲区将建立传入数据包 10 毫秒,然后后台线程将唤醒一次并批量处理所有数据,然后返回睡眠状态,从而防止无缘无故的上下文切换。然后我让同一个后台线程在它唤醒时执行其他与发送相关的处理。当数据量变得最低限度时,完全由事件驱动会失去许多好处。

在 TCP 的情况下,情况就完全不同了,因为您需要一种有效的机制来确定传入数据来自数百个连接中的哪一个,并且即使定期轮询它们也非常缓慢。

因此,在 UDP 的情况下,在其上具有自主开发的 UDP 可靠机制,我通常有一个后台线程扮演与操作系统相同的角色......而操作系统从网卡获取数据然后分发它到内部的各种逻辑 TCP 连接进行处理,我的后台线程从单独的 UDP 套接字(通过定期轮询)获取数据并将其分发到我自己的内部逻辑连接对象进行处理。然后,这些内部逻辑连接将应用程序级别的数据包数据放入线程安全的主队列中,并用它们来自的逻辑连接进行标记。然后主应用程序线程处理该主队列,将数据包直接路由到与该连接关联的游戏级对象。从主应用程序线程的角度来看,

底线是,鉴于对单独 UDP 套接字的轮询调用很少出现空,很难想象会有更有效的方法来解决这个问题。使用此方法唯一会丢失的是您等待长达 10 毫秒才能唤醒,而理论上您可能会在数据刚到达的那一刻醒来,但这仅在您处于极轻负载的情况下才有意义。另外,无论如何,主应用程序线程在下一个帧周期之前都不会使用数据,所以区别是没有意义的,我认为这种技术可以提高整体系统性能。

于 2014-02-13T21:56:31.090 回答
3

我不会把像 PlanetSide 这样古老的游戏作为现代网络实施的典范。尤其是没有看到他们的网络库的内部。:)

不同类型的沟通需要不同的方法。上面的答案之一谈到了帧/位置更新和聊天消息之间的差异,而没有意识到为两者使用相同的传输可能很愚蠢。您绝对应该在您的聊天实现和聊天服务器之间使用连接的 TCP 套接字,以进行文本式聊天。不争辩,做就做。

因此,对于您的游戏客户端通过到达的 UDP 数据包进行更新,从网络适配器通过内核进入您的应用程序的最有效路径(很可能)将是阻塞接收。创建一个线程,从网络中提取数据包,验证它们的有效性(chksum 匹配,序列号增加,无论你有什么其他检查),将数据反序列化为内部对象,然后将内部队列中的对象排队到应用程序线程处理这些更新。

但不要相信我的话:测试它!编写一个可以接收和反序列化 3 或 4 种数据包的小程序,使用阻塞线程和队列来传递对象,然后使用单线程和 IOCP 重新编写它,并在完成例程中进行反序列化和排队。将足够多的数据包通过它以使运行时间达到分钟范围,并测试哪个数据包最快。确保您的测试应用程序中的某些东西(即某个线程)正在消耗队列中的对象,以便您全面了解相对性能。

当你完成两个测试程序后回到这里,让我们知道哪个效果最好,嗯?哪个最快,哪个你更愿意在未来维护,哪个需要最长时间才能让它工作,等等。

于 2012-07-10T00:02:17.070 回答
2

如果要支持多个同时连接,则需要使用事件驱动的网络方法。我知道两个很好的库:libev(由nodeJS使用)和libevent。它们非常便携且易于使用。我已经在一个支持数百个并行 TCP/UDP(DNS) 连接的应用程序中成功使用了 libevent。

我相信在服务器中使用事件驱动的网络 i/o 并不是过早的优化——它应该是默认的设计模式。如果你想做一个快速的原型实现,最好从更高级的语言开始。对于 JavaScript,有nodeJS,对于 Python,有Twisted。我个人都可以推荐。

于 2012-07-15T07:24:07.387 回答
0

NodeJS怎么样? 它支持UDP并且具有高度可扩展性。

于 2012-07-11T11:27:17.853 回答