微软异步服务器套接字示例

5

我有一个关于this问题的疑问(“异步服务器套接字多个客户端”)。

要么微软已经改变了示例,要么我真的不明白——在示例中它说:

        while (true) {
            // Set the event to nonsignaled state.
            allDone.Reset();

            // Start an asynchronous socket to listen for connections.
            Console.WriteLine("Waiting for a connection...");
            listener.BeginAccept( 
                new AsyncCallback(AcceptCallback),
                listener );

            // Wait until a connection is made before continuing.
            allDone.WaitOne();
        }

据我所知,BeginAccept()函数在while(true)循环中被连续调用,只有在AcceptCallback()函数被调用并且第一件事情发生的时候才会停止,因为那里发生的第一件事情就是allDone.Set()。
但是Groo说:
“MSDN示例的问题在于它只允许连接一个客户端(listener.BeginAccept只调用了一次)。”
实际上,我不明白为什么要使用ManualResetEvent allDone...
而且我认为listener.EndAccept(ar)方法无论如何都是阻塞的。
如果在运行时再次调用listener.BeginAccept()会抛出异常吗? 但如果是这样的话,为什么allDone.Set()在listener.EndAccept(ar)之前?
还有一个问题:
在ReadCallback(IAsyncResult ar)函数中,当我收到EOF后,我可以直接调用handler.BeginReceive(...)来等待来自同一客户端的更多传入数据吗?
有经验的人可以解释一下吗?
谢谢!

1
我回答那个问题已经有一段时间了,但是如果我没记错的话,原始示例没有循环。我们可以检查那个时期的互联网档案页面,但目前该服务器似乎已经宕机了。 - vgru
如果旧问题有指向更新源代码的链接,那么就存在一个问题。 - fose
2个回答

8
可能这个示例实际上已经在其他SO答案发布后进行了更新。或者可能回答者Groo自己并没有完全理解这个示例。在任何情况下,您都正确地观察到他声称只能接受一个客户端是不正确的。
我同意usr写的部分内容,但对整个事情有一些不同的看法。而且,我认为这些问题应该得到更全面和具体的处理。
首先,虽然我同意通常发出后续调用 BeginAccept()在接受回调方法中而不是使用循环的优越设计,但在示例中的实现本身没有什么问题。在之前发出的调用完成之后才会进行新的 BeginAccept() 调用; 事件句柄用于将调用 BeginAccept()的线程与最终处理完成的线程同步。只有在之前发出的接受完成后,第一个线程才会被释放,并且在该线程再次阻塞之前,只进行一次新的 BeginAccept() 调用。
这有点别扭,但完全没关系。这个示例的作者可能认为,既然在他的控制台程序中,他总会有一个空闲的线程,他也可以给它做些事情。:)
无论如何,回答问题#1:您是正确的,在该链接中当前存在的示例确实允许多个客户端连接。
问题#2,为什么使用事件句柄,我希望上面的解释已经回答了这个问题。它是示例用于释放调用 BeginAccept() 的线程的方式,以便在前一个调用完成后可以再次调用它。
问题#3,EndAccept() 是阻塞的吗?有点。如果在接受操作实际完成之前调用 EndAccept(),那么是的,它将被阻止。但在这种情况下,它只在完成回调被调用时才被调用。此时,我们可以确定调用 EndAccept() 将不会阻塞。假设没有异常,它所做的只是在那一点上检索已完成操作的结果。
问题#4,是否可以在调用 EndAccept() 之前第二次调用 BeginAccept()?是的,即使不能在多个接受操作排队(实际上可以)。在这里,对第一个 BeginAccept() 的完成回调中发生了 BeginAccept() 的调用。也就是说,虽然代码还没有调用 EndAccept(),但接受操作本身已经完成,因此这甚至不是有多个接受操作未完成的情况。接收和发送操作同样自由;您可以在任何一个操作完成之前多次合法地调用所有这些方法。
问题#5,即使我已经收到,我可以调用 BeginReceive() 吗?可以。实际上,这是MSDN示例中存在缺陷的领域之一,在该示例中,一旦接收到了期望数据的最后一个部分,它就不会继续接收。事实上,直到收到的字节数为0的接收完成为止,它仍然应该始终再次调用BeginReceive(),无论是否还需要更多数据,然后通过在那个点调用Shutdown(SocketShutdown.Both)处理已完成字节数为0的接收来表示连接正常关闭的确认(假设此时它已经发送完毕,那么此时它将已经调用了Shutdown(SocketShutdown.Send)...如果没有,它只需要使用SocketShutdown.Receive和/或直到发送完毕并且可以使用SocketShutdown.Both才调用Shutdown...SocketShutdown.Receive实际上对连接本身没有任何重要作用,所以等待SocketShutdown.Both是合理的)。
换句话说,即使服务器确信客户端不会发送任何其他数据,正确使用套接字API仍应尝试执行另一个接收操作,寻找指示客户端实际上已经开始关闭连接的0字节返回值。只有在那时,服务器才应开始自己的关闭过程并关闭套接字。
最后,你没有提但是usr提出了这个问题:我不同意这个MSDN示例今天没有任何相关性。不幸的是,Microsoft没有为Socket类提供基于任务的异步API版本。还有一些其他支持异步/等待(例如TcpClient/NetworkStream)的网络API,但如果您想直接使用Socket类,则必须使用旧的异步模型(Socket有两种,都基于回调)。
您可以将同步Socket方法包装在Tasks中作为替代旧API,但这样做将失去Socket类中基于I/O完成端口的异步API的优势。更好的选择是某种仍使用底层异步API的Task兼容包装器,但这种方式有些复杂,我目前不知道这种东西是否存在(但它肯定可以存在,因此如果您希望使用异步/等待,则可能值得对其进行一些网络搜索)。
希望能对您有所帮助!我为答案过长而道歉,但这是一个相当广泛的问题(几乎太广泛了,但对我来说似乎仍在合理的SO范围内 :))。

哇,感谢您详细回答问题,@Peter Duniho!我非常感激!感谢您的帮助和时间,大家。 - fose

0

这个示例有些混淆。事件是不必要的。相反,接受回调应该发出下一个接受操作,以便始终存在一个未完成的接受。

在未经限制的循环中调用BeginAccept是不正确的,因为那样会启动无限数量的未完成接受操作。

您是否知道自从引入await后,旧的APM已经过时了?所有这些代码今天都没有意义。

此外,请注意,网络上大多数套接字代码都存在不同的缺陷。


谢谢你的回答,它引导我提出以下问题:
你是否知道自从引入await后,旧的APM已经过时了?所有这些代码今天都没有意义。
- fose
抱歉,之前的回答是错误的... @usr 但感谢您的回答,它引导我提出以下问题:
  • 下一个accept回调是在调用listener.BeginAccept()吗?我理解正确的话,listener.EndAccept()会等待下一个传入的连接吗?
  • 您认为使用TCPListener和TCPClient是否更好?
“您是否知道自从引入await后,旧的APM已经过时了?所有这些代码今天都没有意义。” 您的意思是最好使用EAP吗?再次感谢 ^^
- fose
Begin和End是成对出现的。任何给定的End调用都与Begin调用匹配并获取其结果。End只是给你已经接受的连接。接下来,再次调用Begin以开始重新接受。TcpListener/Client是薄包装器。它们应该被优先考虑使用。EAP也已经过时了。请使用带有NetworkStream的await。使用异步IO方法,如ReadAsync。 - usr
好的,谢谢@usr提供的答案。你真的让我朝着正确的方向前进了——甚至让我思考了APM / EAP / await等问题... - fose

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