【问题标题】:Stack overflow when using the System.Net.Sockets.Socket.AcceptAsync model使用 System.Net.Sockets.Socket.AcceptAsync 模型时的堆栈溢出
【发布时间】:2010-10-17 18:37:14
【问题描述】:

对于 C# 和 .NET 的 System.Net.Sockets.Socket.AcceptAsync 方法,需要处理“false”的返回值,以便从同步处理的连接中处理立即可用的 SocketAsyncEventArgs 状态。 Microsoft 提供了示例(在System.Net.Sockets.SocketAsyncEventArgs 类页面上找到),如果存在大量未决连接,将导致堆栈溢出,这可以在任何实现其处理模型的系统上被利用。

解决此问题的其他想法是创建一个调用处理程序方法的循环,条件是 Socket.AcceptAsync 返回的值等于 false,如果value 表示操作正在异步完成(true)。但是,这个解决方案也会导致堆栈溢出漏洞,因为与传递给Socket.AcceptAsyncSocketAsyncEventArgs 关联的回调在方法的末尾有一个对Socket.AcceptAsync 的调用,它也有一个立即执行的循环可用的、同步接受的连接。

如您所见,这是一个非常可靠的问题,我还没有找到一个不涉及System.Threading.ThreadPool 和创建大量其他方法和调度处理的好解决方案。据我所知,与Socket.AcceptAsync 相关的异步套接字模型需要的不仅仅是 MSDN 上的示例中演示的内容。

有没有人有一个干净有效的解决方案来处理从 Socket.AcceptAsync 同步接受的立即挂起的连接,而无需创建单独的线程来处理连接并且不使用递归?

【问题讨论】:

  • 是的,MSDN 上的示例并不总是最好的(或最佳实践,就此而言)。你能澄清你的问题吗?您是否正在寻找一种使用不会导致 StackOverflowExceptoin 的异步套接字的方法?
  • 没错;我正在寻找一种非递归方式来处理立即挂起的连接。

标签: c# sockets asynchronous


【解决方案1】:

我不会使用AcceptAsync,而是使用BeginAccept/EndAccept,并正确实现常见的异步模式,即检查CompletedSynchronously以避免在回调线程中对完成的操作进行回调。

另见AsyncCallBack CompletedSynchronously


修改使用AcceptAsync的要求:

MSDN documentation 明确表示不会为同步完成的操作调用回调。这与总是调用回调的常见异步模式不同。

如果 I/O 操作是,则返回 true 待办的。这 SocketAsyncEventArgs.Completed 事件 在 e 参数将被提出 操作的完成。退货 如果 I/O 操作完成,则返回 false 同步。这 SocketAsyncEventArgs.Completed 事件 在 e 参数上不会被提升 和作为参数传递的 e 对象 后可立即检查 方法调用返回以检索 运算结果。

我目前看不出循环如何解决堆栈溢出问题。也许您可以更具体地说明导致问题的代码?


编辑 2:我正在考虑这样的代码(仅针对 AcceptAsync,其余的只是为了获得一个可以运行的应用程序来试用它):

static void Main(string[] args) {
    Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    listenSocket.Bind(new IPEndPoint(IPAddress.Loopback, 4444));
    listenSocket.Listen(100);
    SocketAsyncEventArgs e = new SocketAsyncEventArgs();
    e.Completed += AcceptCallback;
    if (!listenSocket.AcceptAsync(e)) {
        AcceptCallback(listenSocket, e);
    }
    Console.ReadKey(true);
}

private static void AcceptCallback(object sender, SocketAsyncEventArgs e) {
    Socket listenSocket = (Socket)sender;
    do {
        try {
            Socket newSocket = e.AcceptSocket;
            Debug.Assert(newSocket != null);
            // do your magic here with the new socket
            newSocket.Send(Encoding.ASCII.GetBytes("Hello socket!"));
            newSocket.Disconnect(false);
            newSocket.Close();
        } catch {
            // handle any exceptions here;
        } finally {
            e.AcceptSocket = null; // to enable reuse
        }
    } while (!listenSocket.AcceptAsync(e));
}

【讨论】:

  • 不幸的是 BeginAccept/EndAccept 不符合我的需要,这是因为需要为每个操作实例化 IAsyncResult 对象。我的服务器需要大量操作,而分析表明这是应用程序中的压力点。即便如此,所提供的解决方案通过在不同的线程上调用回调,或者使用一些奇怪的系统来保持来自单独线程的接受调用的速度,从而破坏了同步完成的目的。其他人做出的回应与你的基本相同。
  • 响应您的编辑,问题出现在通过从同一线程调用 AcceptAsync 处理程序来处理立即可用的 SocketAsyncEventArgs 的情况下,例如您检查返回值是否为 false,然后只需使用 Socket.AcceptAsync 中使用的 SocketAsyncEventArgs 的参数调用处理程序。然后,在接受处理程序中,最后使用相同的模式来确保正确处理所有连接(同步或异步)。
  • 响应您对代码示例的第二次编辑,这正是我在发布对我的问题的答案时所想的。感谢您发布代码,因为有些人可能不明白我试图通过文本表达什么。我相信是您的回应引发了我对如何解决问题的想法;再次感谢。虽然我必须说混合同步和异步套接字工作是令人讨厌的,呵呵。但是,嘿,它让想法得到了理解。
  • 感谢您的回答。当然,将同步代码放在异步处理程序中并不是处理事情的方式,但这就是为什么我明确解释这只是为了测试AcceptAsync 代码(您实际上可以将其粘贴到控制台应用程序主类中,它会跑)。 ;-)
【解决方案2】:

我通过简单地改变循环的位置解决了这个问题。不是从自身内部递归调用接受处理程序,而是将代码包装在一个 do-while 循环中,条件为“!Socket.AcceptAsync(args)”可以防止堆栈溢出。

这背后的原因是您利用回调线程来处理立即可用的连接,然后再费心异步等待其他连接出现。它有效地重用了一个池线程。

我很欣赏这些回复,但由于某种原因,他们都没有点击我,也没有真正解决问题。然而,似乎里面有什么东西让我想到了这个主意。它避免了手动使用 ThreadPool 类并且不使用递归。

当然,如果有人有更好的解决方案甚至替代方案,我很乐意听到。

【讨论】:

  • 听起来很像你写评论时我一直在拼凑的东西。这就是为什么我问循环不能解决问题的原因,因为在循环时不会调用回调,没有递归......
  • @Michael 我意识到这已经快 4 岁了,但我目前正在解决同样的问题,我无法真正理解你做了什么。你还记得吗?
  • @Rotem 是的,我愿意!基本上我最初是从Completed 事件回调中调用AcceptAsync,如果它同步完成,我只是调用相同的事件回调。如果许多调用同步完成,这将导致堆栈溢出,这在频繁加载时很常见。我没有这样做,而是做了一个循环,例如:while (!acceptor.AcceptAsync(state)) { /* do stuff */ }
【解决方案3】:

我没有仔细看,但闻起来可能会有所帮助(请参阅名为“堆栈潜水”的部分):

http://blogs.msdn.com/b/mjm/archive/2005/05/04/414793.aspx

【讨论】:

  • 我们不是在谈论 Windows Communication Foundation。虽然这是一个类似的问题,但它是无关紧要的。通过在单独的线程上启动处理程序,它们看起来好像只是在破坏同步完成操作的目的。反过来,将它放在单独的线程上只会导致处理程序线程上的堆栈溢出。
  • 明确地说,我提到的部分不仅仅是关于 WCF,它是关于编写任何具有 begin-end 模式的异步循环。
  • 我重读了几遍并检查了它,然后澄清了我的评论。如果它被认为是不屑一顾,我深表歉意。这不是我想要的,感谢您的意见。
【解决方案4】:
newSocket.Send(Encoding.ASCII.GetBytes("Hello socket!")); 
newSocket.Disconnect(false); 
newSocket.Close(); 

上面这个sn-p的问题是这会阻塞你的下一个accept操作。

更好的方法是这样的:

while (true)
{
   if (e.SocketError == SocketError.Success)
   {
      //ReadEventArg object user token
      SocketAsyncEventArgs readEventArgs = m_readWritePool.Pop();
      Socket socket = ((AsyncUserToken)readEventArgs.UserToken).Socket = e.AcceptSocket;

      if (!socket.ReceiveAsync(readEventArgs))
         ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessReceiveEx), readEventArgs); .
    }
    else
    {
       HadleBadAccept(e);
    }

    e.AcceptSocket = null;

    m_maxNumberAcceptedClients.WaitOne();
    if (listenSocket.AcceptAsync(e))
       break;
}

【讨论】:

  • 这是对我 2 岁投反对票的评论。我对此投了反对票,因为这对问题的上下文没有意义。我没有发布代码示例,所以您似乎是在回复这个问题的答案,而不是问题本身。这应该是对已接受答案的两句话评论。
猜你喜欢
  • 2019-05-18
  • 2015-03-28
  • 2013-10-30
  • 1970-01-01
  • 2015-06-09
  • 1970-01-01
  • 2021-05-28
  • 2011-05-16
  • 2017-07-02
相关资源
最近更新 更多