Подтвердить что ты не робот

Может ли TCP С# клиент получать и отправлять непрерывно/последовательно без сна?

В какой-то степени это вопрос "основы TCP", но в то же время мне еще предстоит найти убедительный ответ в другом месте и считаю, что у меня хорошее/хорошее понимание основ TCP. Я не уверен, что сочетание вопросов (или один вопрос, и пока я нахожусь на его просьбе о подтверждении пары баллов) противоречит правилам. Надеюсь, что нет.

Я пытаюсь написать реализацию С# для TCP-клиента, который взаимодействует с существующим приложением, содержащим TCP-сервер (у меня нет доступа к его коду, поэтому нет WCF). Как я могу подключиться к нему, отправлять и получать по мере необходимости, когда новая информация поступает или выходит, и в конечном итоге отключается. Используя следующий код MSDN в качестве примера, где они перечисляют асинхронные методы "Отправить" и "Получать" (или просто TcpClient) и игнорируют подключения и отключить, как тривиально, как я могу лучше всего продолжать проверку новых полученных пакетов и в то же время отправлять по мере необходимости?

Первоначально я использовал TCPClient и GetStream(), а код msdn по-прежнему требует, чтобы цикл и сон описывались бит (счетчик интуитивно), где я запускаю метод приема в цикле в отдельном потоке со сном ( 10) миллисекунд, и при необходимости отправьте в основной (или третьей) поток. Это позволяет мне отправлять штрафы, а метод получения эффективно опросает через регулярные интервалы для поиска новых пакетов. Полученные пакеты затем добавляются в очередь.

Это действительно лучшее решение? Должен ли быть эквивалент события DataAvailable (или чего-то, чего я не вижу в коде msdn), который позволяет нам получать, когда и только когда есть новые данные?

В качестве запоздалой мысли я заметил, что сокет можно вырезать с другой стороны, если клиент не узнает до следующей неудачной отправки. Чтобы уточнить, клиент обязан отправлять регулярные keepalives (и получать недостаточно, только отправлять), чтобы определить, жив ли сокет. И частота keepalive определяет, как скоро я узнаю, что ссылка не работает. Это верно? Я попробовал опрос Poll, socket.connected и т.д., Чтобы узнать, почему каждый просто не помогает.

Наконец, чтобы подтвердить (я считаю, что это нехорошо убедиться), в вышеуказанном сценарии отправки по запросу и при получении, если tcpclient.DataAvailable каждые десять секунд, может ли быть потеря данных при отправке и получении в одно и то же время? Если в то же время я получаю, что я пытаюсь отправить, будет один отказ, перезаписать другое или любое другое такое нежелательное поведение?

4b9b3361

Ответ 1

Нет ничего неправильного в том, чтобы объединить вопросы вместе, но он делает ответ на вопрос более сложным...:)

В статье MSDN, которую вы указали, показано, как выполнить одноразовую TCP-связь, то есть одну отправку и один прием. Вы также заметите, что он использует класс Socket, где большинство людей, включая меня, предложит использовать TcpClient. Вы всегда можете получить базовый Socket через свойство Client, если вам нужно сконфигурировать определенный сокет, например (например, SetSocketOption()).

Другой аспект этого примера заключается в том, что, хотя он использует потоки для выполнения делегатов AsyncCallback для BeginSend() и BeginReceive(), это, по сути, однопоточный пример, потому что о том, как используются объекты ManualResetEvent. Для повторного обмена между клиентом и сервером это не то, что вы хотите.

Хорошо, поэтому вы хотите использовать TcpClient. Подключение к серверу (например, TcpListener) должно быть простым - используйте Connect(), если вы хотите операцию блокировки или BeginConnect(), если вы хотите неблокировать операцию. Как только соединение установлено, используйте метод GetStream(), чтобы получить NetworkStream для использования для чтения и записи. Используйте Read()/Write() операции для блокировки I/O и BeginRead()/BeginWrite() операции для неблокирующего ввода-вывода. Обратите внимание, что BeginRead() и BeginWrite() используют тот же механизм AsyncCallback, используемый методами BeginReceive() и BeginSend() класса Socket.

Одной из ключевых моментов, которые следует отметить на этом этапе, является небольшая публикация в документации MSDN для NetworkStream:

Операции чтения и записи могут выполняться одновременно на экземпляр класса NetworkStream без необходимости синхронизации. Пока существует один уникальный поток для записи операций и один уникальный поток для операций чтения, там будет не допускать перекрестной помехи между потоками чтения и записи и нет требуется синхронизация.

Короче говоря, поскольку вы планируете читать и писать из одного и того же экземпляра TcpClient, для этого вам понадобятся два потока. Использование отдельных потоков гарантирует, что никакие данные не будут потеряны при одновременном получении данных, когда кто-то пытается отправить. Способ, которым я подходил к этому в моих проектах, - создать объект верхнего уровня, например Client, который обертывает TcpClient и его базовый NetworkStream. Этот класс также создает и управляет двумя объектами Thread, передавая объект NetworkStream каждому в процессе построения. Первый поток - это поток Sender. Любой, кто хочет отправить данные, делает это с помощью общедоступного метода SendData() на Client, который передает данные в Sender для передачи. Второй поток - это поток Receiver. Этот поток публикует все полученные данные заинтересованным лицам через публичное мероприятие, открытое Client. Это выглядит примерно так:

Client.cs

public sealed partial class Client : IDisposable
{
    // Called by producers to send data over the socket.
    public void SendData(byte[] data)
    {
        _sender.SendData(data);
    }

    // Consumers register to receive data.
    public event EventHandler<DataReceivedEventArgs> DataReceived;

    public Client()
    {
        _client = new TcpClient(...);
        _stream = _client.GetStream();

        _receiver = new Receiver(_stream);
        _sender   = new Sender(_stream);

        _receiver.DataReceived += OnDataReceived;
    }

    private void OnDataReceived(object sender, DataReceivedEventArgs e)
    {
        var handler = DataReceived;
        if (handler != null) DataReceived(this, e);  // re-raise event
    }

    private TcpClient     _client;
    private NetworkStream _stream;
    private Receiver      _receiver;
    private Sender        _sender;
}


Client.Receiver.cs

private sealed partial class Client
{
    private sealed class Receiver
    {
        internal event EventHandler<DataReceivedEventArgs> DataReceived;

        internal Receiver(NetworkStream stream)
        {
            _stream = stream;
            _thread = new Thread(Run);
            _thread.Start();
        }

        private void Run()
        {
            // main thread loop for receiving data...
        }

        private NetworkStream _stream;
        private Thread        _thread;
    }
}


Client.Sender.cs

private sealed partial class Client
{
    private sealed class Sender
    {
        internal void SendData(byte[] data)
        {
            // transition the data to the thread and send it...
        }

        internal Sender(NetworkStream stream)
        {
            _stream = stream;
            _thread = new Thread(Run);
            _thread.Start();
        }

        private void Run()
        {
            // main thread loop for sending data...
        }

        private NetworkStream _stream;
        private Thread        _thread;
    }
}

Обратите внимание, что это три отдельных файла .cs, но они определяют разные аспекты одного и того же класса Client. Я использую трюк Visual Studio, описанный здесь, чтобы вложить соответствующие файлы Receiver и Sender в файл Client. В общем, так, как я это делаю.

Относительно NetworkStream.DataAvailable/Thread.Sleep() вопрос. Я бы согласился, что событие будет приятным, но вы можете эффективно достичь этого, используя метод Read() в сочетании с бесконечным ReadTimeout, Это не окажет негативного влияния на остальную часть вашего приложения (например, пользовательский интерфейс), поскольку он работает в своем потоке. Однако это затрудняет закрытие потока (например, когда приложение закрывается), поэтому вы, вероятно, захотите использовать что-то более разумное, скажем, 10 миллисекунд. Но тогда вы вернулись к опросу, чего мы пытаемся избежать в первую очередь. Вот как я это делаю, с комментариями для объяснения:

private sealed class Receiver
{
    private void Run()
    {
        try
        {
            // ShutdownEvent is a ManualResetEvent signaled by
            // Client when its time to close the socket.
            while (!ShutdownEvent.WaitOne(0))
            {
                try
                {
                    // We could use the ReadTimeout property and let Read()
                    // block.  However, if no data is received prior to the
                    // timeout period expiring, an IOException occurs.
                    // While this can be handled, it leads to problems when
                    // debugging if we are wanting to break when exceptions
                    // are thrown (unless we explicitly ignore IOException,
                    // which I always forget to do).
                    if (!_stream.DataAvailable)
                    {
                        // Give up the remaining time slice.
                        Thread.Sleep(1);
                    }
                    else if (_stream.Read(_data, 0, _data.Length) > 0)
                    {
                        // Raise the DataReceived event w/ data...
                    }
                    else
                    {
                        // The connection has closed gracefully, so stop the
                        // thread.
                        ShutdownEvent.Set();
                    }
                }
                catch (IOException ex)
                {
                    // Handle the exception...
                }
            }
        }
        catch (Exception ex)
        {
            // Handle the exception...
        }
        finally
        {
            _stream.Close();
        }
    }
}

Что касается "keepalives", то, к сожалению, не существует способа узнать, когда другая сторона отключилась от соединения молча, за исключением попыток отправки некоторых данных. В моем случае, поскольку я контролирую как отправляющую, так и принимающую стороны, я добавил в мой протокол небольшое сообщение KeepAlive (8 байт). Это отправляется каждые пять секунд с обеих сторон TCP-соединения, если другие данные уже отправлены.

Я думаю, что я затронул все грани, которые вы затронули. Надеюсь, вы сочтете это полезным.