在 Delphi 中进行异步套接字编程的惯用方法是啥?
Posted
技术标签:
【中文标题】在 Delphi 中进行异步套接字编程的惯用方法是啥?【英文标题】:What's the idiomatic way to do async socket programming in Delphi?在 Delphi 中进行异步套接字编程的惯用方法是什么? 【发布时间】:2010-09-07 10:07:31 【问题描述】:人们在 Delphi 中编写网络代码使用 Windows 风格的重叠异步套接字 I/O 的正常方式是什么?
这是我之前对这个问题的研究:
Indy 组件似乎完全同步。另一方面,虽然 ScktComp 单元确实使用 WSAAsyncSelect,但它基本上只异步 BSD 风格的多路复用套接字应用程序。你会在一个事件回调中被转储,就好像你刚刚从 select() 循环返回一样,并且必须自己完成所有状态机导航。
.NET 的情况要好得多,使用 Socket.BeginRead / Socket.EndRead,其中的延续直接传递给 Socket.BeginRead,而这就是您恢复的地方。一个被编码为闭包的延续显然拥有你需要的所有上下文,甚至更多。
【问题讨论】:
【参考方案1】:我发现 Indy,虽然一开始是一个更简单的概念,但由于需要在应用程序终止时终止套接字以释放线程,因此难以管理。此外,我让 Indy 库在操作系统补丁升级后停止工作。 ScktComp 非常适合我的应用程序。
【讨论】:
如果您必须手动终止套接字,那么您没有正确使用它。 Indy 服务器会自动为您处理这些内容。我应该知道 - 我在 Indy 开发团队。至于操作系统升级,究竟是什么停止了工作? 澄清一下,这是 Indy 9,而不是 Indy 10,它可以解释在退出时需要手动关闭套接字。操作系统安全补丁导致对 Write() 的调用被阻塞而不返回(在 XP SP2 和 3 之间)。该应用程序运行了 2 年,但在安全补丁后失败。部署团队认为这是硬件,将应用程序移至第二台服务器,结果相同。由于它是生产服务器,我们尝试替换 ScktComp 接口,一切正常。我没有时间使用内核调试器来找出导致调用阻塞的原因。【参考方案2】:人们的正常写作方式是什么 Delphi中使用的网络代码 Windows 风格的重叠异步 套接字 I/O?
好吧,Indy 长期以来一直是套接字 I/O 的“标准”库——它基于阻塞套接字。这意味着如果您想要异步行为,您可以使用额外的线程来连接/读取/写入数据。在我看来,这实际上是一个主要优势,因为不需要管理任何类型的状态机导航,或者担心回调过程或类似的东西。我发现我的“阅读”线程的逻辑比非阻塞套接字允许的更简洁,更便携。
Indy 9 对我们来说基本上是防弹、快速和可靠的。然而,Tiburon 迁移到 Indy 10 让我有点担心。
@Mike:“……需要杀死套接字以释放线程……”。
这让“嗯?”直到我记得我们的线程库使用基于异常的技术来安全地终止“等待”线程。我们调用QueueUserAPC 来对引发C++ 异常(不是从类Exception 派生)的函数进行排队,该异常只能被我们的线程包装程序捕获。所有的析构函数都被调用,所以线程都干净地终止并在退出时整理。
【讨论】:
在 Indy9 到 Indy10 API 不稳定阶段,我开始了我的“远离 Indy”阶段。我现在只使用 ICS,我对此非常满意。这不是因为同步/异步的事情,而是所有的故障(尤其是在应用程序关闭时挂起)和整个 IdAntifreeze 事情让我厌倦了 Indy。 +1 提到通过阻塞套接字提高了代码清晰度【参考方案3】:@Roddy - 同步套接字不是我想要的。为了可能长期存在的连接而刻录整个线程意味着您将并发连接的数量限制为您的进程可以包含的线程数。由于线程使用大量资源 - 保留的堆栈地址空间、已提交的堆栈内存和用于上下文切换的内核转换 - 当您需要支持数百个连接时,它们无法扩展,更不用说数千个或更多了。
【讨论】:
另一方面,非阻塞(异步)套接字涉及 Windows 消息队列,如果涉及成百上千个连接,这也可能成为瓶颈。 + 我觉得这篇 2011 年 3 月的文章很有趣:Tenfold increase in server throughput with Servlet 3.0 asynchronous processing【参考方案4】:“同步套接字不是我想要的。”
了解 - 但我认为在这种情况下,您最初的问题的答案是没有用于异步套接字 IO 的 Delphi idiom,因为它实际上是一个高度专业化且不常见的要求。 p>
作为一个附带问题,您可能会发现这些链接很有趣。它们都比 Windows 有点老,而且更 *nxy。第二个暗示 - 在正确的环境中 - 线程可能没有你想象的那么糟糕。
The C10K problem
Why Events Are A Bad Idea (for High-concurrency Servers)
【讨论】:
【参考方案5】:Indy 使用同步套接字,因为它是一种更简单的编程方式。早在 Windows 3.x 时代,异步套接字阻塞就被添加到了 winsock 堆栈中。 Windows 3.x 不支持线程,没有线程就无法进行套接字 I/O。有关 Indy 为何使用阻塞模型的更多信息,请参阅this article。
.NET Socket.BeginRead/EndRead 调用使用线程,它只是由框架而不是由您管理。
@Roddy,自 Delphi 2006 以来,Indy 10 已与 Delphi 捆绑在一起。我发现从 Indy 9 迁移到 Indy 10 是一项简单的任务。
【讨论】:
【参考方案6】:@Chris Miller - 你在回答中所说的实际上是不准确的。
通过 WSAAsyncSelect 提供的 Windows 消息样式异步在很大程度上是一种解决方法,因为在 Win 3.x 时代缺乏适当的线程模型。
然而,.NET Begin/End不使用额外的线程。相反,它使用重叠 I/O,使用 WSASend / WSARecv 上的额外参数,特别是重叠完成例程,来指定继续。
这意味着 .NET 样式利用 Windows 操作系统的异步 I/O 支持来避免通过阻塞套接字来烧毁线程。
由于线程通常比较昂贵(除非您为 CreateThread 指定非常小的堆栈大小),因此在套接字上阻塞线程会阻止您扩展到 10,000 个并发连接。
这就是为什么如果您想扩展,使用异步 I/O 很重要,也是为什么 .NET 不是,我再说一遍,是 不是,只是“使用线程,[...] 仅由框架管理”。
【讨论】:
鉴于 INDY 是一个我不信任的组件集,而 ICS 是一个没有 IOCP 支持的异步异步组件集,我认为 Embarcadero 应该站出来构建一个适当的异步+iocp 网络层。 INDY 是当前 VCL 企业数据库网络层东西下面的层这一事实非常糟糕,这也是我不会用 10 英尺长的杆子碰摇摇晃晃的一堆东西的原因。【参考方案7】:@Roddy - 我已经阅读了您指向的链接,它们都引用自 Paul Tyma 的演示文稿“数千个线程和阻塞 I/O - 编写 Java 服务器的旧方法又是新方法”。
然而,Paul 的演示中不一定会跳出来的一些事情是,他在启动时为 JVM 指定了 -Xss:48k,并且他假设 JVM 的 NIO 实现是高效的,以便它能够是一个有效的比较。
Indy 确实不指定类似的缩小和严格限制的堆栈大小。 Indy 代码库中没有调用 BeginThread(Delphi RTL 线程创建例程,您应该在这种情况下使用)或 CreateThread(原始 WinAPI 调用)。
默认堆栈大小存储在 PE 中,对于 Delphi 编译器,默认为 1MB 的保留地址空间(空间由 OS 以 4K 块逐页提交;实际上,编译器需要生成代码如果函数中有> 4K的局部变量,则触摸页面,因为扩展由页面错误控制,但仅适用于堆栈中的最低(保护)页面)。这意味着在最多 2,000 个并发线程处理连接之后,您将用完地址空间。
现在,您可以使用 $M minStackSize [,maxStackSize] 指令更改 PE 中的默认堆栈大小,但这会影响 所有 线程,包括主线程。我希望你不要做太多的递归,因为 48K 或(类似的)空间并不大。
现在,Paul 对于 Windows 异步 I/O 的非性能是否正确,我不能 100% 确定 - 我必须对其进行测量才能确定。然而,我所知道的是,关于线程编程比基于事件的异步编程更容易的论点呈现出一种错误的二分法。
异步代码不需要是基于事件的;它可以是基于延续的,就像在 .NET 中一样,如果你指定一个闭包作为你的延续,你可以免费为你维护状态。此外,从线性线程式代码到连续传递式异步代码的转换可以通过编译器进行机械化(CPS 转换是机械式的),因此代码清晰度也不需要任何成本。
【讨论】:
我猜您在使用闭包时仍然需要担心数据访问同步,还是所有通信都被序列化了?这如何与 f.x 结合使用。异步的。为大量传入请求提供数据的数据库响应?使用工作线程池?你应该写这篇博客。 Indy 的线程基于 VCL 的 TThread 类,它不允许你指定堆栈大小。【参考方案8】:对于异步的东西,试试 ICS
http://www.overbyte.be/frame_index.html?redirTo=/products/ics.html
【讨论】:
【参考方案9】:使用 ScktComp 类,您需要使用 ThreadBlocking 服务器而不是 NonBlocking 服务器类型。使用 OnGetThread 事件将 ClientSocket 参数传递给您设计的新线程。一旦您实例化了一个继承的 TServerClientThread 实例,您将创建一个 TWinSocketStream 实例(在线程内),您可以使用它来读取和写入套接字。此方法使您无需尝试在事件处理程序中处理数据。这些线程可能只存在需要读取或写入的短暂时间,也可能会在一段时间内挂起以便被重用。
编写套接字服务器的主题相当广泛。您可以选择实施许多技术和实践。在 TServerClientThread 中读取和写入同一个套接字的方法很简单,适用于简单的应用程序。如果您需要一个高可用性和高并发的模型,那么您需要研究像 Proactor 模式这样的模式。
祝你好运!
【讨论】:
【参考方案10】:有一个免费的 IOCP(完成端口)套接字组件:http://www.torry.net/authorsmore.php?id=7131(包含源代码)
“作者 Naberegnyh Sergey N.. 高 基于性能的套接字服务器 Windows 完成端口和使用 Windows 套接字扩展。 IPv6 支持的。 "
我在寻找更好的组件/库来重新架构我的小型即时消息服务器时发现了它。我还没有尝试过,但它作为第一印象看起来不错。
【讨论】:
以上是关于在 Delphi 中进行异步套接字编程的惯用方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章