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

Я нахожусь на этапе проектирования нового приложения Windows Service, которое принимает соединения TCP/IP для длинных подключений (т.е. это не похоже на HTTP, где есть много коротких соединений, а клиент подключается и остается подключенным в течение нескольких часов или дней или даже недель).

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

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

Любые предложения по наилучшему способу сделать это максимально масштабируемым? Основной рабочий процесс? Спасибо.

EDIT: чтобы быть ясным, я ищу решения на основе .net(С#, если возможно, но любой язык .net будет работать)

BOUNTY ПРИМЕЧАНИЕ. Чтобы получить награду, я ожидаю более простого ответа. Мне нужен рабочий пример решения, либо как указатель на то, что я мог бы загрузить, либо короткий пример в строке. И это должно быть .net и Windows based (любой язык .net допустим)

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

У меня есть все ответы, которые, как я думал, были хорошими, я бы хотел сделать больше для вас, ребята. Еще раз спасибо.

Ответ 1

В прошлом я написал нечто подобное этому. Из моих исследований, проведенных несколько лет назад, было показано, что создание собственной реализации сокетов было лучшим выбором, используя асинхронные сокеты. Это означало, что клиенты, которые действительно ничего не делают, фактически требуют относительно небольших ресурсов. Все, что происходит, обрабатывается пулом потоков .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, если был задан параметр конфигурации, но Я хотел удалить его ради краткости.

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

    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 следующего блока от клиента не задерживается, пока не выполняется какой-либо код обработки сообщений.

Обратный вызов 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, для меня это было хорошо из-за размеров сообщений и многопоточного характера моего приложения. Если вы хотите отправить каждому клиенту, вам просто нужно пропустить список _sockets.

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

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

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

using System.Net.Sockets;

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

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

Память

Прежде всего, важно понять, что IOCP может легко привести к проблемам с памятью в .NET, если ваша реализация слишком наивна. Каждый вызов IOCP BeginReceive приведет к "закреплению" буфера, который вы читаете. Для хорошего объяснения, почему это проблема, см.: Yun Jin Weblog: OutOfMemoryException и Pinning.

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

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

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

Обработка

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

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

Резюме

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

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

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

Ответ 4

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

Указанные статьи охватывают, в основном, родной 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. Это проблема, о которой я говорил раньше, что вы не сможете публиковать несколько буферов приема, потому что классы AuthenticateStream не поддерживают это. Операция BeginRead полностью управляет всем IO до тех пор, пока вы не получите весь фрейм, иначе он не сможет обработать аутентификацию сообщения (дешифровать фрейм и проверить подпись на фрейме). Хотя по моему опыту работа, выполняемая классами AuthenticatedStream, достаточно хороша и не должна иметь никаких проблем с ней. То есть. вы должны иметь возможность насыщать сеть GB только 4-5% CPU. Классы AuthenticatedStream также накладывают на вас ограничения по размеру определенного протокола (16k для SSL, 12k для Kerberos).

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

BTW убедитесь, что если у вас есть операции с базами данных на сервере или в файле IO, вы также используете для них асинхронный вкус, иначе вы слейте пул потоков в кратчайшие сроки. Для соединений SQL Server убедитесь, что вы добавили в строку подключения "Asyncronous Processing = true".

Ответ 5

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

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

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

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

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

Ответ 6

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

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

Ответ 7

Мне интересно об одном:

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

Почему? 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();
    }
  }
}

Ответ 8

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

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

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

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

Microsoft Concurrency Время выполнения координации является одним из примеров библиотеки .NET, предназначенной для облегчения сложности такого программирования. Это выглядит великолепно, но поскольку я не использовал его, я не могу прокомментировать, насколько он масштабируется.

Для моих личных проектов, которые необходимы для работы с асинхронной сетью или дисковыми вводами/выводами, я использую набор инструментов .NET concurrency/​​IO, которые я построил за последний год, называемый Squared.Task. Это вдохновило библиотеки, такие как imvu.task и twisted, и я включил некоторые рабочие примеры в репозиторий, которые делают сетевой ввод-вывод. Я также использовал его в нескольких приложениях, которые я написал - самый крупный публично выпущенный из них NDexer (который использует его для безреверсного диска I/О). Библиотека была написана на основе моего опыта работы с imvu.task и имеет набор довольно полных модульных тестов, поэтому я настоятельно рекомендую вам попробовать. Если у вас возникнут какие-либо проблемы, я был бы рад оказать вам некоторую помощь.

По моему мнению, исходя из моего опыта использования асинхронного/поточного ввода-вывода вместо потоков, стоит упомянуть на платформе .NET, если вы готовы справиться с кривой обучения. Это позволяет избежать проблем с масштабируемостью, налагаемых стоимостью объектов Thread, и во многих случаях вы можете полностью избежать использования блокировок и мьютексов, тщательно используя примитивы concurrency, такие как Futures/Promises.

Ответ 9

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

Ответ 10

Я использовал решение Kevin, но он говорит, что в решении отсутствует код для повторной сборки сообщений. Разработчики могут использовать этот код для повторной сборки сообщений:

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

}

Ответ 12

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

Ответ 13

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

Ответ 14

чтобы люди копировали вложенный принятый ответ, вы можете переписать метод acceptCallback, удалив все вызовы _serverSocket.BeginAccept(новый AsyncCallback (acceptCallback), _serverSocket); и поместите его в предложение finally {} следующим образом:

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

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

Ответ 15

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

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

Ответ 16

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

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

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

Ответ 17

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

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

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

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

Ответ 18

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

http://www.pushframework.com