3

我们有一个通过 TCP 套接字与客户端通信的服务器应用程序。在运行几周后,它会因无法处理的 NullReferenceException 而崩溃。我已经能够用一个非常小的控制台程序重现异常,但内部套接字线程池中似乎存在未处理的异常。所以我不能用任何 try/catch 块来处理它,因为它不在我的控制范围内。

有人对此有任何想法吗?是框架错误还是我如何在套接字线程池上捕获异常(所以我们的应用程序没有崩溃)?这是经过几次迭代 (3-10) 后生成异常的示例代码。重要的是要知道服务器处于脱机状态,因此套接字无法连接。它用于 Visual Studio 2010 和 .Net 框架 4.0。

internal class Program
{
    private static string host;

    private static Socket socket;

    private static void Main(string[] args)
    {
        Trace.Listeners.Add(new ConsoleTraceListener());

        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        host = "127.0.0.1";
        //aslo the problem is happening whe the host is other network ip address
        //host = "192.168.0.1";

        //when in other thread doesn not crash application
        //Task.Factory.StartNew(() => StartConnecting());

        //also crashing the application
        //Task.Factory.StartNew(() => StartConnecting(), TaskCreationOptions.LongRunning);

        //when it is regular thread the exception occurs
        ///*
        var thread = new Thread(new ThreadStart(StartConnecting));
        thread.Start();
        //*/

        //when it is blocking exception also occurs
        //StartConnecting();
        Console.WriteLine("Press any key to exit ...");
        Console.ReadKey();
    }

    private static void StartConnecting()
    {
        try
        {
            int count = 0;
            while (true)
            {
                try
                {
                    // if i must switch to Socket.Connect(...)?
                    Trace.WriteLine(string.Format("Connect Try {0} begin", ++count));

                    var ar = socket.BeginConnect(host, 6500, new AsyncCallback(ConnectCallback), socket);

                    Trace.WriteLine(string.Format("Connect Try {0} end", count));
                }
                catch (Exception err)
                {
                    Trace.WriteLine(string.Format("[BeginConnect] error {0}", err.ToString()));
                }
                System.Threading.Thread.Sleep(1000);
                //will see the exception more quick
            }
        }
        catch (Exception e)
        {
            Trace.WriteLine(string.Format("[StartConnecting] error {0}", e.ToString()));
        }
    }

    private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        string msg = e.ExceptionObject.ToString();

        Trace.WriteLine(string.Format("[CurrentDomain_UnhandledException] isTerminating={0} error {1}", e.IsTerminating, msg));

        Trace.WriteLine("Exiting process");

        //the other processing threads continue working
        //without problems untill there is thread.sleep
        //Thread.Sleep(10000);
    }

    private static void ConnectCallback(IAsyncResult ar)
    {
        try
        {
            Trace.WriteLine("[ConnectCallback] enter");
            var socket = (Socket)ar.AsyncState;
            socket.EndConnect(ar);

            Trace.WriteLine("[ConnectCallback] exit");
        }
        catch (Exception e)
        {
            Trace.WriteLine(string.Format("[ConnectCallback] error {0}", e.ToString()));
        }
    }
}

应用程序启动后不可避免的崩溃会发生:

[CurrentDomain_UnhandledException] isTerminating=True error System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Net.Sockets.Socket.ConnectCallback()
   at System.Net.Sockets.Socket.RegisteredWaitCallback(Object state, Boolean timedOut)
   at System.Threading._ThreadPoolWaitOrTimerCallback.PerformWaitOrTimerCallback(Object state, Boolean timedOut)
4

3 回答 3

1

您提供的示例代码重复调用BeginConnect,无需等待异步操作完成。

粗略地说,你正在这样做

while(true)
{
    socket.BeginConnect(...);
    Sleep(1000);
}

因此,当您的线程启动时,它首先调用BeginConnect(),然后等待一秒钟,然后BeginConnect()在前一个调用仍在执行时再次调用。

在我的电脑上,它给了我一个InvalidOperationException,但我猜异常类型可能取决于 CLR 版本(我使用的是 .NET 4.5.1)。

以下是 3 种不同的解决方案:

  1. 取消异步操作Socket.EndConnect()
  2. 等待异步操作完成IAsyncResult.AsyncWaitHandle.WaitOne()
  3. 不要使用BeginConnect()Connect()使用
于 2013-10-22T18:38:39.233 回答
1

我很确定这个无法捕获的错误是由 Socket 代码中的错误引起的,您应该将其报告给connect

这是 .NET 参考源中 Socket.cs 代码的摘录:http ://referencesource.microsoft.com/#System/net/System/Net/Sockets/Socket.cs,938ed6a18154d0fc

private void ConnectCallback()
{
  LazyAsyncResult asyncResult = (LazyAsyncResult) m_AcceptQueueOrConnectResult;

  // If we came here due to a ---- between BeginConnect and Dispose
  if (asyncResult.InternalPeekCompleted)
  {
     // etc.
      return;
  }
}

此回调由另一个静态方法调用:

private static void RegisteredWaitCallback(object state, bool timedOut)
{
  Socket me = (Socket)state;

  // Interlocked to avoid a race condition with DoBeginConnect
  if (Interlocked.Exchange(ref me.m_RegisteredWait, null) != null)
  {
    switch (me.m_BlockEventBits)
    {
    case AsyncEventBits.FdConnect:
      me.ConnectCallback();
      break;

    case AsyncEventBits.FdAccept:
      me.AcceptCallback(null);
      break;
    }
  }
}

这个静态方法永远不会取消注册,它总是被调用,但它依赖于一个m_RegisteredWait事件来确定它是否必须传递给套接字成员方法。

问题是我想这个事件有时不是空的,而m_AcceptQueueOrConnectResult可以是空的,这会导致问题,在一个不可捕获的线程中。

话虽如此,问题的根本原因是您的代码首先出现问题,正如其他人所指出的那样。为避免这种可怕的无法捕获的错误,只需确保在发生错误时调用CloseDispose在套接字上,这将在内部清除m_RegisteredWait成员。例如,BeginConnect 文档是这样说的:

要取消对 BeginConnect 方法的挂起调用,请关闭 Socket。当异步操作正在进行时调用 Close 方法时,将调用提供给 BeginConnect 方法的回调。对 EndConnect 方法的后续调用将引发 ObjectDisposedException 以指示操作已被取消。

在您的示例中,只需将以下行添加到您的回调代码中:

 private static void ConnectCallback(IAsyncResult ar)
    {
        try
        {
         ...
        }
        catch (Exception e)
        {
          if (_socket != null) _socket.Dispose();
        }
    }

现在,您仍然会遇到错误,但它们将是正常错误。

于 2016-01-31T17:08:35.937 回答
0

如果您仔细查看堆栈跟踪,您会NullReferenceException发现System.Net.Sockets.Socket.ConnectCallback. 如果您查看您的代码,您会发现您有一个名为ConnectCallback.

这就是我们所说的“巧合”。

请将您的回调方法的名称更改为MyConnectCallback,并将BeginConnect调用更改为:

var ar = socket.BeginConnect(host, 6500, new AsyncCallback(MyConnectCallback), socket);

看看这是否改变了什么。

如果我是正确的,并且您的ConnectCallback方法从未被调用,那么我也不得不怀疑您的代码是如何工作的。

于 2013-10-22T19:44:39.760 回答