Остановка TcpListener после вызова BeginAcceptTcpClient

У меня есть этот код...

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. Каков наилучший способ обойти это? Я мог бы просто поставить флаг, чтобы остановить выполнение 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

Я изменил его, чтобы отразить ответ Спенсера Рупорта.

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);
    }
}

person Anthony D    schedule 23.07.2009    source источник
comment
Вам не нужно ставить в очередь другой поток для выполнения обработки; вы можете просто вызвать EndAcceptTcpClient, чтобы сообщить слушателю, что он обработан, а затем вызвать BeginAcceptTcpClient сразу после него, чтобы запланировать другую обработку. Используйте текущий поток для обработки только что полученного запроса.   -  person Ricardo Nolde    schedule 26.11.2010


Ответы (5)


Я только что сам столкнулся с этой проблемой, и я считаю, что ваше текущее решение является неполным/неправильным. Нет никакой гарантии атомарности между проверкой IsBound и последующим вызовом EndAcceptTcpClient(). Вы все еще можете получить исключение, если прослушиватель Stop() между этими двумя операторами. Вы не сказали, какое исключение вы получаете, но я предполагаю, что это то же самое, что и я, ObjectDisposedException (жалуясь, что базовый сокет уже удален).

Вы должны быть в состоянии проверить это, моделируя планирование потока:

  • Установите точку останова в строке после проверки IsBound в обратном вызове.
  • Заморозить поток, достигший точки останова (окно «Потоки» -> щелкните правой кнопкой мыши «Заморозить»)
  • Запустите/запустите код, который вызывает TcpListener.Stop()
  • Прервите и пройдите вызов EndAcceptTcpClient(). Вы должны увидеть ObjectDisposedException.

ИМО, идеальным решением для Microsoft было бы выдать в этом случае исключение, отличное от EndAcceptTcpClient, например. ListenCanceledException или что-то в этом роде.

Как бы то ни было, мы должны сделать вывод о том, что происходит, из ObjectDisposedException. Просто поймайте исключение и ведите себя соответственно. В моем коде я молча ем исключение, так как у меня есть код в другом месте, который выполняет настоящую работу по завершению работы (то есть код, который вызвал TcpListener.Stop() в первую очередь). В любом случае у вас уже должна быть обработка исключений в этой области, так как вы можете получить различные SocketExceptions. Это просто добавление другого обработчика catch к этому блоку try.

Я признаю, что мне не нравится этот подход, поскольку в принципе подвох может быть ложным срабатыванием с подлинным «плохим» доступом к объекту. Но, с другой стороны, в вызове EndAcceptTcpClient() не слишком много обращений к объектам, которые в противном случае могли бы вызвать это исключение. Я надеюсь.

Вот мой код. Это ранний/прототипный материал, игнорируйте вызовы консоли.

    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);
    }
person David Pope    schedule 04.08.2009
comment
Да, я закончил тем, что поставил стоп-буул, который я установил непосредственно перед закрытием. Затем я проверяю это перед вызовом end accept. - person Anthony D; 05.08.2009
comment
Даже этот подход страдает от проблемы с потоками. По сути, если нет блокировки, которая охватывает как логическое значение, так и вызовы TcpListener, вы можете получить планирование потока, которое вызывает исключение: 1) обратный вызов проверяет логическое значение, мы все еще слушаем, круто, 2) этот поток заменяется на поток, который устанавливает логическое значение и вызывает Stop(), 3) поток обратного вызова возобновляется и вызывает EndAccept. - person David Pope; 05.08.2009
comment
Почему вы вызываете ObjectDisposedException, вызывая EndAcceptTcpClient на закрытом прослушивателе, вместо того, чтобы просто сначала проверить, жив ли он, и просто вернуться из обратного вызова? - person Roman Starkov; 27.03.2012

Нет, вы ничего не упускаете. Вы можете проверить свойство IsBound объекта Socket. По крайней мере, для TCP-соединений, пока сокет прослушивает, для этого будет установлено значение true, а после того, как вы вызовете close, его значение будет ложным. Хотя ваша собственная реализация может работать так же хорошо.

person Spencer Ruport    schedule 23.07.2009
comment
Круто, это именно то, что я искал! Спасибо! - person Anthony D; 23.07.2009
comment
Это ответ; просто проверьте listener.Server.IsBound в асинхронном обратном вызове и, если оно ложно, просто вернитесь. Нет необходимости вызывать EndAccept*, а затем перехватывать (ожидаемое и задокументированное) исключение. - person Roman Starkov; 27.03.2012

Попробуй это. он отлично работает для меня, не ловя исключений.

private void OnAccept(IAsyncResult pAsyncResult)
{
    TcpListener listener = (TcpListener) pAsyncResult.AsyncState;
    if(listener.Server == null)
    {
        //stop method was called
        return;
    }
    ...
}
person Stefan Gerunde    schedule 18.11.2010
comment
Проблема в том, что Stop() фактически воссоздает новый (несвязанный) сокет для сервера. Затем Start() привязывает этот сокет (к IPEndPoint или IPAddress + порту, для которого он был создан) и запускает его прослушивание. Вот почему IsBound немного точнее. - person J Bryan Price; 12.08.2011

я думаю, что все три вещи необходимы и что перезапуск BeginAcceptTcpClient должен быть помещен вне tryctach EndAcceptTcpClient.

    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();
        }            
    }
person Andreas Dirnberger    schedule 15.09.2011

Это простой пример того, как начать прослушивание, как обрабатывать запросы асинхронно и как прекратить прослушивание.

Полный пример здесь.

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
}
person Andrey Goncharov    schedule 01.10.2015