调用BeginAcceptTcpClient后停止TcpListener

22

我有这段代码...

internal static void Start()
{
    TcpListener listenerSocket = new TcpListener(IPAddress.Any, 32599);
    listenerSocket.Start();
    listenerSocket.BeginAcceptTcpClient(new AsyncCallback(AcceptClient), null);
}

那么我的回调函数看起来像这样...

private static void AcceptClient(IAsyncResult asyncResult)
{
    MessageHandler handler = new MessageHandler(listenerSocket.EndAcceptTcpClient(asyncResult));
    ThreadPool.QueueUserWorkItem((object state) => handler.Process());
    listenerSocket.BeginAcceptTcpClient(new AsyncCallback(AcceptClient), null);
}

现在,我调用BeginAcceptTcpClient,然后过一段时间我想停止服务器。为了做到这一点,我一直在调用TcpListener.Stop()或TcpListener.Server.Close()。然而,两者都会执行我的AcceptClient函数。这时,当我调用EndAcceptTcpClient时,就会抛出异常。有什么最佳实践方法可以解决这个问题吗?我可以在调用stop后放置一个标志来停止AcceptClient的执行,但我想知道是否有什么我遗漏的东西。

更新1

目前,我已通过更改代码来修补它,使其看起来像这样。

private static void AcceptClient(IAsyncResult asyncResult)
{
     if (!shutdown)
     {
          MessageHandler handler = new MessageHandler(listenerSocket.EndAcceptTcpClient(asyncResult));
          ThreadPool.QueueUserWorkItem((object state) => handler.Process());
          listenerSocket.BeginAcceptTcpClient(new AsyncCallback(AcceptClient), null);
     }
}

private static bool shutdown = false;
internal static void Stop()
{
     shutdown = true;
     listenerSocket.Stop();
}

更新 2

我改为实现 Spencer Ruport 的答案。

private static void AcceptClient(IAsyncResult asyncResult)
{
    if (listenerSocket.Server.IsBound)
    {
            MessageHandler handler = new MessageHandler(listenerSocket.EndAcceptTcpClient(asyncResult));
            ThreadPool.QueueUserWorkItem((object state) => handler.Process());
            listenerSocket.BeginAcceptTcpClient(new AsyncCallback(AcceptClient), null);
    }
}

2
你不必排队等待另一个线程来处理工作;你可以直接调用EndAcceptTcpClient告诉监听器已经处理完毕,然后立即调用BeginAcceptTcpClient来安排另一个处理。使用当前线程来处理刚收到的请求。 - Ricardo Nolde
5个回答

17
我最近也遇到了这个问题,我认为你当前的解决方案不完整/错误。在检查 IsBound 和随后调用 EndAcceptTcpClient() 之间不存在原子性保证。如果监听器在这两个语句之间停止,则仍可能出现异常。你没有说你遇到了什么异常,但我想它跟我遇到的一样,是 ObjectDisposedException(抱怨底层套接字已被处理)。
您应该能够通过模拟线程调度来检查此错误:
  • 在回调中的 IsBound 检查后的行上设置断点
  • 冻结到达断点的线程(“线程”窗口-> 右键单击,“冻结”)
  • 运行/触发调用 TcpListener.Stop() 的代码
  • 在调用 EndAcceptTcpClient() 处中断并逐步执行。你应该可以看到 ObjectDisposedException
在我看来,理想的解决方案是Microsoft在这种情况下抛出与 EndAcceptTcpClient 不同的异常,例如 ListenCanceledException 或类似的异常。
现在我们必须从 ObjectDisposedException 推断出发生了什么。只需捕获异常并相应地处理即可。在我的代码中,我默认忽略异常,因为我已经有其他代码在执行真正的关闭工作(即首先调用 TcpListener.Stop() 的代码)。你应该在那个区域内已经有异常处理了,因为你可能会得到各种 SocketExceptions。这只是在 try 块上附加另一个 catch 处理程序。
我承认我对这种方法感到不舒服,因为原则上 catch 可能是误报,存在真正的“坏”对象访问。但另一方面,在 EndAcceptTcpClient() 调用中没有太多对象访问可以触发此异常。我希望如此。
下面是我的代码。这是早期/原型版本的东西,请忽略 Console 调用。
    private void OnAccept(IAsyncResult iar)
    {
        TcpListener l = (TcpListener) iar.AsyncState;
        TcpClient c;
        try
        {
            c = l.EndAcceptTcpClient(iar);
            // keep listening
            l.BeginAcceptTcpClient(new AsyncCallback(OnAccept), l);
        }
        catch (SocketException ex)
        {
            Console.WriteLine("Error accepting TCP connection: {0}", ex.Message);

            // unrecoverable
            _doneEvent.Set();
            return;
        }
        catch (ObjectDisposedException)
        {
            // The listener was Stop()'d, disposing the underlying socket and
            // triggering the completion of the callback. We're already exiting,
            // so just return.
            Console.WriteLine("Listen canceled.");
            return;
        }

        // meanwhile...
        SslStream s = new SslStream(c.GetStream());
        Console.WriteLine("Authenticating...");
        s.BeginAuthenticateAsServer(_cert, new AsyncCallback(OnAuthenticate), s);
    }

是的,最后我加了一个停止布尔值,在调用关闭之前设置它。然后在调用结束接受之前检查它。 - Anthony D
即使采用这种方法,也会遇到线程问题。基本上,除非有一个锁覆盖了布尔值和对TcpListener的调用,否则您可能会遇到导致异常的线程调度:1)回调检查布尔值,我们仍在侦听,很酷,2)该线程被换出以设置布尔值并调用Stop()的线程,3)回调线程恢复并调用EndAccept。 - David Pope
为什么你在关闭监听器后调用EndAcceptTcpClient方法会导致ObjectDisposedException异常,而不是先检查它是否还活着,然后从回调函数中简单地返回呢? - Roman Starkov

7

不,你没有错过任何东西。您可以检查Socket对象的IsBound属性。至少对于TCP连接,当套接字正在侦听时,它将设置为true,而在调用close之后,它的值将为false。但是,您自己的实现同样有效。


1
这就是答案:只需在异步回调中检查listener.Server.IsBound,如果为false,则直接返回。无需调用EndAccept*,然后捕获(预期且已记录的)异常。 - Roman Starkov

1

这是一个简单的示例,展示如何开始监听、如何异步处理请求以及如何停止监听。

完整示例在此处

public class TcpServer
{
    #region Public.     
    // Create new instance of TcpServer.
    public TcpServer(string ip, int port)
    {
        _listener = new TcpListener(IPAddress.Parse(ip), port);
    }

    // Starts receiving incoming requests.      
    public void Start()
    {
        _listener.Start();
        _ct = _cts.Token;
        _listener.BeginAcceptTcpClient(ProcessRequest, _listener);
    }

    // Stops receiving incoming requests.
    public void Stop()
    { 
        // If listening has been cancelled, simply go out from method.
        if(_ct.IsCancellationRequested)
        {
            return;
        }

        // Cancels listening.
        _cts.Cancel();

        // Waits a little, to guarantee 
        // that all operation receive information about cancellation.
        Thread.Sleep(100);
        _listener.Stop();
    }
    #endregion

    #region Private.
    // Process single request.
    private void ProcessRequest(IAsyncResult ar)
    { 
        //Stop if operation was cancelled.
        if(_ct.IsCancellationRequested)
        {
            return;
        }

        var listener = ar.AsyncState as TcpListener;
        if(listener == null)
        {
            return;
        }

        // Check cancellation again. Stop if operation was cancelled.
        if(_ct.IsCancellationRequested)
        {
            return;
        }

        // Starts waiting for the next request.
        listener.BeginAcceptTcpClient(ProcessRequest, listener);

        // Gets client and starts processing received request.
        using(TcpClient client = listener.EndAcceptTcpClient(ar))
        {
            var rp = new RequestProcessor();
            rp.Proccess(client);
        }
    }
    #endregion

    #region Fields.
    private CancellationToken _ct;
    private CancellationTokenSource _cts = new CancellationTokenSource();
    private TcpListener _listener;
    #endregion
}

1

试试这个。我用它没有捕获异常,很好用。

private void OnAccept(IAsyncResult pAsyncResult)
{
    TcpListener listener = (TcpListener) pAsyncResult.AsyncState;
    if(listener.Server == null)
    {
        //stop method was called
        return;
    }
    ...
}

1
这个问题在于 Stop() 实际上会为 Server 创建一个新的(未绑定的)套接字。然后 Start() 会将该套接字绑定到指定的 IPEndPoint 或者 IPAddress 和端口,并开始侦听。这就是为什么 IsBound 稍微更加准确的原因。 - J Bryan Price

0

我认为需要三件事情,并且BeginAcceptTcpClient的重新启动应该放在EndAcceptTcpClient的tryctach之外。

    private void AcceptTcpClientCallback(IAsyncResult ar)
    {
        var listener = (TcpListener)ar.AsyncState;

        //Sometimes the socket is null and somethimes the socket was set
        if (listener.Server == null || !listener.Server.IsBound)
            return;

        TcpClient client = null;

        try
        {
            client = listener.EndAcceptTcpClient(ar);
        }
        catch (SocketException ex)
        {
            //the client is corrupt
            OnError(ex);
        }
        catch (ObjectDisposedException)
        {
            //Listener canceled
            return;
        }

        //Get the next Client
        listener.BeginAcceptTcpClient(new AsyncCallback(AcceptTcpClientCallback), listener);

        if (client == null)
            return; //Abort if there was an error with the client

        MyConnection connection = null;
        try
        {
            //Client-Protocoll init
            connection = Connect(client.GetStream()); 
        }
        catch (Exception ex)
        {
            //The client is corrupt/invalid
            OnError(ex);

            client.Close();
        }            
    }

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