我不推荐使用这个,但技术上接收 UDP 数据报最有效的方法是阻塞recvfrom(或者WSARecvFrom,如果你愿意的话)。当然,你需要一个专门的线程,否则在你阻塞时不会发生太多事情。
除了 TCP,您确实没有在协议中内置了连接,并且您没有没有定义边界的流。这意味着您可以通过每个传入的数据报获得发件人的地址,并且您会收到完整的消息或什么也没有。总是。没有例外。
现在,阻塞recvfrom 意味着一个上下文切换到内核,一个上下文在收到某些内容时切换回来。通过在飞行中进行多次重叠读取也不会加快速度,因为只有一个数据报可以同时到达线路,这是迄今为止最大的限制因素(CPU 时间不是瓶颈!)。使用 IOCP 意味着至少有 4 个上下文切换,两个用于接收,两个用于通知。或者,带有完成回调的重叠接收也不会好多少,因为您必须 NtTestAlert 或 SleepEx 才能运行 APC 队列,因此您至少有 2 个额外的上下文切换(尽管所有通知只有 +2在一起,而且你可能顺便已经睡着了)。
但是:
使用 IOCP 和重叠读取仍然是最好的方法,即使它不是最有效的方法。完成端口与使用 TCP 无关,它们也适用于 UDP。只要您使用重叠读取,您使用什么协议(甚至是网络或磁盘,或其他一些可等待或可警报的内核对象)都没有关系。
是否为完成端口额外燃烧几百个周期对于延迟或 CPU 负载也无关紧要。我们在这里谈论的是“纳米”与“毫”,相差一到一百万。另一方面,完成端口总体而言是一个非常舒适、健全和高效的系统。
例如,当您没有及时收到 ACK 时,您可以简单地实现重新发送的逻辑(当您需要某种形式的可靠性时,您必须这样做,而 UDP 不会为您这样做),以及keepalive。
对于 keepalive,添加一个可等待计时器(可能会在 15 或 20 秒后触发),您每次收到 anything 时都会重置该计时器。如果您的完成端口告诉您此计时器已关闭,则您知道连接已失效。
对于重新发送,您可以例如在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 */
}
}
注意处理完成消息、用户任务和线程控制的整个逻辑是如何在一个简单的循环中发生的,没有晦涩的东西,没有复杂的路径,每个线程只执行相同的循环。
相同的代码适用于服务 1 个套接字的 1 个线程,或服务于 5,000 个套接字的 50 个线程池中的 16 个线程、10 个重叠文件传输以及执行并行计算。