使用select()和带有MSG_PEEK的recv之间的效率比较。异步。

3
我想了解在检查传入数据(异步)时最有效的方法是什么。假设我有500个连接,我可以想到以下3种情况:
  1. 使用select()一次检查FD_SETSIZE个套接字,然后迭代所有套接字以接收数据。(这不需要为每个套接字返回两次调用recv吗?MSG_PEEK分配一个缓冲区,然后再次recv(),这与#3相同)
  2. 使用select()逐个检查一个套接字。(这也不像#3吗?它需要两次调用recv。)
  3. 使用recv()和MSG_PEEK逐个套接字接收数据,分配缓冲区,然后再次调用recv()。这样做会更好,因为我们可以跳过所有对select()的调用吗?还是一个recv()的开销太大了?
我已经编写了情况1和2的代码,但我不确定该使用哪一个。如果我表达不清楚,对不起。
谢谢。

如果您的系统支持,建议使用poll()而不是select(),因为它避免了FD_SETSIZE限制。 - David Gelhar
3个回答

4

FD_SETSIZE通常是1024,因此您可以一次检查所有500个连接。然后,您将仅在已准备好的连接上执行两个recv调用-例如,在非常繁忙的系统中,每次循环大约有半打这样的连接。使用其他方法,您需要进行大约500个额外的系统调用(在许多数百个套接字上执行巨大数量的“失败”的recvselect调用,这些套接字在任何给定时间都不会准备好!)。

此外,使用第一种方法,您可以阻塞直到至少一个连接准备就绪(在这种情况下没有开销,这在并不那么繁忙的系统中并不罕见)-使用其他方法,您需要“轮询”,即持续不断地翻转,无益地消耗大量CPU(或者,如果您在每次检查循环之后休眠一段时间,那么尽管系统根本不忙碌,但您仍然需要延迟响应)。-)。

这就是为什么我认为轮询是一种反模式:经常使用,但仍然具有破坏性。有时您绝对没有替代方案(这基本上告诉您,您必须与设计非常糟糕的系统进行交互-遗憾的是,在这个不完美的生活中,有时您确实需要!),但是当任何体面的替代方案存在时,仍然进行轮询实际上是一种非常糟糕的设计实践,应该避免。


谢谢提供这个信息。我不知道FD_SETSIZE通常是1024,在我的情况下,它设置为64。我正在使用Visual C++ Express。 - Marlon
2
@Marlon,嗯,如果我没记错的话,在Windows上,这样的内核参数设置方式强烈依赖于“版本”,基本上如果你想在客户端版本上执行服务器任务,你会受到严重限制(从而被鼓励升级到服务器版,并相应地支付许可费用)。如果您可以访问用于开发的Windows服务器(正如你肯定应该为部署准备),那么您可能需要检查一下(或者,任何Mac或Linux桌面都可以更好地为您服务;-)。 - Alex Martelli
@Tony,不确定这是否“恶劣”——当一个人决定使用商业平台时,当然必须预计供应商将采取行动来尝试最大化他们的利润,毕竟...这是你为这个选择支付的总体价格(货币和战略),然而许多个人和公司仍然决定去做,毫无疑问,出于他们自己深思熟虑的原因。除非能安排一台专门用于开发和分期的服务器(或者也许通用许可证和/或非Express编译器会有所帮助),否则它可能会使开发变得更加困难。 - Alex Martelli
@Alex Martelli,关于您对轮询作为反模式的看法:我们可以采用智能轮询循环:如果上一次轮询请求发现有工作要做,则立即执行该工作并立即发出下一个请求。否则,在发出下一个请求之前稍等一会儿。 - Anton Shepelev

3
您可以在以下3种情况下进行一些效率模拟:
情况A(0/500个传入数据)
- 对于解决方案#1,您只需调用单个select()。 - 对于解决方案#2,您需要500个select()。 - 对于解决方案#3,您需要500个recv()。
情况B(250/500个传入数据)
- 对于解决方案#1,单个select()+(500个recv())。 - 对于解决方案#2,500个select()+(500个recv())。 - 对于解决方案#3,750个recv()。
**假设跳过无缓冲区大小的套接字 @ 无传入数据
答案是显而易见的 :)

1
......在异步检查传入数据时最有效。假设我有500个连接。我有3种情况(我能想到的):

使用select()一次检查FD_SETSIZE个套接字,然后迭代所有套接字以接收数据。(这不需要为每个返回的套接字调用两次recv吗?MSG_PEEK分配缓冲区,然后再次recv()它,这与#3相同)

我相信您正在仔细构建仅包含当前连接描述符的fd集合……?然后,您遍历该集合,并仅对具有读取或异常/错误条件(后者差异在于BSD和Windows实现之间)的套接字发出recv()。虽然从功能上讲可以(并且在概念上可以说是优雅的),但在大多数实际应用程序中,您不需要在recv之前进行窥视:即使您不确定消息大小并知道可以从缓冲区中窥视它,您也应该考虑是否可以:

  • 分块处理消息(例如,读取每个良好工作单元 - 可能为8k,处理它,然后将下一个小于等于8k的内容读入相同的缓冲区中...)
  • 读入足以容纳大多数/所有消息的缓冲区,仅在发现消息不完整时动态分配更多。

使用select()逐个检查套接字。 (这难道不也像#3吗?它需要两次调用recv。)

一点也不好。如果您保持单线程,则需要在select上放置0超时值,并通过监听和客户端描述符不断旋转。非常浪费CPU时间,并且会极大地降低延迟。

使用带有MSG_PEEK 的recv()逐个套接字,分配缓冲区,然后再次调用recv()。这样做是否更好,因为我们可以跳过所有对select()的调用?还是一个recv()调用的开销太大?

(忽略不推荐使用MSG_PEEK)-你如何知道在哪个套接字上使用MSG_PEEK或recv()?如果你是单线程的,那么要么你会在第一次peek/recv尝试时阻塞,要么你会使用非阻塞模式,然后通过所有描述符旋转来希望peek/recv会返回一些内容。这很浪费时间。
因此,要么坚持使用一个套接字,要么使用多线程模型。对于后者,最简单的方法是让监听线程循环调用accept,并且每次accept返回新的客户端描述符时,它应该生成一个新的线程来处理连接。这些客户端连接处理线程可以在recv()中简单地阻塞。这样,操作系统本身就进行监视并在事件响应中唤醒线程,您可以相信它将是合理高效的。虽然这种模型听起来很容易,但是你应该意识到多线程编程还有很多其他的复杂性-如果你还不熟悉它,你可能不想同时尝试学习socket I/O和多线程编程。

在我看来,为每个连接创建一个新线程是一种不好的设计。 - YeenFei
这不是很好,但对于刚学习如何做这个的人来说,这是进入多线程最简单的方式,并且在数据开始流动后使实际的recv()调用非常快。这就是我们被要求解决的功能规范。线程池更好,但更复杂,特别是如果你从头开始实现它们。 - Tony Delroy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接