Як написати масштабований сервер на основі Tcp / Ip


148

Я перебуваю на стадії проектування нового додатка служби Windows, який приймає TCP / IP-з'єднання для тривалих з'єднань (тобто це не так, як HTTP, де є багато коротких з'єднань, а скоріше клієнт підключається і залишається на зв’язку годинами чи днями або навіть тижні).

Я шукаю ідеї для найкращого способу проектування мережевої архітектури. Мені потрібно буде запустити хоча б одну нитку для служби. Я розглядаю можливість використання API Asynch (BeginRecieve і т. Д.), Оскільки я не знаю, скільки клієнтів у мене буде підключено в даний момент часу (можливо, сотні). Я точно не хочу запускати потік для кожного з'єднання.

Дані в першу чергу будуть надходити до клієнтів з мого сервера, але при цьому будуть надіслані від клієнтів деякі випадки. Це насамперед програма моніторингу, в якій мій сервер періодично надсилає клієнтам дані про стан.

Будь-які пропозиції щодо найкращого способу зробити це максимально масштабованим? Основний робочий процес? Дякую.

EDIT: Щоб зрозуміти, я шукаю рішення на основі .net (якщо це можливо, C #, але будь-яка мова .net буде працювати)

ПРИМІТКА: Для того, щоб присвоїти нагороду, я очікую більше, ніж просту відповідь. Мені знадобиться робочий приклад рішення, або як вказівник на щось, що я можу завантажити, або короткий приклад в рядку. І це повинно бути .net та Windows (будь-яка мова .net прийнятна)

EDIT: Я хочу подякувати всім, хто дав хороші відповіді. На жаль, я міг прийняти лише один, і я вирішив прийняти більш відомий метод Початок / Кінець. Рішення Esac може бути і кращим, але воно все ще досить нове, що я не знаю точно, як це вийде.

Я підтримав усі відповіді, які, як я вважав, хороші, я б хотів зробити більше для вас, хлопці. Знову дякую.


1
Ви абсолютно впевнені, що це потребує тривалого зв'язку? Важко сказати з наданої обмеженої інформації, але я б це зробив лише в разі крайньої необхідності ..
markt

Так, це має бути довго бігати. Дані потрібно оновлювати в режимі реального часу, тому я не можу періодично проводити опитування, дані повинні бути спрямовані клієнтові в міру їх виникнення, що означає постійний зв'язок.
Ерік Функенбуш

1
Це не є вагомою причиною. HTTP підтримує тривалі з'єднання просто чудово. Ви просто відкриваєте з'єднання і чекаєте повторного опитування (затримка опитування). Це добре працює для багатьох додатків у стилі AJAX тощо. Як ви думаєте, що працює Gmail :-)
TFD

2
Gmail працює, оглядаючи електронну пошту періодично, він не підтримує тривале з'єднання. Це добре для електронної пошти, де відповідь у реальному часі не потрібна.
Ерік Функенбуш

2
Опитування чи витягування добре масштабує, але швидко розвиває затримку. Натискання також не масштабується, але допомагає зменшити або усунути затримку.
andrewbadera

Відповіді:


92

Я писав щось подібне до цього в минулому. З моїх досліджень років тому було показано, що написання власної реалізації сокета було найкращим варіантом, використовуючи асинхронні розетки. Це означало, що клієнти, які насправді нічого не робили, вимагали відносно невеликих ресурсів. Все, що відбувається, обробляється пулом потоків .net.

Я написав це як клас, який управляє всіма підключеннями до серверів.

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

private List<xConnection> _sockets;

Також вам потрібна розетка, яка насправді прослуховує вхідні з'єднання.

private System.Net.Sockets.Socket _serverSocket;

Метод запуску фактично запускає серверний сокет і починає прослуховувати будь-які вхідні з'єднання.

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якщо встановлено параметр config, але я хотів його видалити для стислість заради.

_ServerSocket.BeginAccept (новий 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);
       }
     }

Наведений вище код по суті щойно закінчив приймати з'єднання, яке входить, черги 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);
       }
     }
   }
 }

EDIT: У цій шаблоні я забув зазначити, що в цій області коду:

//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 наступного блоку від клієнта не затримується під час виконання будь-якого коду обробки повідомлень.

Зворотний виклик прийняття завершує зчитування розетки даних викликом кінцевого прийому. Це заповнює буфер, передбачений функцією початку прийому. Після того, як ви зробите все, що завгодно, де я залишив коментар, ми зателефонуємо на наступний 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дзвінок, для мене це було чудово через розміри повідомлень та багатопотоковий характер моєї програми. Якщо ви хочете надіслати кожен клієнт, вам просто потрібно пройти через список _sockets.

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

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

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

using System.Net.Sockets;

Я сподіваюся, що це корисно, можливо, це не найчистіший код, але він працює. Також у коді є деякі нюанси, які ви повинні бути змушені змінювати. Для одного лише мати єдиний BeginAcceptдзвінок у будь-який час. Раніше це було дуже дратівливою помилкою .net, що було років тому, тому я не пригадую деталей.

Крім того, в ReceiveCallbackкоді ми обробляємо все, що надійшло з сокета, перш ніж черговий черговий прийом. Це означає, що для однієї розетки ми є насправді лише в ReceiveCallbackбудь-який момент часу, і нам не потрібно використовувати синхронізацію потоків. Однак якщо ви упорядкуєте це для виклику наступного прийому відразу після витягування даних, що може бути трохи швидшим, вам потрібно буде переконатися, що ви правильно синхронізували потоки.

Крім того, я зламав багато свого коду, але залишив суть того, що відбувається на місці. Це має бути гарним початком для вашого дизайну. Залиште коментар, якщо у вас є питання щодо цього.


1
Це хороша відповідь, Кевін .. схоже, ти на шляху, щоб отримати виграш. :)
Ерік Функенбуш

6
Я не знаю, чому це найвища відповідь. Початок * Кінець * - це не найшвидший спосіб роботи з мережею в C #, а також не найбільш масштабований. Це швидше, ніж синхронне, але є багато операцій, які тривають під кришкою в Windows, які дійсно сповільнюють цей мережевий шлях.
esac

6
Майте на увазі, що писав esac у попередньому коментарі. Модель початкового кінця, ймовірно, спрацює для вас до певного моменту, в моєму коді зараз використовується початковий кінець, але в .net 3.5 є покращення його обмежень. Мене не хвилює баунті, але рекомендую вам прочитати посилання у моїй відповіді, навіть якщо ви реалізуєте такий підхід. "Підвищення продуктивності сокета у версії 3.5"
jvanderh

1
Я просто хотів викласти їх, оскільки я, можливо, не був достатньо зрозумілий, це код .Net 2.0 ери, де я вважаю, що це була дуже життєздатна модель. Однак відповідь esac виглядає дещо сучаснішою, якщо орієнтуватись на .net 3.5, єдиний, який у мене є, - це кидання подій :), але це можна легко змінити. Крім того, я провів пропускну здатність за допомогою цього коду, і на двоядерному оптероні 2 ГГц вдалося збільшити максимум 100 Мбіт / с, і це додало шар шифрування поверх цього коду.
Кевін Нісбет

1
@KevinNisbet Я знаю, що це досить пізно, але для тих, хто використовує цю відповідь, щоб створити власні сервери - відправка також повинна бути асинхронною, тому що в іншому випадку ви відкриваєте себе перед можливістю глухого кута. Якщо обидві сторони записують дані, які заповнюють їхні відповідні буфери, Sendметоди будуть блокуватися нескінченно в обох сторонах, оскільки вхідних даних ніхто не читає.
Луань

83

У C # існує багато способів здійснення мережевих операцій. Усі вони використовують різні механізми, що знаходяться під кришкою, і, таким чином, страждають від серйозних проблем з високою продуктивністю. Операції Початок * - одна з таких, що багато людей часто помиляються за те, що вони є найшвидшим / швидким способом роботи в мережі.

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

Клас SocketAsyncEventArgs є частиною набору вдосконалень до System.Net.Sockets .. ::. Клас сокетів, що надають альтернативний асинхронний зразок, який може бути використаний спеціалізованими високоефективними програмами сокета. Цей клас був розроблений спеціально для мережевих серверних додатків, які вимагають високої продуктивності. Додаток може використовувати вдосконалений асинхронний малюнок виключно або лише в націлених гарячих зонах (наприклад, при отриманні великої кількості даних).

Головною особливістю цих удосконалень є уникнення повторного розподілу та синхронізації об'єктів під час вводу / виводу великого об'єму асинхронного сокета. Шаблон дизайну Початок / Кінець, що в даний час реалізований системою System.Net.Sockets .. ::. Класс Socket вимагає виділення об'єкта System .. ::. IAsyncResult для кожної операції асинхронного сокета.

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

І просто для того, щоб допомогти вам, я включаю вихідний код для сервера telnet, який я написав за допомогою API * Async. Я включаю лише відповідні частини. Також зауважте, що замість обробки даних вбудованих даних я замість цього вирішу натиснути на чергову (незачекану) чергу, яка обробляється окремим потоком. Зауважте, що я не включаю відповідний клас 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
    }
}

Це досить прямо вперед і простий приклад. Дякую. Мені доведеться оцінити плюси та мінуси кожного методу.
Ерік Функенбуш

У мене не було можливості перевірити це, але я отримую тут невиразне відчуття стану гонки чомусь тут. По-перше, якщо ви отримаєте багато повідомлень, я не знаю, що події будуть оброблятися в порядку (можливо, це не має значення для додатків користувачів, але слід зазначити), або я можу помилитися і події обробляються в порядку. По-друге, я, можливо, його пропустив, але чи не існує ризику очищення буфера очищеним, поки DataReceived все ще працює, якщо це триватиме тривалий час? Якщо вирішити ці, можливо, невиправдані проблеми, я думаю, що це дуже хороше сучасне рішення.
Кевін Нісбет

1
У моєму випадку для мого сервера telnet 100%, ТАК вони в порядку. Ключ - це встановлення правильного методу зворотного виклику перед викликом AcceptAsync, ReceiveAsync тощо. У моєму випадку я роблю SendAsync окремим потоком, тому, якщо це змінено, щоб зробити шаблон Accept / Send / Receive / Send / Receive / Disconnect, тоді його потрібно буде модифікувати.
esac

1
Точка №2 також має щось враховувати. Я зберігаю свій об'єкт 'Connection' в контексті SocketAsyncEventArgs. Це означає, що у мене є лише один буфер прийому на з'єднання. Я не публікую ще один прийом за допомогою цього SocketAsyncEventArgs, поки DataReceived не буде завершений, тому подальші дані не можуть бути прочитані на цьому, поки він не буде завершений. Я РОЗУМУВАТЬСЯ, що над цими даними не робити довгих операцій. Я фактично переміщую весь буфер усіх отриманих даних на безстрокову чергу, а потім обробляю її окремим потоком. Це забезпечує низьку затримку на ділянці мережі.
esac

1
У бічній записці я написав тестові одиниці та тести навантаження для цього коду, і коли я збільшив завантаження користувача з 1 користувача до 250 користувачів (в одній двоядерній системі, 4 Гб оперативної пам’яті), час відповіді на 100 байт (1 пакет) і 10000 байт (3 пакети) залишилися однаковими протягом усієї кривої завантаження користувача.
esac

46

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

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

Повідомлення Кріса Маллінза засновані на використанні Begin/End, з яким я особисто маю досвід. Зауважте, що Кріс створив на основі цього рішення, яке масштабувало до 10000 одночасних клієнтських з'єднань на 32-розрядній машині з 2 Гб пам'яті, а також до 100 000 на 64-бітній платформі з достатньою кількістю пам'яті. З мого власного досвіду роботи з цією технікою (вже ніде немає подібного навантаження) у мене немає підстав сумніватися в цих орієнтовних цифрах.

IOCP проти потоку за з'єднанням або приміток "select"

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

Контекстні комутатори - це те, що, безумовно, знищить механізм "за ниткою на з'єднання", хоча це життєздатне рішення, якщо ви маєте справу лише з кількома десятками з'єднань. Цей механізм, однак, не розтягує уяву "масштабованою".

Важливі міркування при використанні IOCP

Пам'ять

Перш за все, важливо розуміти, що IOCP може легко спричинити проблеми з пам'яттю під .NET, якщо ваша реалізація занадто наївна. Кожен BeginReceiveдзвінок IOCP призведе до "закріплення" буфера, який ви читаєте. Хороше пояснення, чому це проблема, дивіться у веб-журналі Юнь Цзінь: OutOfMemoryException та прив’язування .

На щастя, цю проблему можна уникнути, але вона потребує трохи компромісу. Пропоноване рішення полягає у виділенні великого byte[]буфера при запуску програми (або закритті до нього), щонайменше, 90 КБ або близько того (на .NET 2, необхідний розмір може бути більшим у більш пізніх версіях). Причиною цього є те, що великі розміщення пам'яті автоматично закінчуються в сегменті пам'яті, що не ущільнюється (The Big Object Heap), який ефективно автоматично закріплюється. Виділяючи один великий буфер під час запуску, ви переконуєтесь, що цей блок нерухомої пам’яті знаходиться на відносно «низькій адресі», де він не заважатиме і спричинятиме фрагментацію.

Потім можна використовувати компенсації для сегментації цього великого буфера на окремі ділянки для кожного з'єднання, для якого потрібно прочитати деякі дані. Тут починається компроміс; оскільки цей буфер потрібно заздалегідь виділити, вам доведеться визначити, скільки буферного простору вам потрібно для кожного з'єднання, і яку верхню межу ви хочете встановити на кількість з'єднань, до яких потрібно масштабувати (або ви можете реалізувати абстракцію які можуть виділяти додаткові закріплені буфери, як тільки вони вам знадобляться).

Найпростішим рішенням було б призначити кожному з'єднанню по одному байту з унікальним зміщенням у межах цього буфера. Тоді ви можете здійснити BeginReceiveдзвінок за один байт, який слід прочитати, і виконати решту читання в результаті отриманого зворотного дзвінка.

Обробка

Коли ви отримуєте зворотний дзвінок від Beginзробленого дзвінка, дуже важливо усвідомити, що код у зворотному дзвінку буде виконуватися на низькорівневому потоці IOCP. Абсолютно важливо, щоб уникнути тривалих операцій у цьому зворотному дзвінку. Використання цих потоків для складної обробки знищить вашу масштабованість так само ефективно, як і використання "потоку за з'єднання".

Пропоноване рішення - використовувати зворотний виклик лише для встановлення черги на робочий елемент для обробки вхідних даних, які будуть виконані в якомусь іншому потоці. Уникайте будь-яких потенційно блокуючих операцій всередині зворотного дзвінка, щоб потік IOCP міг повернутися до пулу якнайшвидше. У .NET 4.0 я б запропонував найпростіше рішення - це нереститися Task, давши йому посилання на клієнтський сокет і копію першого байта, який вже був прочитаний під час BeginReceiveвиклику. Потім це завдання відповідає за зчитування всіх даних із сокета, що представляють запит, який ви обробляєте, виконуючи його, а потім здійснює новий BeginReceiveвиклик, щоб ще раз поставити в чергу сокет для IOCP. Попередньо .NET 4.0, ви можете використовувати ThreadPool або створити власну реалізацію потокової робочої черги.

Підсумок

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

  • Переконайтесь, що буфер, до якого ви переходите BeginReceive, уже 'прикріплений'
  • Переконайтеся, що зворотний виклик, до якого ви переходите, BeginReceiveне має нічого іншого, як чергувати завдання для обробки фактичної обробки вхідних даних

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


1
Щоб закріпити менший блок пам'яті, для закріплення буфера можна використовувати метод об’єкта GCHandle Alloc. Після цього, UnsafeAddrOfPinnedArrayElement об'єкта Marshal може бути використаний для отримання вказівника на буфер. Наприклад: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Боб Брайан

@BobBryan Якщо я не пропускаю тонку точку, яку ви намагаєтеся зробити, такий підхід насправді не допомагає вирішити проблему, з якою моє рішення намагається вирішити, виділяючи великі блоки, а саме потенціал драматичної фрагментації пам'яті, властивий багаторазовому розподілу невеликих закріплених блоків пам'яті.
jerryjvl

Ну, справа в тому, що вам не потрібно виділяти великий блок для того, щоб він залишався закріпленим у пам'яті. Ви можете виділити менші блоки та використовувати вищевказану техніку, щоб закріпити їх у пам’яті, щоб уникнути того, щоб gc перемістив їх. Ви можете зберігати посилання на кожен з менших блоків, подібно до того, як ви зберігаєте посилання на один більший блок і повторно використовувати їх за потребою. Будь-який підхід справедливий - я просто вказував, що вам не потрібно використовувати дуже великий буфер. Але, сказав, що іноді використання дуже великого буфера - це найкращий шлях, оскільки gc буде лікувати це більш ефективно.
Боб Брайан

@BobBryan, оскільки закріплення буфера відбувається автоматично при виклику BeginReceive, закріплення насправді не є важливою точкою тут; ефективність була;) ... і це особливо викликає занепокоєння при спробі написання масштабованого сервера, отже, необхідність виділення великих блоків для використання в буферному просторі.
jerryjvl

@jerryjvl Вибачте, що виникла справді старе питання, проте недавно я виявив цю точну проблему з методами асинхрування BeginXXX / EndXXX. Це чудовий пост, але потрібно було багато копати, щоб знайти. Мені подобається ваше запропоноване рішення, але не розумію його частини: "Тоді ви можете здійснити виклик BeginReceive для того, щоб прочитати один байт, і виконати решту читання в результаті зворотного дзвінка, який ви отримаєте." Що ви маєте на увазі під рештою готової роботи в результаті зворотного дзвінка, який ви отримали?
Маусімо

22

Ви вже отримали більшу частину відповіді за допомогою зразків коду вище. Використання асинхронної операції вводу-виводу - це абсолютно шлях до цього. Async IO - це спосіб, яким Win32 призначений для внутрішнього масштабування. Найкраща ефективність, яку ви можете досягти, досягається за допомогою портів завершення, прив’язуючи ваші сокети до портів завершення і пул потоків чекає завершення порту завершення. Загальна мудрість полягає в тому, щоб 2–4 нитки на процесор (ядро) чекали завершення. Я настійно рекомендую ознайомитися з цими трьома статтями Ріка Віціка з команди Windows Performance:

  1. Проектування програм для виконання - Частина 1
  2. Проектування програм для виконання - Частина 2
  3. Проектування програм для виконання - Частина 3

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

Друге, що вам потрібно зробити, це переконатися, що ви переглянете книгу « Удосконалення .NET додатків та масштабованість» , доступну в Інтернеті. Ви знайдете доречні та дійсні поради щодо використання потоків, асинхронних викликів та блокувань у розділі 5. Але справжні дорогоцінні камені - у розділі 17, де ви знайдете такі смаколики, як практичні вказівки щодо налаштування пулу потоків. У моїх додатків виникли серйозні проблеми, поки я не скоригував maxIothreads / maxWorkerThreads відповідно до рекомендацій у цій главі.

Ви говорите, що хочете зробити чистий TCP-сервер, тому наступний мій пункт є хибним. Однак якщо ви потрапили в кут і використовуєте клас WebRequest та його похідні, попередити, що дракон охороняє ці двері: ServicePointManager . Це клас конфігурації, який має одну мету в житті: зіпсувати вашу ефективність. Переконайтеся, що ви звільнили свій сервер від штучно нав’язаного ServicePoint.ConnectionLimit або ваша програма ніколи не буде масштабуватись (я дозволяю вам відкрити себе, що таке значення за замовчуванням ...). Ви також можете переглянути політику за замовчуванням для надсилання заголовка Expect100Continue в http-запитах.

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

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

Це має привести вас до правильного шляху. Я не збираюся розміщувати тут код, є ідеально хороший приклад на MSDN . Я зробив багато подібних проектів, і мені вдалося наблизити до 1000 користувачів, які підключились без проблем. Вище цього вам потрібно буде змінити ключі реєстру, щоб дозволити ядру більше ручок сокета. і переконайтеся, що ви розгортаєтесь на ОС сервера , тобто W2K3 не XP або Vista (тобто клієнтська ОС), це має велике значення.

BTW переконайтеся, що у вас є операції з базами даних на сервері або файлі IO, ви також використовуєте для них аромат async, або ви злиєте пул потоків за будь-який час. Для підключень SQL Server переконайтеся, що ви додали "Асинхронна обробка = true" до рядка з'єднання.


Тут є чудова інформація. Я хотів би, щоб я міг нагородити кількох людей винагородою. Однак я підтримав вас. Тут добре, дякую.
Ерік Функенбуш

11

У мене такий сервер працює в деяких моїх рішеннях. Ось дуже детальне пояснення різних способів зробити це у .net: Закрийте провід із високоефективними розетками у .NET

Останнім часом я шукав шляхи вдосконалення нашого коду і розглядаю це: " Підвищення продуктивності сокета у версії 3.5 ", яке було включено спеціально "для використання додатками, які використовують асинхронну мережевий введення / виведення для досягнення найвищої продуктивності".

"Основною особливістю цих удосконалень є уникнення повторного розподілу та синхронізації об'єктів під час вводу / виводу асинхронного гнізда з великим об'ємом. Початок / кінець дизайнерської схеми, реалізованої в даний час класом Socket, для асинхронного вводу-виводу сокета вимагає системи. Об'єкт IAsyncResult повинен бути розподілений для кожної операції асинхронного сокета. "

Ви можете продовжувати читати, якщо перейти за посиланням. Я особисто завтра перевіряю їх зразок коду, щоб порівняти його з тим, що у мене є.

Редагувати: Тут ви можете знайти робочий код як для клієнта, так і для сервера, використовуючи новий 3.5 SocketAsyncEventArgs, щоб ви могли його протестувати протягом декількох хвилин і перейти через код. Це простий підхід, але є основою для початку набагато більшого впровадження. Також ця стаття майже двох років тому в журналі MSDN була цікавою для читання.



9

Чи розглядали ви просто використання WCF net TCP прив'язки та шаблон публікації / підписки? WCF дозволить вам зосередитись [переважно] на вашому домені замість сантехніки.

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


8

Мені цікаво одне:

Я точно не хочу запускати потік для кожного з'єднання.

Чому так? Windows може обробляти сотні потоків у програмі, щонайменше, з Windows 2000. Я це зробив, з цим дійсно легко працювати, якщо потоки не потрібно синхронізувати. Тим більше, що ви робите багато вводу-виводу (тому ви не пов'язані з процесором, і багато потоків буде заблоковано ні на диску, ні на мережевому спілкуванні), я не розумію цього обмеження.

Ви перевірили багатопотоковий спосіб і виявили, що йому чогось не вистачає? Чи плануєте ви також мати підключення до бази даних для кожного потоку (це вбило б сервер бази даних, тому це погана ідея, але це легко вирішується за допомогою 3-х рівневого дизайну). Ви переживаєте, що у вас буде тисячі клієнтів замість сотні, і тоді у вас дійсно будуть проблеми? (Хоча я б спробував тисячу потоків або навіть десять тисяч, якби у мене було 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();
    }
  }
}

Windows може обробляти безліч потоків, але .NET насправді не розроблений для їх обробки. Кожен додаток .NET має пул потоків, і ви не хочете вичерпувати цей пул потоків. Я не впевнений, якщо ви запускаєте Thread вручну, якщо вона надходить з нитки чи ні. Тим не менш, сотні ниток, які нічого не роблять, більшу частину часу - це величезна витрата ресурсів.
Ерік Функенбуш

1
Я вважаю, що ви неправильно бачите теми. Нитки надходять із пулу ниток, лише якщо ви цього дійсно хочете - звичайні теми не роблять. Сотні ниток, які нічого не роблять, точно нічого не витрачають :) (Ну, трохи пам’яті, але пам’ять така дешева, що насправді це вже не проблема.) Я збираюся написати для цього пару прикладних додатків, я опублікую URL на це колись я закінчу. Тим часом я рекомендую вам знову переглянути те, що я написав вище, і спробувати відповісти на мої запитання.
Марсель Попеску

1
Хоча я погоджуюся з коментарем Марселя щодо перегляду ниток у створених нитках, не надходять з нитки потоків, решта тверджень не вірна. Пам'ять - це не те, скільки встановлено в машині, усі програми на Windows працюють у віртуальному адресному просторі та в 32-бітовій системі, які дають вам 2 Гб даних для вашого додатка (не важливо, скільки оперативної пам'яті встановлено на коробці). Вони все ще повинні керуватися під час виконання. Якщо введення асинхронного вводу-виводу не використовує потік для очікування (він використовує IOCP, який дозволяє перекривати IO), і є кращим рішенням і збільшуватиме масштабність.
Брайан ONeil

7
При роботі багатьох потоків проблема не в пам'яті, а в процесорі. Контекстний перемикач між потоками - відносно дорога операція, і чим активніші потоки, тим більше контекстних комутаторів буде відбуватися. Кілька років тому я провів тест на своєму ПК із консольною програмою C # та з прибл. 500 потоків мій процесор був 100%, нитки не робили нічого істотного. Для мережевих комунікацій краще зменшити кількість потоків.
sipwiz

1
Я б або пішов із рішенням Task, або використав async / wait. Рішення Завдання здається більш простим, тоді як асинхронізація / очікування, ймовірно, є більш масштабованою (вони були спеціально призначені для ситуацій, пов'язаних з IO).
Марсель Попеску

5

Використання інтегрованого Async IO ( BeginReadтощо) .NET - це гарна ідея, якщо ви зможете зрозуміти всі деталі. Коли ви правильно налаштуєте ручки сокета / файлу, він використовуватиме основну реалізацію IOCP в ОС, що дозволить завершити ваші операції без використання потоків (або, в гіршому випадку, використовуючи нитку, яка, на мою думку, походить замість пулу потоків IO ядра ядра пулу потоків .NET, який допомагає полегшити перевантаженість каналів.)

Основна проблема - переконатися, що ви відкриваєте свої сокети / файли в режимі, що не блокує. Більшість функцій зручності за замовчуванням (наприклад File.OpenRead) цього не роблять, тому вам потрібно буде написати власну.

Однією з інших головних проблем є поводження з помилками - правильне поводження з помилками при написанні асинхронного коду вводу / виводу набагато складніше, ніж це робити в синхронному коді. Так само дуже легко закінчитись із умовами перегонів та тупиками, навіть якщо ви, можливо, не використовуєте потоки безпосередньо, тому вам потрібно знати про це.

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

Виконання координації конкурентоспроможності Microsoft - це один із прикладів бібліотеки .NET, розробленої для полегшення труднощів у виконанні подібних програм. Це виглядає чудово, але оскільки я його не використовував, я не можу коментувати, наскільки добре він би масштабувався.

Для моїх особистих проектів, яким потрібно виконати асинхронну мережеву чи дискову введення-виведення, я використовую набір інструментів .NET concurrency / IO, створений за останній рік, під назвою Squared.Task . Він натхненний такими бібліотеками, як imvu.task і кручений , і я включив у сховище кілька робочих прикладів, які роблять мережеві введення-виведення. Я також використовував його в декількох написаних нами програмах - найбільший публічно випущений - NDexer (який використовує його для введення / виводу бездискових дисків). Бібліотека була написана на основі мого досвіду роботи з imvu.task і має набір досить вичерпних тестових одиниць, тому настійно рекомендую спробувати. Якщо у вас є якісь проблеми з цим, я б радий запропонувати вам допомогу.

На мою думку, виходячи з мого досвіду використання асинхронного / потокового IO замість ниток є вагомим починанням на платформі .NET, доки ви готові мати справу з кривою навчання. Це дозволяє уникнути проблем зі збільшенням масштабів, накладених вартістю об’єктів Thread, і в багатьох випадках ви можете повністю уникнути використання замків і мютексів, обережно використовуючи примітиви одночасності, такі як Futures / Promises.


Чудова інформація, я перевірю ваші посилання та побачу, що має сенс.
Ерік Функенбуш

3

Я використовував рішення Кевіна, але він говорить, що в рішенні не вистачає коду для повторної збірки повідомлень. Розробники можуть використовувати цей код для повторної збірки повідомлень:

private static void ReceiveCallback(IAsyncResult asyncResult )
{
    ClientInfo cInfo = (ClientInfo)asyncResult.AsyncState;

    cInfo.BytesReceived += cInfo.Soket.EndReceive(asyncResult);
    if (cInfo.RcvBuffer == null)
    {
        // First 2 byte is lenght
        if (cInfo.BytesReceived >= 2)
        {
            //this calculation depends on format which your client use for lenght info
            byte[] len = new byte[ 2 ] ;
            len[0] = cInfo.LengthBuffer[1];
            len[1] = cInfo.LengthBuffer[0];
            UInt16 length = BitConverter.ToUInt16( len , 0);

            // buffering and nulling is very important
            cInfo.RcvBuffer = new byte[length];
            cInfo.BytesReceived = 0;

        }
    }
    else
    {
        if (cInfo.BytesReceived == cInfo.RcvBuffer.Length)
        {
             //Put your code here, use bytes comes from  "cInfo.RcvBuffer"

             //Send Response but don't use async send , otherwise your code will not work ( RcvBuffer will be null prematurely and it will ruin your code)

            int sendLenghts = cInfo.Soket.Send( sendBack, sendBack.Length, SocketFlags.None);

            // buffering and nulling is very important
            //Important , set RcvBuffer to null because code will decide to get data or 2 bte lenght according to RcvBuffer's value(null or initialized)
            cInfo.RcvBuffer = null;
            cInfo.BytesReceived = 0;
        }
    }

    ContinueReading(cInfo);
 }

private static void ContinueReading(ClientInfo cInfo)
{
    try 
    {
        if (cInfo.RcvBuffer != null)
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, cInfo.BytesReceived, cInfo.RcvBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, cInfo.BytesReceived, cInfo.LengthBuffer.Length - cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);
        }
    }
    catch (SocketException se)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
    catch (Exception ex)
    {
        //Handle exception and  Close socket here, use your own code 
        return;
    }
}

class ClientInfo
{
    private const int BUFSIZE = 1024 ; // Max size of buffer , depends on solution  
    private const int BUFLENSIZE = 2; // lenght of lenght , depends on solution
    public int BytesReceived = 0 ;
    public byte[] RcvBuffer { get; set; }
    public byte[] LengthBuffer { get; set; }

    public Socket Soket { get; set; }

    public ClientInfo(Socket clntSock)
    {
        Soket = clntSock;
        RcvBuffer = null;
        LengthBuffer = new byte[ BUFLENSIZE ];
    }   

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

    Socket servSock = (Socket)asyncResult.AsyncState;
    Socket clntSock = null;

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

    }
    catch (SocketException se)
    {
        clntSock.Close();
    }
}
private static void Receive(ClientInfo cInfo )
{
    try
    {
        if (cInfo.RcvBuffer == null)
        {
            cInfo.Soket.BeginReceive(cInfo.LengthBuffer, 0, 2, SocketFlags.None, ReceiveCallback, cInfo);

        }
        else
        {
            cInfo.Soket.BeginReceive(cInfo.RcvBuffer, 0, cInfo.BytesReceived, SocketFlags.None, ReceiveCallback, cInfo);

        }

    }
    catch (SocketException se)
    {
        return;
    }
    catch (Exception ex)
    {
        return;
    }

}


1

Ви можете спробувати використовувати рамку під назвою ACE (Адаптивне середовище комунікацій), яка є загальною основою C ++ для мережевих серверів. Це дуже міцний, зрілий продукт і розроблений для підтримки додатків з високим обсягом надійності до високого рівня.

Рамка стосується досить широкого спектру моделей одночасності, і, ймовірно, має таку, яка підходить для вашої програми поза програмою. Це повинно спростити систему налагодження, оскільки більшість неприємних проблем паралельної валюти вже розібрані. Недоліком тут є те, що фреймворк написаний на C ++ і не є найбільш теплим і пухнастим базою кодів. З іншого боку, ви отримуєте тестовану, мережеву інфраструктуру промислового класу та надзвичайно масштабовану архітектуру.


2
Це гарна пропозиція, але з тегів цього питання, я вважаю, що ОП буде використовувати C #
JPCosta

Я помітив що; було припущення, що це доступно для C ++, і я не знаю нічого еквівалентного для C #. Налагодження такої системи не найкраще в найкращі часи, і ви можете отримати повернення від переходу до цієї рамки, навіть якщо це означає перехід на C ++.
ЗанепокоєнийOfTunbridgeWells

Так, це C #. Я шукаю хороших .net-рішень. Я мав би бути більш чітким, але я припускав, що люди будуть читати теги
Ерік Функенбуш


1

Ну, здається, що .NET-сокети забезпечують select () - це найкраще для обробки даних. Для виводу я мав би пул записів для запису сокетів, які прослуховують робочу чергу, приймаючи дескриптор / об'єкт сокета як частину робочого елемента, тому вам не потрібна нитка на сокет.


1

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


1

щоб люди копіювали вставлення прийнятої відповіді, ви можете переписати метод acceptCallback, видаливши всі виклики _serverSocket.BeginAccept (новий AsyncCallback (acceptCallback), _serverSocket); і поставити це остаточно {} так:

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);
       }
       catch (SocketException e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       catch (Exception e)
       {
         if (conn.socket != null)
         {
           conn.socket.Close();
           lock (_sockets)
           {
             _sockets.Remove(conn);
           }
         }
       }
       finally
       {
         //Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
         _serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);       
       }
     }

Ви навіть можете видалити перший улов, оскільки його вміст однаковий, але це шаблонний метод, і ви повинні використовувати набране виключення, щоб краще обробляти винятки та зрозуміти, що спричинило помилку, тому просто застосуйте ці уловки з корисним кодом


0

Я б рекомендував прочитати ці книги на ACE

щоб отримати ідеї про шаблони, що дозволяють створити ефективний сервер.

Хоча ACE реалізований на C ++, книги охоплюють багато корисних зразків, які можна використовувати в будь-якій мові програмування.


-1

Щоб було зрозуміло, я шукаю рішення на базі .net (якщо це можливо, C #, але будь-яка мова .net буде працювати)

Ви не збираєтесь отримати найвищий рівень масштабованості, якщо ви будете користуватися виключно .NET. Паузи ГК можуть утруднити затримку.

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

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


1
Я не розумію ваш коментар про паузу в GC .. Я ніколи не бачив системи з проблемами масштабованості, яка була безпосередньо пов'язана з GC.
Маркт

4
Набагато ймовірніше, що ви створили додаток, який не може змінювати масштаби через погану архітектуру, ніж через те, що існує GC. Побудовано величезні масштабовані + ефективні системи як для .NET, так і для Java. В обох посиланнях, які ви надали, причиною було не безпосередньо вивезення сміття .., а пов'язане із заміною купи. Я б підозрював, що це справді проблема архітектури, якої можна було б уникнути. Якщо ви можете мені показати мовою, що неможливо побудувати систему, яка не може масштабувати, я з радістю буду її використовувати;)
markt

1
Я не згоден з цим коментарем. Невідомо, питання, на які ви посилаєтеся, - це Java, і вони спеціально мають справу з більшим розподілом пам'яті та намагаються вручну примусити gc. У мене насправді не відбувається велика кількість розподілу пам'яті. Це просто не проблема. Але спасибі Так, модель асинхронного програмування зазвичай реалізується поверх перекритого IO.
Ерік Функенбуш

1
Насправді найкращою практикою є не постійно вручну змушувати GC збирати. Це може дуже добре погіршити ваш додаток. .NET GC - це покоління GC, яке налаштовуватиме на використання вашого додатка. Якщо ви дійсно думаєте, що вам потрібно вручну викликати GC.Collect, я б сказав, що ваш код, швидше за все, потрібно написати іншим способом ..
markt

1
@markt, це коментар для людей, які насправді нічого не знають про збирання сміття. Якщо у вас є час простою, нічого поганого в тому, щоб зробити колекцію вручну. Після завершення роботи програма не погіршить вашу програму. Наукові документи показують, що покоління ГК працюють, оскільки це наближення терміну експлуатації ваших об'єктів. Очевидно, це не ідеальне уявлення. Насправді є парадокс, коли найстаріше покоління часто має найвищий коефіцієнт сміття, оскільки воно ніколи не збирає сміття.
Невідомо

-1

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

http://www.pushframework.com


1
Це повідомлення було позначено C # і .net. Чому ви запропонували структуру C ++?
Ерік Функенбуш

Можливо, тому, що він це написав. kromsoftware.com/…
quillbreaker

чи підтримує pushframework кілька екземплярів сервера? якщо ні, то як він масштабується?
esskar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.