Pat*_*ski 13 c++ performance networking udp iocp
我很熟悉的东西输入/输出完成端口是当它涉及到TCP.
但是,如果我编写一个FPS游戏,或者需要低延迟的任何东西都可以成为交易破坏者 - 我希望立即响应玩家以提供最佳的游戏体验,即使以丢失一些空间数据为代价走.很明显我应该使用UDP,除了频繁发送坐标更新外,我还应该实现一种半可靠的协议(afaik TCP在UDP中引起数据包丢失,所以我们应该避免混合这两个)来处理聊天消息等事件,或者枪支丢失可能是至关重要的.
假设我的目标是适用于MMOFPS游戏的性能,该游戏允许在一个持久的世界中遇见数百名玩家,除了与枪支战斗之外,它还允许他们通过聊天消息等进行交流 - 这样的事实实际存在并且运作良好 - 查看PlanetSide 2.
网上有很多文章(例如来自msdn的文章)说重叠套接字是最好的,而IOCP是神层概念,但它们似乎并没有区分我们使用其他协议而不是TCP的情况.
所以几乎没有关于开发这样一个服务器时使用的I/O技术的可靠信息,我看过这个,但这个话题似乎备受争议,我也看到过这个,但考虑到第一个链接中的讨论,我不知道我是否应该遵循第二个假设,是否应该将IOCP与UDP一起使用,如果不是,那么在UDP方面,最具可扩展性和效率的I/O概念是什么.
或许我只是做了另一个过早的优化,目前还不需要提前思考?
考虑将其发布在gamedev.stackexchange.com上,但这个问题更适用于我认为的通用网络.
Dam*_*mon 14
我不建议使用它,但从技术上讲,接收UDP数据报的最有效方法是阻止recvfrom(或者WSARecvFrom如果你愿意).当然,你需要一个专门的线程,否则当你阻止时不会发生太多.
除了使用TCP之外,协议中没有内置连接,并且没有没有定义边框的流.这意味着你可以获得每个数据报的发件人地址,并且你得到一条完整的信息或什么也没有.总是.没有例外.
现在,阻塞recvfrom意味着一个上下文切换到内核,一个上下文在收到内容时切换回来.通过在飞行中进行多次重叠读取也不会更快,因为只有一个数据报可以同时到达线路,这是迄今为止最大的限制因素(CPU时间不是瓶颈!).使用IOCP意味着至少有4个上下文切换,两个用于接收,两个用于通知.或者,带有完成回调的重叠接收也不是更好,因为你必须NtTestAlert或者SleepEx要运行APC队列,所以你再次拥有至少2个额外的上下文切换(但是,所有通知一起只有+2,你可能会非常无论如何已经睡了).
但是:
使用IOCP和重叠读取仍然是最好的方法,即使它不是最有效的方法.完成端口与使用TCP无关,它们也适用于UDP.只要您使用重叠读取,您使用的协议(甚至是网络或磁盘,还是其他一些可等待或可警告的内核对象)都无关紧要.
无论是延迟还是CPU负载,无论是为完成端口刻录额外的几百个周期,它也无关紧要.我们在这里谈论的是"纳米"与"毫米",相当于一百万到一百万.另一方面,完井港总体上是一个非常舒适,健全和高效的系统.
例如,当您没有及时收到ACK时(例如,当您需要一种可靠性,UDP不会为您执行此操作时必须执行此操作时),以及keepalive,您可以简单地实现重新发送的逻辑.
对于keepalive,添加一个可以在每次收到任何内容时重置的等待计时器(可能在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,虽然它们可以很好地应对丢失的数据包,但它们根本不会像重要的重复丢包一样.尽管如此,它们在具有大量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 */
}
}
Run Code Online (Sandbox Code Playgroud)
注意处理完成消息,用户任务和线程控制的整个逻辑如何在一个简单的循环中发生,没有模糊的东西,没有复杂的路径,每个线程只执行相同的相同循环.
相同的代码适用于1个线程服务1个套接字,或者用于50个服务5,000个套接字的池中的16个线程,10个重叠文件传输,以及执行并行计算.
我已经看到许多使用UDP作为网络协议的FPS游戏的代码.
标准解决方案是在一个大型UDP数据包中发送更新单个游戏帧所需的所有数据.该数据包应包括帧号和校验和.数据包当然应该被压缩.
通常,UDP数据包包含播放器附近的每个实体的位置和复杂性,发送的任何聊天消息以及所有最近的状态更改.(例如,新实体创建,实体destrouyed等)
然后客户端侦听UDP数据包.它将仅使用具有最高帧编号的数据包.因此,如果出现乱序数据包,则会忽略旧数据包.
任何具有错误校验和的数据包也会被忽略.
每个数据包应包含将客户端的游戏状态与服务器同步的所有信息.
聊天消息通过多个数据包重复发送,每条消息都有一个唯一的消息ID例如,您重新发送相同的聊天消息,比如说一整秒的帧.如果客户端在获得60次聊天消息后错过了聊天消息 - 则网络频道的质量太低而无法播放游戏.客户端将显示他们在UDP数据包中获得的任何消息,这些消息具有尚未显示的消息ID.
类似地,对于正在创建或销毁的对象 所有创建或销毁的对象都具有服务器设置的唯一对象ID.如果对象在之前没有被操作的情况下被创建或销毁,则会被创建或销毁.
所以这里的关键是冗余地发送数据,并将所有状态转换键入服务器设置的唯一ID.
@edit:另一张海报提到,对于聊天消息,您可能希望在不同的端口上使用不同的协议.他们可能是正确的,这可能是最佳的.这适用于延迟并不重要的消息类型,但可靠性更重要,您可能希望打开不同的端口并使用TCP.但我会把它留作后来的练习.一开始游戏只使用一个频道肯定更容易和更清洁,并且稍后可以找出多个端口,多个频道的变幻莫测的各种故障模式.(例如,如果UDP频道正常工作会发生什么,但聊天频道会出现故障?如果您成功打开一个端口而不是另一个端口怎么办?)
| 归档时间: |
|
| 查看次数: |
7676 次 |
| 最近记录: |