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

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

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

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

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

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

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

ИЗМЕНИТЬ: I хотим поблагодарить всех, кто дал хорошие ответы. К сожалению, я мог принять только один, и я решил принять более известный метод начала/конца. Решение Esac может быть лучше, но оно все еще достаточно новое, и я не знаю точно, как это сработает.

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

18 ответов


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

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

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

private List<xConnection> _sockets;

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

private System.Net.Sockets.Socket _serverSocket;

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

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

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

в _serverSocket.BeginAccept(new AsyncCallback (acceptCallback)), _serverSocket) выше по существу устанавливает наш сокет сервера для вызова метода acceptCallback всякий раз, когда пользователь подключается. Этот метод выполняется из .NET 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, вам нужно дать ему массив байтов, который где он будет копировать данные, когда клиент отправляет данные. The 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);

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

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

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

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

using System.Net.Sockets;

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

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

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


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

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

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

основной особенностью этих усовершенствований является избежание повторного выделения и синхронизации объектов во время ввода-вывода асинхронного сокета большого объема.шаблон проектирования Begin/End в настоящее время реализуется системой.Сеть.Розетки..::.Класс сокета требует системы..::.Объект IAsyncResult будет выделен для каждого асинхронного сокета операция.

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

и просто, чтобы помочь вам, я включаю исходный код для сервера telnet, который я написал с помощью API * Async. Я включаю только соответствующие части. Кроме того, чтобы отметить, вместо обработки данных inline, я вместо этого предпочитаю нажать его на блокировку бесплатно (wait free) очередь, которая обрабатывается в отдельном потоке. Обратите внимание, что я не включаю соответствующий класс Пула, который является простым пулом, который создаст новый объект, если он пуст, и класс буфера, который является просто саморасширяющимся буфером, который на самом деле не нужен, если вы не получаете неопределенный объем данных. Если вы хотите больше информации, не стесняйтесь присылать мне 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
    }
}

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

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

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

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

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

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

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

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

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

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

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

обработка

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

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

резюме

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

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

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


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

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

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

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

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

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

после того, как вы закончите играть с BeginRead / BeginWrite API и начать серьезную работу, вы поймете, что вам нужна безопасность движения, т. е. Проверка подлинности NTLM / Kerberos и шифрование трафика или, по крайней мере, защита от изменения трафика. То, как вы это делаете, вы используете встроенную систему.Сеть.Безопасность.NegotiateStream (или SslStream, если вам нужно перейти через разрозненные Домены). Это означает, что вместо того, чтобы полагаться на асинхронные операции прямого сокета, вы будете полагаться на асинхронные операции AuthenticatedStream. Как только вы получите сокет (либо из connect on client, либо из accept on server) вы создаете поток в сокете и отправляете его для аутентификации, вызывая 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" для строки подключения.


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

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

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

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

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


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

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


меня интересует одна вещь:

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

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

вы протестировали многопоточный способ и обнаружили, что ему чего-то не хватает? Вы также собираетесь иметь подключение к базе данных для каждого потока (это убьет сервер базы данных, поэтому это плохая идея, но она легко решается с помощью 3-уровневого дизайна). Вы беспокоитесь, что у вас будут тысячи клиентов вместо сотен, и тогда у вас действительно будут проблемы? (Хотя я бы попробовал тысячу потоков или даже десять тысяч, если бы у меня было 32 + GB RAM-опять же, учитывая, что вы не связаны с процессором, время переключения потоков должно быть абсолютно неуместным.)

вот код-чтобы увидеть, как это выглядит работает, перейдите к 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();
    }
  }
}

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

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

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

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

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

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

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


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


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

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

}

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

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


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


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


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


чтобы люди скопировали вставку принятого ответа, вы можете переписать метод 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);       
       }
     }

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


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

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

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


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

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

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

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


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

http://www.pushframework.com