我不建议使用它,但从技术上讲,接收 UDP 数据报的最有效方法是阻塞recvfrom
(或者WSARecvFrom
如果你愿意的话)。当然,您需要一个专用线程,否则在您阻塞时不会发生太多事情。
除了 TCP,您没有内置到协议中的连接,也没有没有定义边界的流。这意味着您可以通过每个传入的数据报获得发件人的地址,并且您会收到完整的消息或什么也没有。总是。没有例外。
现在,阻止recvfrom
意味着一个上下文切换到内核,一个上下文在收到东西时切换回来。通过在飞行中进行多次重叠读取也不会更快,因为只有一个数据报可以同时到达线路,这是迄今为止最大的限制因素(CPU时间不是瓶颈!)。使用 IOCP 意味着至少有 4 个上下文切换,两个用于接收,两个用于通知。或者,使用完成回调的重叠接收也不会好多少,因为您必须NtTestAlert
或SleepEx
运行 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 个重叠的文件传输以及执行并行计算。