在Delphi中,异步套接字编程的惯用方式是什么?

10

人们在Delphi中编写网络代码的普遍方式是使用Windows风格的重叠异步套接字I/O吗?

这里是我对这个问题的先前研究:

Indy组件似乎完全是同步的。另一方面,虽然ScktComp单元确实使用WSAAsyncSelect,但它基本上只是将一个BSD风格的多路复用套接字应用程序异步化。你被投入到单个事件回调中,就像你刚从select()循环中返回一样,并且必须自己完成所有状态机导航。

.NET情况要好得多,使用Socket.BeginRead / Socket.EndRead,其中继续传递直接到Socket.BeginRead,那里就可以重新开始了。作为闭包编码的继续 obviously具有您需要的所有上下文以及更多。

10个回答

2

我发现Indy在一开始看起来概念简单,但由于需要在应用程序终止时杀死套接字以释放线程,使其管理起来很麻烦。此外,在操作系统补丁升级后,我发现Indy库停止工作了。对我的应用来说,ScktComp工作得很好。


1
如果你需要手动关闭套接字,那么你就没有正确地使用它。Indy服务器可以自动处理这些事情。我应该知道——我是Indy开发团队的一员。至于操作系统升级,究竟什么出了问题? - Remy Lebeau
需要澄清的是,这是Indy 9,而不是Indy 10,这可能解释了需要在退出时手动关闭套接字的原因。操作系统安全补丁导致调用Write()被阻塞并且不返回(在XP SP2和3之间)。该应用程序已经运行了2年,在安全补丁后失败。考虑到这可能是硬件问题,部署团队将应用程序移动到第二个具有相同结果的服务器上。由于它是生产服务器,我们尝试替换ScktComp接口,一切都正常工作了。我没有时间使用内核调试器查找是什么原因导致调用被阻止。 - Mike

2

@Roddy - 我不需要同步套接字。为了可能长时间存在的连接而烧掉一个线程,这意味着你将并发连接的数量限制在进程可以包含的线程数上。由于线程使用大量资源 - 保留堆栈地址空间、已提交的堆栈内存和用于上下文切换的内核转换 - 当你需要支持数百个甚至数千个以上的连接时,它们无法扩展。


另一方面,非阻塞(异步)套接字涉及Windows消息队列,如果涉及数百或数千个连接,这也可能成为瓶颈。+ 我发现2011年3月的这篇文章非常有趣:使用Servlet 3.0异步处理将服务器吞吐量提高十倍 - mjn

1

有一个免费的IOCP(完成端口)套接字组件:http://www.torry.net/authorsmore.php?id=7131(包括源代码)

“由Naberegnyh Sergey N.开发的高性能套接字服务器基于Windows Completion Port并使用Windows Socket扩展。支持IPv6。”

我在寻找更好的组件/库来重新架构我的小型即时通讯服务器时发现了它。虽然我还没有尝试过,但第一印象是它看起来编码良好。


1
人们在 Delphi 中编写网络代码的正常方式是什么?使用 Windows 风格的重叠异步套接字 I/O 吗?
嗯,Indy 已经成为套接字 I/O 的“标准”库有一段时间了 - 它是基于阻塞套接字的。这意味着如果您想要异步行为,则使用其他线程来连接/读取/写入数据。在我看来,这实际上是一个主要优点,因为不需要管理任何状态机导航,也不必担心回调过程或类似的东西。我发现我的“读取”线程的逻辑比非阻塞套接字允许的更简洁、更具可移植性。
对我们来说,Indy 9 大部分时间都非常稳定、快速和可靠。然而,对于 Tiburon 转移到 Indy 10 却让我有些担心。
@mike:“……需要杀死套接字以释放线程……”。
这让我感到困惑,直到我想起我们的线程库使用基于异常的技术来安全地终止“等待”线程。我们调用QueueUserAPC来排队一个函数,该函数引发一个C++异常(不是从Exception类派生的),应该只由我们的线程包装程序捕获。所有析构函数都被调用,因此所有线程都会干净地终止并在退出时进行整理。

1
正是在Indy9到Indy10 API不稳定的阶段,我开始了我的“远离Indy”的阶段。现在我只使用ICS,我非常满意。这不是因为同步/异步的问题,而是因为所有的故障(特别是在应用程序关闭时出现的挂起)和整个IdAntifreeze的事情让我厌倦了Indy。 - Warren P
+1 表示使用阻塞式套接字改善代码清晰度。 - mjn

1
"同步套接字不是我想要的。"
明白了 - 但我认为在这种情况下,你最初问题的答案是因为Delphi实际上没有异步套接字IO的习惯用语,因为它实际上是一个高度专业化和不常见的需求。
作为一个副问题,您可能会发现这些链接有趣。它们都有点老旧,而且更*nxy比Windows。第二个链接暗示,在正确的环境中,线程可能并不像你想象的那样糟糕。 The C10K problem Why Events Are A Bad Idea (for High-concurrency Servers)

1

@Chris Miller - 你在回答中提到的事实是错误的。

Windows 消息风格异步,如通过 WSAAsyncSelect 可用,实际上主要是解决了 Win 3.x 时代缺乏适当线程模型的问题。

然而,.NET Begin/End 并没有使用额外的线程。相反,它使用了重叠 I/O,利用 WSASend / WSARecv 上的额外参数,特别是重叠完成例程,来指定后续操作。

这意味着 .NET 风格利用 Windows 操作系统的异步 I/O 支持,避免通过在套接字上阻塞线程来消耗资源。

由于线程通常是昂贵的(除非您为 CreateThread 指定一个非常小的堆栈大小),因此在套接字上阻塞线程将会阻止您扩展到数万个并发连接。

这就是为什么如果您想要扩展,必须使用异步 I/O,并且 .NET 是 不是简单地“使用线程,[...]只是由框架进行管理”。


3
考虑到我不信任 INDY 组件集,而 ICS 是一个没有 IOCP 支持的第三方异步组件集,因此我认为 Embarcadero 应该站出来构建一个适当的异步 + iocp 网络层。INDY 是当前 VCL 企业数据库网络层底层,这实在是糟糕透了,这也是我不想碰这个摇摇欲坠的东西的原因。 - Warren P

1

@Roddy - 我已经阅读了你提到的链接,它们都是来自Paul Tyma的演示文稿 "Thousands of Threads and Blocking I/O - The old way to write Java Servers is New again" 的参考资料。

然而,有些东西并不一定会从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或(类似)并不是很大的空间。

现在,关于异步I/O在Windows上的非性能问题,我不是100%确定-我必须测量才能确定。然而,我知道的是,关于线程编程比异步事件驱动编程更容易的论点,呈现了一个错误的二分法

异步代码不需要基于事件;它可以基于续体,就像在.NET中一样,如果您将闭包指定为续体,则可以免费为您维护状态。此外,从线性线程样式代码到传递样式异步代码的转换可以通过编译器(CPS变换是机械的)进行机械化,因此在代码清晰度方面也没有成本。


我想你仍然需要担心在使用闭包时的数据访问同步,或者所有通信都是序列化的吗?这如何与例如异步数据库响应相结合,以提供大量传入请求的数据?使用工作线程池?你应该写个博客记录一下。 - Lars Fosdal
Indy的线程基于VCL的TThread类,该类不允许您指定堆栈大小。 - Remy Lebeau

0
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是一个直截了当的任务。

0

0

使用ScktComp类时,需要使用ThreadBlocking服务器而不是NonBlocking服务器类型。使用OnGetThread事件将ClientSocket参数移交给您自己设计的新线程。一旦您实例化了TServerClientThread的继承实例,就可以在线程内创建TWinSocketStream的实例,并用它来读写套接字。这种方法使您远离尝试在事件处理程序中处理数据的困境。这些线程可以存在于仅需要读取或写入的短暂时期,或者为了被重复使用而持续挂起。

编写套接字服务器是一个相当广泛的主题。您可以选择实施许多技术和实践。使用TServerClientThread在同一个套接字上进行读写操作的方法对于简单的应用程序来说是非常直接和好用的。如果您需要高可用性和高并发性的模型,则需要研究Proactor模式等模式。

祝你好运!


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