Как мне написать масштабируемый сервер сокетов с использованием C # 4.0?

#c# #sockets #c#-4.0 #task-parallel-library #tcplistener

#c# #сокеты #c #-4.0 #задача-параллельная-библиотека #tcplistener

Вопрос:

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

Сервер получает запрос, содержащий запрос, и передает произвольно большой результат.

Я хотел бы использовать идиоматический способ сделать это, используя методы и библиотеки, доступные на C # 4, с акцентом на простой код, а не на сырую производительность.

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

Ответ №1:

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

Если вы сосредоточены на простом коде, я бы рекомендовал использовать классы TcpClient и TcpListener . Они оба значительно упрощают работу с сокетами. Хотя они существуют со времен .NET Framework 1.1, они были обновлены и по-прежнему являются вашим лучшим выбором.

С точки зрения того, как использовать .NET Framework 4.0 при написании упрощенного кода, Задачи — это первое, что приходит на ум. Они делают написание асинхронного кода намного менее болезненным, и перенос вашего кода станет намного проще после выхода C # 5 (новые ключевые слова async и await). Вот пример того, как задачи могут упростить ваш код:

Вместо использования tcpListener.BeginAcceptTcpClient(AsyncCallback callback, object state); и предоставления метода обратного вызова, который вызывал EndAcceptTcpClient(); бы и, при необходимости, приводил бы ваш объект состояния, C # 4 позволяет вам использовать замыкания, лямбды и задачи, чтобы сделать этот процесс намного более читаемым и масштабируемым. Вот один из примеров:

 private void AcceptClient(TcpListener tcpListener)
{
    Task<TcpClient> acceptTcpClientTask = Task.Factory.FromAsync<TcpClient>(tcpListener.BeginAcceptTcpClient, tcpListener.EndAcceptTcpClient, tcpListener);

    // This allows us to accept another connection without a loop.
    // Because we are within the ThreadPool, this does not cause a stack overflow.
    acceptTcpClientTask.ContinueWith(task => { OnAcceptConnection(task.Result); AcceptClient(tcpListener); }, TaskContinuationOptions.OnlyOnRanToCompletion);
}

private void OnAcceptConnection(TcpClient tcpClient)
{
    string authority = tcpClient.Client.RemoteEndPoint.ToString(); // Format is: IP:PORT

    // Start a new Task to handle client-server communication
}
  

FromAsync очень полезен, поскольку Microsoft предоставила множество перегрузок, которые могут упростить обычные асинхронные операции. Вот еще один пример:

 private void Read(State state)
{
    // The int return value is the amount of bytes read accessible through the Task's Result property.
    Task<int> readTask = Task<int>.Factory.FromAsync(state.NetworkStream.BeginRead, state.NetworkStream.EndRead, state.Data, state.BytesRead, state.Data.Length - state.BytesRead, state, TaskCreationOptions.AttachedToParent);

    readTask.ContinueWith(ReadPacket, TaskContinuationOptions.OnlyOnRanToCompletion);
    readTask.ContinueWith(ReadPacketError, TaskContinuationOptions.OnlyOnFaulted);
}
  

State — это просто определяемый пользователем класс, который обычно содержит только экземпляр TcpClient, данные (массив байтов) и, возможно, также прочитанные байты.

Как вы можете видеть, ContinueWith можно использовать для замены множества громоздких try-catches , которые до сих пор были необходимым злом.

В начале своего сообщения вы упомянули, что не хотите создавать поток для каждого соединения или создавать очень длительные задачи, и я подумал, что рассмотрю это на этом этапе. Лично я не вижу проблемы с созданием потока для каждого соединения.

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

Вы должны помнить, что ThreadPool является общим ресурсом со специализированной функцией (избегая накладных расходов на создание потока, а не на его фактическое использование). Поскольку он является общим, если вы использовали поток, другой ресурс не сможет, и это может быстро привести к истощению пула потоков и сценариям взаимоблокировки.

Комментарии:

1. Как насчет 1 миллиона подключений при использовании одного потока на соединение?

2. В случае одного миллиона подключений: используйте linux. Если вам действительно нужно использовать Windows, обрабатывайте много сокетов в одном потоке, используя метод статического выбора, и группируйте их разумно. Не используйте TcpClient для этого, потому что тогда вы действительно могли бы сэкономить накладные расходы.