使用完成端口与UDP?(Using IOCP with UDP?)

2019-06-26 02:07发布

我很熟悉的东西输入/输出完成端口是当它涉及到TCP。

但是,如果我是例如编码FPS游戏,或任何其中需要低时延可以是一个大忌 - 我想给玩家立即响应,以提供最佳的游戏体验,即使在失去对某些空间数据的成本走。 因此很显然,我应该使用UDP和除了发送坐标更新频繁,我也应该实现一种半可靠的协议(据我所知TCP诱导UDP数据包丢失,所以我们应该避免混淆这两种)来处理这样的事件,如聊天消息,枪声或丢失数据包的可能是至关重要的。

比方说,我的目标在性能将适用于MMOFPS游戏,允许符合在一个数百名玩家,持久的世界,并且除了与枪的战斗,它允许他们通过聊天消息等通信 - 这样的事情确实存在并且效果很好 - 退房行星边际2。

在网上很多文章存在(例如,这些从MSDN)说重叠插座是最好的,IOCP是神级的概念,但他们似乎没有区别,我们所使用的协议不是TCP的情况。

所以几乎对I /开发这样一个服务器时使用Ø技术,我看没有可靠的信息本 ,但主题似乎是很大的争议,我也看到了这个 ,但考虑到在第一个链接的讨论,我不知道我是否应该遵循的第二个假设的, 我是否应该使用UDP在所有使用IOCP,如果没有, 什么是最可扩展性和高效的I / O的概念,当涉及到UDP。

或者,也许我只是做未来需要的时候,又有不成熟的优化,没有思想?

想过张贴在gamedev.stackexchange.com,但这个问题更好的适用于通用的网络,我认为。

Answer 1:

我不建议用这个,但在技术上是最有效的方式来接收UDP数据包将只是阻止recvfrom (或WSARecvFrom如果你愿意)。 当然,你需要一个专门的线程为,或当你阻挡不了多少否则会发生。

除使用TCP,你没有内置到协议的连接,而你没有没有确定的边界流。 这意味着你每自带数据报得到发送者的地址,你会得到一个整体的消息或没有。 总是。 没有例外。
现在,阻塞在recvfrom意味着一个上下文切换到内核,并收到东西时,一个上下文切换回来。 它不会去任何更快的有几个重叠在飞行中读取或者,因为只有一个数据报可在同一时间,这是迄今为止最大的限制因素上线到达(CPU时间不是瓶颈!)。 使用IOCP指至少4间上下文切换,两个用于接收和两个用于通知。 或者,重叠完成回调接受也好不了多少或者,因为你必须NtTestAlertSleepEx运行APC队列,所以你又至少有2个额外的上下文切换(不过,这只是+2所有通知在一起,和你可能incidentially反正已经睡)。

然而:
使用完成端口和重叠读取不过是做到这一点的最好办法,即使它是不是最有效的一个。 完成端口,不论是使用TCP,他们的工作只是UDP也很好。 只要您使用重叠的读取,它不会不管你使用哪种协议(甚至它是否是网络或磁盘,或其他一些可等待或警惕的内核对象)。
它也并不重要,无论是对延迟或CPU负载是否燃烧多余的完成端口几百个周期。 我们谈论的是“纳米”与“毫”在这里,一比一百万的因素。 在另一方面,完成端口是整体很舒服,声音和高效的系统。

例如,你可以平凡实现逻辑重新发送时,您没有收到及时的ACK(其时所需的可靠性的形式,你必须做的,UDP不为你做的话),以及存活。
对于存活,添加您重置每次你得到了什么时间可等待计时器(15或20秒后可能发射)。 如果你完成端口曾经告诉你,这个计时器走了,你知道连接已经死了。
对于重发,你可以如设置超时GetQueuedCompletionStatus ,每次你醒来时发现比某某这么老越来越尚未获得确认的数据包。
整个逻辑发生在同一个地方,这是非常好的。 这是通用的,高效的,并且很难做到的错误。

你甚至可以有多个线程(事实上,比你的CPU更多的线程具有核心)的完成端口块。 许多线程听起来像一个不明智的设计,但它实际上是做的最好的事情。

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

这意味着你可以阻止和等待进入的数据包并处理它(解密,解压缩,从磁盘执行逻辑,读成才,不管)和另一个线程将立即准备处理下一个数据包,可能出现在未来微秒。 您可以使用重叠的磁盘IO与同完成端口了。 如果您有计算的工作(如AI)这样做可以分成任务,您可以手动发布( PostQueuedCompletionStatus )那些完成端口,以及和你有一个并行任务调度程序是免费的。 所有你需要做的是包装一个OVERLAPPED成后,它有一些额外的数据结构,并使用您将识别的关键。 不担心线程同步的,它神奇地工作(你甚至不严格需要有一个OVERLAPPED在自定义结构中发布自己的通知时,它会与你通过任何结构的工作,但我不喜欢撒谎的操作系统,你永远不知道...)。

它甚至没有多大关系从磁盘读取数据时是否阻塞,例如。 有时候,这恰好和你一点办法也没有。 还等什么,一个线程块,但是你的系统仍然接收消息和反应吧! 当有必要完成端口自动从它的游泳池另一个线程。

关于TCP UDP的诱导丢包,这是我倾向于称之为一个城市的神话(虽然它是有点正确的 )东西。 这种共同的口头禅是措辞方式却是误导性的。 它可能已从前是真实的(存在于这个问题,但是这是,近十年的老研究),该路由器会丢弃UDP 支持TCP,从而引起数据包丢失。 然而,这是,肯定不是现在的情况。
来看一个更真实的一点是, 任何你发送导致数据包丢失。 TCP诱导的TCP数据包丢失和UDP导致TCP和反之亦然丢包,这是一个正常的状态(这是TCP如何实现拥塞控制,顺便说一句)。 路由器将大致前一个输入的分组,如果在另一插头的电缆是“沉默的”,它将排队用硬期限几个分组(缓冲器经常故意小),任选地它可以应用某种形式的QoS,它会只是默默地放下一切
很多具有相当苛刻的实时要求的应用(VoIP,视频流,你的名字)现在使用UDP,而他们与丢失的包或两个很好应付,他们不都喜欢显著,反复出现数据包丢失。 尽管如此,他们demonstrably上有大量的TCP流量的网络正常工作。 我的手机(如千百万人的手机)的作品完全通过VoIP,数据会在同一个路由器作为互联网流量。 没有办法,我能挑起与TCP辍学,不管我怎么努力。
从日常的观察,一看就知道肯定是UDP是明确不支持TCP的下降。 如果有的话,服务质量可能有利于UDP通过TCP,但它肯定不会penaltize它。
否则,像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个套接字,或者16个线程出50服务5000个插座,10个重叠的文件传输,并执行并行计算的池。



Answer 2:

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

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

一般来说,UDP数据包包含了播放器附近的每个实体的位置和velicities,任何聊天邮件已发送,而所有最近的状态变化。 (例如,新实体创建,实体destrouyed等)

然后客户端侦听UDP数据包。 它将只使用最高帧数的数据包。 因此,如果不按顺序包出现,旧的数据包被简单地忽略。

与错误校验和任何数据包也将被忽略。

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

聊天消息反复地发送了多个数据包,每个消息有例如,重传的说一个完整的第二身价帧中的同一聊天消息的唯一消息ID。 如果客户端得到它60次后错过了聊天消息 - 那么网络信道的质量仅仅是太低了玩游戏。 客户端会显示他们有他们还没有显示消息ID的UDP数据包得到任何消息。

同样,对于对象被创建或销毁。 所有创建或销毁的对象都有一个唯一对象ID由服务器设置。 对象将会被创建或销毁,如果他们所对应的对象ID以前没有采取行动。

因此,这里的关键是冗余发送数据,密钥的所有状态转换为唯一的ID由服务器设置。

@edit:另一个海报提到,聊天消息,你可能需要使用不同协议的不同端口上。 他们可能是对,可能是最佳的。 这是消息类型,其中延迟并不重要,但可靠性你可能想打开一个不同的端口,并使用TCP更重要。 但我会离开,作为一个后来锻炼; Tibial。 这是在第一当然更容易和更清洁你的游戏只使用一个信道,并找出的多个端口,多渠道,变幻莫测与他们的各种故障模式后。 (例如,如果UDP通道的工作,但聊天频道推移会发生什么降?如果你打开一个端口成功是什么,而不是其他?)



Answer 3:

当我这样做的客户端 ,我们使用ENET为基数可靠的UDP协议和重新实现这个从头开始使用IOCP的服务器端,而使用免费的ENET代码客户端。

IOCP正常工作与UDP,然后用您也可能会在处理任何TCP连接,很好地集成(我们有TCP,网页套接字或UDP客户端连接和服务器节点之间的TCP连接,并能够对所有的这些插入,如果我们在同一个线程池要的是方便)。

如果绝对延迟和UDP数据包的处理速度是最重要的(和它不可能真的是),则使用新的服务器2012 RIO API可能是值得的,但我不相信,但(看到这里的一些初步的性能测试和一些例如服务器)。

你可能想看看使用GetQueuedCompletionStatusEx()与您的入站数据处理,因为它降低了每个数据包的上下文切换,你可以拉多个数据包回来一个电话。



Answer 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一个游戏,所述资产从CDN经由HTTP / TCP下载和它会尝试使用所有可用的带宽,这将增加在主游戏信道分组丢失(通常是UDP)。 我普遍发现的干扰足够低,我不认为你应该担心太多。

3)对于IOCP,我与他们的经验是非常有限的,但已经进行了大量的游戏在过去的网络,我是持怀疑态度,他们在UDP的情况下,增加价值。 通常,服务器将有一个UDP套接字是处理所有输入数据。 随着数百个用户连接,在该数据进入服务器的比率是非常高的。 有一个后台线程的套接字上做阻塞调用如其他人所说,然后迅速将数据移动到队列中的主应用程序线程回暖是一个合理的解决方案,但是有些不必要的,因为在实践中,数据以使未来在负载下,有没有当过块睡觉线程多点快速。

让我换一种方式,如果阻塞插座Call调查的一个数据包,然后把线程休眠,直到下一个包进来了,这将是上下文切换到该线程数千次每秒时,数据传输速率得到高。 要么,由当时的畅通线程执行和清算数据,就已经是额外的数据准备处理为好。 相反,我更愿意把插座非阻塞模式,然后有一个后台线程旋在100FPS左右处理它(如需要达到帧速率投票,睡)。 以这种方式,该套接字缓冲区将建立传入分组为10毫秒,然后在后台线程将再次醒来并处理散装所有这些数据,然后回到睡眠,从而防止无端上下文切换。 然后我有同样的后台线程做其他的发送相关的处理,当它醒来为好。 作为整个事件驱动失去了许多它的好处,当数据量得到至少有点高。

在TCP的情况下,这个故事是完全不同的,因为你需要一个有效的机制,以找出哪些数百所连接的输入数据是来自和投票的他们都非常缓慢,即使是定期。

所以,在UDP与在它上面的一个土生土长的UDP-可靠的机制的情况下,我通常有一个后台线程播放该操作系统起到同样的作用......而OS从网卡然后分发获取数据它在内部进行处理各种逻辑的TCP连接,我的后台线程会从孤UDP套接字的数据(通过定期轮询)并将其分配给处理我自己的内部逻辑连接对象。 这些内部逻辑连接,然后把应用程序级的分组数据与他们来自逻辑连接标记一个线程安全的主队列。 主应用程序线程,然后处理在主队列中,直接路由所述数据包与该连接相关联的游戏级对象。 从视主应用程序线程点,它只是有一个事件驱动的队列它正在处理。

底线是,鉴于投票调用孤UDP套接字很少出现空洞,很难想象那里将是解决这个问题的一种更有效的方式。 你失去了用这种方法唯一的一点是你等待长达10ms的醒来时在理论上可以被唤醒数据最先到达的瞬间,但这是唯一有意义的,如果你是极轻负载下的反正。 此外,主应用程序线程不会被利用数据的,直到它的下一个帧周期不管怎么说,这样的差别是没有实际意义,我认为整个系统的性能是通过这种技术增强。



Answer 5:

我不会持有游戏一样古老的星际了现代网络实施的典范。 尤其是没有见过他们的网络库的内部。 :)

不同类型的通信需要不同的方法。 一个围绕上述框架/位置更新之间的差异会谈的答案和聊天消息,没有认识到使用相同的传输两个可能是愚蠢的。 你最应该用你的聊天实施和聊天服务器,文本式的聊天之间的连接TCP套接字。 不争论,就去做。

所以,为你的游戏客户端通过到达的UDP数据包做更新,从网络适配器通过内核和你的应用程序中最有效的途径是(最有可能)会是一个阻止的recv。 创建剥去包断网的线,验证其有效性(CHKSUM比赛,序列号增加,你有什么其他检查),数据反序列化到内部对象,然后排队的对象进行内部队列应用程序线程用于处理那些五花八门的更新。

但是,不要把我的话:测试一下吧! 写一个小程序可以接收和反序列化3或4类型的数据包,使用一个阻塞线程和一个队列传送的对象,然后使用它的单个线程和IOCPS,与反序列化,并在完成例程排队重新写。 英镑足够的数据包,通过它来获得运行时间最长的分钟范围,以及测试哪一个是最快的。 确保在测试应用的东西(即某些线程),所以你得到的相对性能的全貌被消耗掉队列中的对象。

后回到这里时,你已经完成了两个测试程序,并让我们知道哪些制定出最好的,mm'kay? 这是最快的,你更愿意在将来维护,历时最长得到它的工作,等等。



Answer 6:

如果你想支持多并发连接,你需要使用事件驱动的联网方式。 我所知道的两个很好的库: libev (由所使用的NodeJS )和libevent的 。 他们是非常轻便,易于使用。 我已经成功地使用的libevent在支持数百个并行TCP / UDP(DNS)的连接的应用程序。

我相信,通过事件驱动的网络I / O不在服务器过早的优化 - 它应该是默认的设计模式。 如果你想要做一个快速原型实现它可能是更好的高级语言开始。 对于JavaScript存在的NodeJS和Python的有扭曲的 。 无论我个人建议。



Answer 7:

怎么样的NodeJS支持UDP ,它是高度可扩展的。



文章来源: Using IOCP with UDP?