Как написать масштабируемый сервер на основе Tcp / Ip

Джексон предоставляет аннотацию JsonIdentityInfo для предотвращения циклических ссылок. Вы можете проверить учебник здесь .

144
задан Erik Funkenbusch 24 May 2009 в 18:36
поделиться

16 ответов

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

Я написал его как класс, который управляет всеми соединениями для серверов.

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

private List<xConnection> _sockets;

Также вам нужен сокет, который фактически прослушивает входящие соединения.

private System.Net.Sockets.Socket _serverSocket;

Метод start фактически запускает серверный сокет и начинает прослушивание любых входящих соединений.

public bool Start()
{
  System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
  System.Net.IPEndPoint serverEndPoint;
  try
  {
     serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
  }
  catch (System.ArgumentOutOfRangeException e)
  {
    throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
  }
  try
  {
    _serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
   }
   catch (System.Net.Sockets.SocketException e)
   {
      throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
    }
    try
    {
      _serverSocket.Bind(serverEndPoint);
      _serverSocket.Listen(_backlog);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured while binding socket, check inner exception", e);
    }
    try
    {
       //warning, only call this once, this is a bug in .net 2.0 that breaks if 
       // you're running multiple asynch accepts, this bug may be fixed, but
       // it was a major pain in the ass previously, so make sure there is only one
       //BeginAccept running
       _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
    }
    catch (Exception e)
    {
       throw new ApplicationException("Error occured starting listeners, check inner exception", e);
    }
    return true;
 }

Я просто хотел бы отметить, что код обработки исключений выглядит плохо, но причина в том, что у меня там был код подавления исключений, чтобы любые исключения подавлялись и возвращали false , если был установлен параметр конфигурации, но я хотел удалить его для краткости.

_serverSocket.BeginAccept (new AsyncCallback (acceptCallback)), _serverSocket) выше по существу устанавливает наш серверный сокет для вызова метода acceptCallback всякий раз, когда пользователь подключается. Этот метод запускается из пула потоков .Net, который автоматически обрабатывает создание дополнительных рабочих потоков, если у вас много операций блокировки. Это должно оптимально обрабатывать любую нагрузку на сервер.

    private void acceptCallback(IAsyncResult result)
    {
       xConnection conn = new xConnection();
       try
       {
         //Finish accepting the connection
         System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
         conn = new xConnection();
         conn.socket = s.EndAccept(result);
         conn.buffer = new byte[_bufferSize];
         lock (_sockets)
         {
           _sockets.Add(conn);
         }
         //Queue recieving of data from the connection
         conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
         //Queue the accept of the next incomming connection
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
       }
     }

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

Вызов метода BeginReceive сообщает сокету, что делать, когда он получает данные от клиент. Для BeginReceive вам необходимо предоставить ему массив байтов, в который он будет копировать данные, когда клиент отправляет данные. Будет вызван метод ReceiveCallback , именно так мы обрабатываем получение данных.

private void ReceiveCallback(IAsyncResult result)
{
  //get our connection from the callback
  xConnection conn = (xConnection)result.AsyncState;
  //catch any errors, we'd better not have any
  try
  {
    //Grab our buffer and count the number of bytes receives
    int bytesRead = conn.socket.EndReceive(result);
    //make sure we've read something, if we haven't it supposadly means that the client disconnected
    if (bytesRead > 0)
    {
      //put whatever you want to do when you receive data here

      //Queue the next receive
      conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
     }
     else
     {
       //Callback run but no data, close the connection
       //supposadly means a disconnect
       //and we still have to close the socket, even though we throw the event later
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
   catch (SocketException e)
   {
     //Something went terribly wrong
     //which shouldn't have happened
     if (conn.socket != null)
     {
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
 }

РЕДАКТИРОВАТЬ: В этом шаблоне я забыл упомянуть, что в этой области кода:

//put whatever you want to do when you receive data here

//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);

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

Обратный вызов accept завершает чтение сокета данных вызовом end receive. Это заполняет буфер, предусмотренный в функции начала приема. Как только вы сделаете все, что хотите, в том месте, где я оставил комментарий, мы вызываем следующий метод BeginReceive , который снова запустит обратный вызов, если клиент отправит дополнительные данные. Теперь вот действительно сложная часть: когда клиент отправляет данные, обратный вызов приема может вызываться только с частью сообщения. Повторная сборка может стать очень сложной. Я использовал свой собственный метод и создал для этого своего рода проприетарный протокол. Я оставил его, но, если вы попросите, я могу добавить его. Этот обработчик был на самом деле самым сложным фрагментом кода, который я когда-либо писал.

public bool Send(byte[] message, xConnection conn)
{
  if (conn != null && conn.socket.Connected)
  {
    lock (conn.socket)
    {
    //we use a blocking mode send, no async on the outgoing
    //since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
       conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
     }
   }
   else
     return false;
   return true;
 }

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

Класс xConnection, на который вы видите ссылку выше, в основном представляет собой простую оболочку для сокета, включающую байтовый буфер, а в моей реализации - некоторые дополнения.

public class xConnection : xBase
{
  public byte[] buffer;
  public System.Net.Sockets.Socket socket;
}

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

using System.Net.Sockets;

Надеюсь, это поможет, возможно, это не самый чистый код, но он работает . В коде также есть некоторые нюансы, от которых вам стоит утомиться. Во-первых, только один вызов BeginAccept одновременно. Раньше вокруг этого была очень неприятная ошибка .net, которая была много лет назад, поэтому я не помню подробностей.

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

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

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

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

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

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

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

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

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

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

92
ответ дан 23 November 2019 в 22:30
поделиться

Что ж, сокеты .NET, похоже, предоставляют select () - это лучше всего для обработки ввода. Для вывода у меня был бы пул потоков записи сокетов, прослушивающих рабочую очередь, принимающих дескриптор / объект сокета как часть рабочего элемента, поэтому вам не нужен поток на сокет.

1
ответ дан 23 November 2019 в 22:30
поделиться

Вы можете попробовать использовать структуру под названием ACE (Adaptive Communications Environment), которая является универсальной Фреймворк C ++ для сетевых серверов. Это очень надежный, зрелый продукт, разработанный для поддержки высоконадежных приложений с большим объемом работы вплоть до телекоммуникационного уровня.

Фреймворк имеет дело с довольно широким спектром моделей параллелизма и, вероятно, имеет подходящую для вашего приложения вне коробка. Это должно упростить отладку системы, поскольку большинство неприятных проблем с параллелизмом уже решены. Компромисс здесь в том, что фреймворк написан на C ++ и не является самой теплой и пушистой из кодовых баз. С другой стороны, вы проходите тестирование,

1
ответ дан 23 November 2019 в 22:30
поделиться

Я бы использовал SEDA или облегченную библиотеку потоков (erlang или более новая Linux см. Масштабируемость NTPL на стороне сервера ). Асинхронное кодирование очень обременительно, если вы не общаетесь :)

1
ответ дан 23 November 2019 в 22:30
поделиться

Using .NET's integrated Async IO (BeginRead, etc) is a good idea if you can get all the details right. When you properly set up your socket/file handles it will use the OS's underlying IOCP implementation, allowing your operations to complete without using any threads (or, in the worst case, using a thread that I believe comes from the kernel's IO thread pool instead of .NET's thread pool, which helps alleviate threadpool congestion.)

The main gotcha is to make sure that you open your sockets/files in non-blocking mode. Most of the default convenience functions (like File.OpenRead) don't do this, so you'll need to write your own.

One of the other main concerns is error handling - properly handling errors when writing asynchronous I/O code is much, much harder than doing it in synchronous code. It's also very easy to end up with race conditions and deadlocks even though you may not be using threads directly, so you need to be aware of this.

If possible, you should try and use a convenience library to ease the process of doing scalable asynchronous IO.

Microsoft's Concurrency Coordination Runtime is one example of a .NET library designed to ease the difficulty of doing this kind of programming. It looks great, but as I haven't used it, I can't comment on how well it would scale.

For my personal projects that need to do asynchronous network or disk I/O, I use a set of .NET concurrency/IO tools that I've built over the past year, called Squared.Task. It's inspired by libraries like imvu.task and twisted, and I've included some working examples in the repository that do network I/O. I also have used it in a few applications I've written - the largest publicly released one being NDexer (which uses it for threadless disk I/O). The library was written based on my experience with imvu.task and has a set of fairly comprehensive unit tests, so I strongly encourage you to try it out. If you have any issues with it, I'd be glad to offer you some assistance.

In my opinion, based on my experience using asynchronous/threadless IO instead of threads is a worthwhile endeavor on the .NET platform, as long as you're ready to deal with the learning curve. It allows you to avoid the scalability hassles imposed by the cost of Thread objects, and in many cases, you can completely avoid the use of locks and mutexes by making careful use of concurrency primitives like Futures/Promises.

5
ответ дан 23 November 2019 в 22:30
поделиться

Для ясности, я ищу решения на основе .net (если возможно, C #, но подойдет любой язык .net)

Вы не получите наивысшего уровня масштабируемости если вы идете чисто с .NET. Паузы GC могут снизить задержку.

Мне нужно запустить хотя бы один поток для службы. Я рассматриваю возможность использования Asynch API (BeginRecieve и т. Д.), Так как я не знаю, сколько клиентов я подключу в любой момент времени (возможно, сотни). Я определенно не хочу запускать поток для каждого соединения.

Overlapped IO обычно считается самым быстрым API Windows для сетевого взаимодействия. Я не знаю, совпадает ли это с вашим Asynch API. Не используйте select, так как каждый вызов должен проверять каждый открытый сокет вместо того, чтобы выполнять обратные вызовы на активных сокетах.

-1
ответ дан 23 November 2019 в 22:30
поделиться

Рассматривали ли вы просто использование TCP-привязки WCF и шаблона публикации / подписки? WCF позволит вам сосредоточиться [в основном] на вашем домене, а не на сантехнике.

В разделе загрузки IDesign есть множество примеров WCF и даже платформа публикации / подписки, которая может быть полезна: http: // www.idesign.net

9
ответ дан 23 November 2019 в 22:30
поделиться

Вы можете найти хороший обзор методов на странице C10k .

2
ответ дан 23 November 2019 в 22:30
поделиться

У меня есть такой сервер, работающий в некоторых моих решениях. Вот очень подробное объяснение различных способов сделать это в .net: Станьте ближе к сети с помощью высокопроизводительных сокетов в .NET

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

«Основная особенность эти улучшения заключаются в предотвращении повторного выделения и синхронизации объектов во время асинхронного ввода-вывода большого объема. Для шаблона проектирования Begin / End, который в настоящее время реализуется классом Socket для асинхронного ввода-вывода сокета, требуется System. Здесь вы можете найти рабочий код как для клиента, так и для сервера, используя новый SocketAsyncEventArgs 3.5, так что вы можете протестировать его в течение нескольких минут и пройти через код. Это простой подход, но он является основой для начала более масштабной реализации. Также эта статья, опубликованная почти два года назад в MSDN Magazine, была интересным чтением.

Здесь вы можете найти рабочий код как для клиента, так и для сервера, используя новый SocketAsyncEventArgs 3.5, так что вы можете протестировать его в течение нескольких минут и пройти через код. Это простой подход, но он является основой для начала более масштабной реализации. Также эта статья, опубликованная почти два года назад в MSDN Magazine, была интересным чтением.

11
ответ дан 23 November 2019 в 22:30
поделиться

Я бы порекомендовал прочитать эти книги по ACE

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

Хотя ACE реализован на C ++, книги охватывают множество полезных шаблонов, которые можно использовать на любом языке программирования.

0
ответ дан 23 November 2019 в 22:30
поделиться

Меня интересует одно:

Я определенно не хочу начинать нить для каждого соединения.

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

Вы тестировали многопоточность и обнаружили, что чего-то не хватает? Планируете ли вы также иметь соединение с базой данных для каждого потока (это убило бы сервер базы данных, поэтому это плохая идея, но она легко решается с помощью трехуровневого дизайна). Вы беспокоитесь, что у вас будут тысячи клиентов вместо сотен, и тогда у вас действительно возникнут проблемы? (Хотя я' Я бы попробовал тысячу потоков или даже десять тысяч, если бы у меня было 32+ ГБ ОЗУ - опять же, учитывая, что вы не привязаны к процессору, время переключения потока не должно иметь никакого значения.)

Вот код - чтобы увидеть, как это выглядит работающим, перейдите на http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html и щелкните изображение.

Класс сервера:

  public class Server
  {
    private static readonly TcpListener listener = new TcpListener(IPAddress.Any, 9999);

    public Server()
    {
      listener.Start();
      Console.WriteLine("Started.");

      while (true)
      {
        Console.WriteLine("Waiting for connection...");

        var client = listener.AcceptTcpClient();
        Console.WriteLine("Connected!");

        // each connection has its own thread
        new Thread(ServeData).Start(client);
      }
    }

    private static void ServeData(object clientSocket)
    {
      Console.WriteLine("Started thread " + Thread.CurrentThread.ManagedThreadId);

      var rnd = new Random();
      try
      {
        var client = (TcpClient) clientSocket;
        var stream = client.GetStream();
        while (true)
        {
          if (rnd.NextDouble() < 0.1)
          {
            var msg = Encoding.ASCII.GetBytes("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
            stream.Write(msg, 0, msg.Length);

            Console.WriteLine("Status update from thread " + Thread.CurrentThread.ManagedThreadId);
          }

          // wait until the next update - I made the wait time so small 'cause I was bored :)
          Thread.Sleep(new TimeSpan(0, 0, rnd.Next(1, 5)));
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

Основная программа сервера :

namespace ManyThreadsServer
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      new Server();
    }
  }
}

Класс клиента:

  public class Client
  {
    public Client()
    {
      var client = new TcpClient();
      client.Connect(IPAddress.Loopback, 9999);

      var msg = new byte[1024];

      var stream = client.GetStream();
      try
      {
        while (true)
        {
          int i;
          while ((i = stream.Read(msg, 0, msg.Length)) != 0)
          {
            var data = Encoding.ASCII.GetString(msg, 0, i);
            Console.WriteLine("Received: {0}", data);
          }
        }
      }
      catch (SocketException e)
      {
        Console.WriteLine("Socket exception in thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, e);
      }
    }
  }

Основная программа клиента:

using System;
using System.Threading;

namespace ManyThreadsClient
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      // first argument is the number of threads
      for (var i = 0; i < Int32.Parse(args[0]); i++)
        new Thread(RunClient).Start();
    }

    private static void RunClient()
    {
      new Client();
    }
  }
}
8
ответ дан 23 November 2019 в 22:30
поделиться

Я бы использовал методы AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync, которые были добавлены в .Net 3.5. Я провел тест, и они примерно на 35% быстрее (время отклика и битрейт), 100 пользователей постоянно отправляют и получают данные.

1
ответ дан 23 November 2019 в 22:30
поделиться

Существует много способов выполнения сетевых операций на C #. Все они используют разные механизмы под капотом и, таким образом, страдают от серьезных проблем с производительностью из-за высокого уровня параллелизма. Операции Begin * являются одними из тех, которые многие люди часто принимают за самый быстрый / самый быстрый способ создания сети.

Для решения этих проблем они ввели набор методов * Async: Из MSDN http: // msdn .microsoft.com / en-us / library / system.net.sockets.socketasynceventargs.aspx

Класс SocketAsyncEventArgs является частью набора усовершенствований класса System.Net.Sockets .. ::. Socket, которые предоставляют альтернативный асинхронный шаблон, который может использоваться специализированными высокопроизводительными приложениями сокетов. Этот класс был специально разработан для сетевых серверных приложений, требующих высокой производительности. Приложение может использовать усовершенствованный асинхронный шаблон исключительно или только в целевых «горячих» областях (например, при получении больших объемов данных).

Основной особенностью этих улучшений является предотвращение повторного выделения и синхронизации объектов во время высокой нагрузки. объем ввода-вывода асинхронного сокета. Шаблон проектирования «Начало / конец», который в настоящее время реализуется классом System.Net.Sockets .. ::. Socket, требует, чтобы объект System .. ::. IAsyncResult был выделен для каждой операции асинхронного сокета.

Под обложками * Async API использует порты завершения ввода-вывода, что является самым быстрым способом выполнения сетевых операций, см. http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

И просто чтобы помочь вам, я включая исходный код telnet-сервера, который я написал с помощью * Async API. Я включаю только соответствующие части. Также следует отметить, что вместо внутренней обработки данных я предпочитаю помещать их в очередь без блокировок (без ожидания), которая обрабатывается в отдельном потоке. Обратите внимание, что я не включаю соответствующий класс Pool, который представляет собой простой пул, который создаст новый объект, если он пуст, и класс Buffer, который является просто саморасширяющимся буфером, который на самом деле не нужен, если вы не получаете недетерминированный количество данных. Если вам нужна дополнительная информация, напишите мне в личку.

 public class Telnet
{
    private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
    private Socket m_ListenSocket;

    /// <summary>
    /// This event fires when a connection has been established.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Connected;

    /// <summary>
    /// This event fires when a connection has been shutdown.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Disconnected;

    /// <summary>
    /// This event fires when data is received on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataReceived;

    /// <summary>
    /// This event fires when data is finished sending on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataSent;

    /// <summary>
    /// This event fires when a line has been received.
    /// </summary>
    public event EventHandler<LineReceivedEventArgs> LineReceived;

    /// <summary>
    /// Specifies the port to listen on.
    /// </summary>
    [DefaultValue(23)]
    public int ListenPort { get; set; }

    /// <summary>
    /// Constructor for Telnet class.
    /// </summary>
    public Telnet()
    {           
        m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
        ListenPort = 23;
    }

    /// <summary>
    /// Starts the telnet server listening and accepting data.
    /// </summary>
    public void Start()
    {
        IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
        m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        m_ListenSocket.Bind(endpoint);
        m_ListenSocket.Listen(100);

        //
        // Post Accept
        //
        StartAccept(null);
    }

    /// <summary>
    /// Not Yet Implemented. Should shutdown all connections gracefully.
    /// </summary>
    public void Stop()
    {
        //throw (new NotImplementedException());
    }

    //
    // ACCEPT
    //

    /// <summary>
    /// Posts a requests for Accepting a connection. If it is being called from the completion of
    /// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
    /// the new user.
    /// </summary>
    /// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
    private void StartAccept(SocketAsyncEventArgs e)
    {
        if (e == null)
        {
            e = m_EventArgsPool.Pop();
            e.Completed += Accept_Completed;
        }
        else
        {
            e.AcceptSocket = null;
        }

        if (m_ListenSocket.AcceptAsync(e) == false)
        {
            Accept_Completed(this, e);
        }
    }

    /// <summary>
    /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
    /// and then setup a Receive chain to begin receiving data.
    /// </summary>
    /// <param name="sender">object which posted the AcceptAsync</param>
    /// <param name="e">Information about the Accept call.</param>
    private void Accept_Completed(object sender, SocketAsyncEventArgs e)
    {
        //
        // Socket Options
        //
        e.AcceptSocket.NoDelay = true;

        //
        // Create and setup a new connection object for this user
        //
        Connection connection = new Connection(this, e.AcceptSocket);

        //
        // Tell the client that we will be echo'ing data sent
        //
        DisableEcho(connection);

        //
        // Post the first receive
        //
        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;

        //
        // Connect Event
        //
        if (Connected != null)
        {
            Connected(this, args);
        }

        args.Completed += Receive_Completed;
        PostReceive(args);

        //
        // Post another accept
        //
        StartAccept(e);
    }

    //
    // RECEIVE
    //    

    /// <summary>
    /// Post an asynchronous receive on the socket.
    /// </summary>
    /// <param name="e">Used to store information about the Receive call.</param>
    private void PostReceive(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection != null)
        {
            connection.ReceiveBuffer.EnsureCapacity(64);
            e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);

            if (connection.Socket.ReceiveAsync(e) == false)
            {
                Receive_Completed(this, e);
            }              
        }
    }

    /// <summary>
    /// Receive completion callback. Should verify the connection, and then notify any event listeners
    /// that data has been received. For now it is always expected that the data will be handled by the
    /// listeners and thus the buffer is cleared after every call.
    /// </summary>
    /// <param name="sender">object which posted the ReceiveAsync</param>
    /// <param name="e">Information about the Receive call.</param>
    private void Receive_Completed(object sender, SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
        {
            Disconnect(e);
            return;
        }

        connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);

        OnDataReceived(e);

        HandleCommand(e);
        Echo(e);

        OnLineReceived(connection);

        PostReceive(e);
    }

    /// <summary>
    /// Handles Event of Data being Received.
    /// </summary>
    /// <param name="e">Information about the received data.</param>
    protected void OnDataReceived(SocketAsyncEventArgs e)
    {
        if (DataReceived != null)
        {                
            DataReceived(this, e);
        }
    }

    /// <summary>
    /// Handles Event of a Line being Received.
    /// </summary>
    /// <param name="connection">User connection.</param>
    protected void OnLineReceived(Connection connection)
    {
        if (LineReceived != null)
        {
            int index = 0;
            int start = 0;

            while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
            {
                string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
                s = s.Backspace();

                LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
                Delegate[] delegates = LineReceived.GetInvocationList();

                foreach (Delegate d in delegates)
                {
                    d.DynamicInvoke(new object[] { this, args });

                    if (args.Handled == true)
                    {
                        break;
                    }
                }

                if (args.Handled == false)
                {
                    connection.CommandBuffer.Enqueue(s);
                }

                start = index;
                index++;
            }

            if (start > 0)
            {
                connection.ReceiveBuffer.Reset(0, start + 1);
            }
        }
    }

    //
    // SEND
    //

    /// <summary>
    /// Overloaded. Sends a string over the telnet socket.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="s">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, string s)
    {
        if (String.IsNullOrEmpty(s) == false)
        {
            return Send(connection, Encoding.Default.GetBytes(s));
        }

        return false;
    }

    /// <summary>
    /// Overloaded. Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, byte[] data)
    {
        return Send(connection, data, 0, data.Length);
    }

    public bool Send(Connection connection, char c)
    {
        return Send(connection, new byte[] { (byte)c }, 0, 1);
    }

    /// <summary>
    /// Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <param name="offset">Starting offset of date in the buffer.</param>
    /// <param name="length">Amount of data in bytes to send.</param>
    /// <returns></returns>
    public bool Send(Connection connection, byte[] data, int offset, int length)
    {
        bool status = true;

        if (connection.Socket == null || connection.Socket.Connected == false)
        {
            return false;
        }

        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;
        args.Completed += Send_Completed;
        args.SetBuffer(data, offset, length);

        try
        {
            if (connection.Socket.SendAsync(args) == false)
            {
                Send_Completed(this, args);
            }
        }
        catch (ObjectDisposedException)
        {                
            //
            // return the SocketAsyncEventArgs back to the pool and return as the
            // socket has been shutdown and disposed of
            //
            m_EventArgsPool.Push(args);
            status = false;
        }

        return status;
    }

    /// <summary>
    /// Sends a command telling the client that the server WILL echo data.
    /// </summary>
    /// <param name="connection">Connection to disable echo on.</param>
    public void DisableEcho(Connection connection)
    {
        byte[] b = new byte[] { 255, 251, 1 };
        Send(connection, b);
    }

    /// <summary>
    /// Completion callback for SendAsync.
    /// </summary>
    /// <param name="sender">object which initiated the SendAsync</param>
    /// <param name="e">Information about the SendAsync call.</param>
    private void Send_Completed(object sender, SocketAsyncEventArgs e)
    {
        e.Completed -= Send_Completed;              
        m_EventArgsPool.Push(e);
    }        

    /// <summary>
    /// Handles a Telnet command.
    /// </summary>
    /// <param name="e">Information about the data received.</param>
    private void HandleCommand(SocketAsyncEventArgs e)
    {
        Connection c = e.UserToken as Connection;

        if (c == null || e.BytesTransferred < 3)
        {
            return;
        }

        for (int i = 0; i < e.BytesTransferred; i += 3)
        {
            if (e.BytesTransferred - i < 3)
            {
                break;
            }

            if (e.Buffer[i] == (int)TelnetCommand.IAC)
            {
                TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
                TelnetOption option = (TelnetOption)e.Buffer[i + 2];

                switch (command)
                {
                    case TelnetCommand.DO:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                    case TelnetCommand.WILL:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                }

                c.ReceiveBuffer.Remove(i, 3);
            }
        }          
    }

    /// <summary>
    /// Echoes data back to the client.
    /// </summary>
    /// <param name="e">Information about the received data to be echoed.</param>
    private void Echo(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            return;
        }

        //
        // backspacing would cause the cursor to proceed beyond the beginning of the input line
        // so prevent this
        //
        string bs = connection.ReceiveBuffer.ToString();

        if (bs.CountAfterBackspace() < 0)
        {
            return;
        }

        //
        // find the starting offset (first non-backspace character)
        //
        int i = 0;

        for (i = 0; i < connection.ReceiveBuffer.Count; i++)
        {
            if (connection.ReceiveBuffer[i] != '\b')
            {
                break;
            }
        }

        string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);

        if (connection.Secure)
        {
            s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
        }

        s = s.Replace("\b", "\b \b");

        Send(connection, s);
    }

    //
    // DISCONNECT
    //

    /// <summary>
    /// Disconnects a socket.
    /// </summary>
    /// <remarks>
    /// It is expected that this disconnect is always posted by a failed receive call. Calling the public
    /// version of this method will cause the next posted receive to fail and this will cleanup properly.
    /// It is not advised to call this method directly.
    /// </remarks>
    /// <param name="e">Information about the socket to be disconnected.</param>
    private void Disconnect(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            throw (new ArgumentNullException("e.UserToken"));
        }

        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch
        {
        }

        connection.Socket.Close();

        if (Disconnected != null)
        {
            Disconnected(this, e);
        }

        e.Completed -= Receive_Completed;
        m_EventArgsPool.Push(e);
    }

    /// <summary>
    /// Marks a specific connection for graceful shutdown. The next receive or send to be posted
    /// will fail and close the connection.
    /// </summary>
    /// <param name="connection"></param>
    public void Disconnect(Connection connection)
    {
        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception)
        {
        }            
    }

    /// <summary>
    /// Telnet command codes.
    /// </summary>
    internal enum TelnetCommand
    {
        SE = 240,
        NOP = 241,
        DM = 242,
        BRK = 243,
        IP = 244,
        AO = 245,
        AYT = 246,
        EC = 247,
        EL = 248,
        GA = 249,
        SB = 250,
        WILL = 251,
        WONT = 252,
        DO = 253,
        DONT = 254,
        IAC = 255
    }

    /// <summary>
    /// Telnet command options.
    /// </summary>
    internal enum TelnetOption
    {
        Echo = 1,
        SuppressGoAhead = 3,
        Status = 5,
        TimingMark = 6,
        TerminalType = 24,
        WindowSize = 31,
        TerminalSpeed = 32,
        RemoteFlowControl = 33,
        LineMode = 34,
        EnvironmentVariables = 36
    }
}
83
ответ дан 23 November 2019 в 22:30
поделиться

Раньше было действительно хорошее обсуждение масштабируемого TCP / IP с использованием .NET, написанное Крисом Маллинсом из Coversant, к сожалению, похоже, что его блог исчез из своего предыдущего местоположения, поэтому я постараюсь собрать воедино его советы по памяти (некоторые его полезные комментарии можно найти в этой теме: C ++ vs. C #: разработка высокомасштабируемого сервера IOCP )

Прежде всего, обратите внимание, что оба используют Begin / End и Async в классе Socket используют порты завершения ввода-вывода (IOCP) для обеспечения масштабируемости. Это имеет гораздо большее значение (при правильном использовании; см. Ниже) для масштабируемости, чем то, какой из двух методов вы фактически выбираете для реализации своего решения.

Сообщения Криса Маллинза основывались на использовании Begin / End , это тот, с которым я лично сталкивался. Обратите внимание, что Крис собрал решение, основанное на этом, которое масштабировало до 10 000 одновременных клиентских подключений на 32-битной машине с 2 ГБ памяти и до 100 000 на 64-битной платформе с достаточным объемом памяти. Исходя из моего собственного опыта работы с этой техникой (хотя и близко не к такой нагрузке), у меня нет причин сомневаться в этих ориентировочных цифрах.

IOCP по сравнению с потоком на соединение или примитивами «выбора»

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

Контекст Переключатели - это то, что определенно убьет механизм «поток на соединение», хотя это жизнеспособное решение, если вы имеете дело только с несколькими десятками соединений. Однако этот механизм ни в коем случае не является «масштабируемым».

Важные соображения при использовании IOCP

Память

В первую очередь важно понимать, что IOCP может легко привести к проблемам с памятью в .NET, если вы реализация слишком наивна. Каждый вызов IOCP BeginReceive приводит к «закреплению» буфера, в который вы читаете. Хорошее объяснение того, почему это проблема, можно найти в: Журнал Юн Джина: OutOfMemoryException и закрепление .

К счастью, этой проблемы можно избежать, но это требует некоторого компромисса. Предлагаемое решение - выделить большой байт [] буфер при запуске приложения (или близко к нему), по крайней мере, 90 КБ или около того (в .NET 2 требуемый размер может быть больше в дальнейшем версии). Причина этого заключается в том, что большие выделения памяти автоматически попадают в некомпактный сегмент памяти (куча больших объектов), который эффективно автоматически закрепляется. Выделив один большой буфер при запуске, вы убедитесь, что этот блок неподвижной памяти находится по относительно «низкому адресу», где он не будет мешать и вызывать фрагментацию.

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

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

Обработка

Когда вы получите обратный вызов от сделанный вами звонок Begin , очень важно понимать, что код в обратном вызове будет выполняться в низкоуровневом потоке IOCP. Совершенно существенно , чтобы вы избегали длительных операций в этом обратном вызове. Использование этих потоков для сложной обработки убьет вашу масштабируемость так же эффективно, как и использование «потока на соединение».

Предлагаемое решение состоит в том, чтобы использовать обратный вызов только для постановки рабочего элемента в очередь для обработки входящих данных, которые будут выполняется в другом потоке. Избегайте любых потенциально блокирующих операций внутри обратного вызова, чтобы поток IOCP мог вернуться в свой пул как можно быстрее. В .NET 4.0 я бы предложил самое простое решение - создать задачу , указав ей ссылку на клиентский сокет и копию первого байта, который уже был прочитан BeginReceive ] вызов. Затем эта задача отвечает за чтение всех данных из сокета, которые представляют обрабатываемый вами запрос, выполнение его, а затем выполнение нового вызова BeginReceive для повторной постановки сокета в очередь для IOCP. До .NET 4.0 вы можете использовать ThreadPool или создать свою собственную реализацию рабочей очереди с потоками.

Резюме

В принципе, я бы предложил использовать пример кода Кевина для этого решения со следующими добавленными предупреждениями:

  • Убедитесь, что буфер, который вы передаете в BeginReceive , уже «закреплен»
  • Убедитесь, что обратный вызов, который вы передаете в BeginReceive , не делает ничего, кроме постановки задачи в очередь для обработки фактической обработки входящих данных

Когда вы это сделаете, я не сомневаюсь, что вы сможете воспроизвести Криса.

46
ответ дан 23 November 2019 в 22:30
поделиться

Вы уже получили большую часть ответа из приведенных выше примеров кода. Здесь можно использовать асинхронную операцию ввода-вывода. Асинхронный ввод-вывод - это способ масштабирования Win32 изнутри. Наилучшая возможная производительность достигается за счет использования портов завершения, привязки ваших сокетов к портам завершения и наличия пула потоков, ожидающего завершения порта завершения. Принято считать, что 2-4 потока на процессор (ядро) ждут завершения. Я настоятельно рекомендую ознакомиться с этими тремя статьями Рика Вицика из команды по производительности Windows:

  1. Разработка приложений для повышения производительности - Часть 1
  2. Разработка приложений для повышения производительности - Часть 2
  3. Разработка приложений для повышения производительности - Часть 3

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

Второе, что вам нужно сделать, это убедиться, что вы прочитали книгу Улучшение производительности и масштабируемости приложений .NET , которая доступна в Интернете . Вы найдете уместные и действенные советы по использованию потоков, асинхронных вызовов и блокировок в главе 5. Но настоящие жемчужины - в главе 17, где вы найдете такие полезные вещи, как практическое руководство по настройке пула потоков. У моих приложений были серьезные проблемы, пока я не скорректировал maxIothreads / maxWorkerThreads в соответствии с рекомендациями в этой главе.

Вы говорите, что хотите создать чистый TCP-сервер, поэтому мой следующий пункт является ложным. Однако , Если вы оказались в загнанном в угол и используете класс WebRequest и его производные, имейте в виду, что эту дверь охраняет дракон: ServicePointManager . Это класс конфигурации, у которого одна цель в жизни: испортить вашу производительность. Убедитесь, что вы освободили свой сервер от искусственно навязанного ServicePoint.ConnectionLimit, иначе ваше приложение никогда не будет масштабироваться (я позволю вам узнать, какое значение используется по умолчанию ...). Вы также можете пересмотреть политику по умолчанию отправки заголовка Expect100Continue в HTTP-запросах.

Теперь о API, управляемом ядром сокета, все довольно просто на стороне отправки, но они значительно сложнее на стороне получения. Для достижения высокой пропускной способности и масштабирования вы должны убедиться, что сокет не контролируется потоком, потому что у вас нет буфера, отправленного для приема. В идеале для высокой производительности вы должны размещать вперед 3-4 буфера и размещать новые буферы, как только вы получите один из них (, прежде чем вы обработаете возвращенный), чтобы убедиться, что у сокета всегда есть место для хранения данные, поступающие из сети. Вы поймете, почему вы, вероятно, не сможете этого добиться в ближайшее время.

После того, как вы закончите играть с BeginRead / BeginWrite API и начнете серьезную работу, вы поймете, что вам нужна безопасность для вашего трафика, т.е. . Проверка подлинности NTLM / Kerberos и шифрование трафика или, по крайней мере, защита от взлома трафика. То, как вы это делаете, заключается в использовании встроенного System.Net.Security.NegotiateStream (или SslStream, если вам нужно перейти между разными доменами). Это означает, что вместо того, чтобы полагаться на асинхронные операции с прямым сокетом, вы будете полагаться на асинхронные операции AuthenticatedStream. Как только вы получаете сокет (либо из подключения на клиенте, либо из приема на сервере), вы создаете поток в сокете и отправляете его для аутентификации, вызывая либо BeginAuthenticateAsClient, либо BeginAuthenticateAsServer. После завершения аутентификации (по крайней мере, вашего сейфа из собственного безумия InitiateSecurityContext / AcceptSecurityContext ...) вы выполните свою авторизацию, проверив свойство RemoteIdentity вашего аутентифицированного потока и выполнив любую проверку ACL, которую должен поддерживать ваш продукт. После этого вы будете отправлять сообщения с помощью BeginWrite и получать их с помощью BeginRead. Это проблема, о которой я говорил раньше, когда ты выиграл t иметь возможность отправлять несколько буферов приема, потому что классы AuthenticateStream не поддерживают это. Операция BeginRead управляет внутренним вводом-выводом до тех пор, пока вы не получите весь кадр, в противном случае она не смогла бы обработать аутентификацию сообщения (расшифровать кадр и проверить подпись на кадре). Хотя по моему опыту работа, выполняемая классами AuthenticatedStream, довольно хороша и не должна иметь с ней никаких проблем. Т.е. у вас должно получиться заполнить сеть ГБ только 4-5% ЦП. Классы AuthenticatedStream также налагают ограничения на размер кадра для конкретного протокола (16 КБ для SSL, 12 КБ для Kerberos).

Это должно помочь вам начать работу в правильном направлении. Я не собираюсь публиковать здесь код, на MSDN есть отличный пример . Я' Я выполнил много подобных проектов и смог без проблем масштабироваться до 1000 подключенных пользователей. Кроме того, вам нужно изменить ключи реестра, чтобы позволить ядру использовать больше дескрипторов сокетов. и убедитесь, что вы выполняете развертывание на сервере ОС, то есть W2K3, а не XP или Vista (т.е. клиентской ОС), это имеет большое значение.

Кстати, убедитесь, что на сервере есть операции с базами данных. или файл ввода-вывода, вы также используете для них асинхронный вариант, или вы мгновенно истощите пул потоков. Для подключений к SQL Server убедитесь, что вы добавили «Асинхронная обработка = true» в строку подключения.

это имеет большое значение.

Кстати, убедитесь, что если у вас есть операции с базами данных на сервере или файловый ввод-вывод, вы также используете для них асинхронный вариант, иначе вы мгновенно истощите пул потоков. Для подключений к SQL Server убедитесь, что вы добавили «Асинхронная обработка = true» в строку подключения.

это имеет большое значение.

Кстати, убедитесь, что если у вас есть операции с базами данных на сервере или файловый ввод-вывод, вы также используете для них асинхронный вариант, иначе вы мгновенно истощите пул потоков. Для подключений к SQL Server убедитесь, что вы добавили «Асинхронная обработка = true» в строку подключения.

22
ответ дан 23 November 2019 в 22:30
поделиться

Вы можете использовать платформу с открытым исходным кодом Push Framework для разработки высокопроизводительных серверов. Он построен на IOCP и подходит для сценариев push и трансляции сообщений.

http://www.pushframework.com

-1
ответ дан 23 November 2019 в 22:30
поделиться
Другие вопросы по тегам:

Похожие вопросы: