Cómo escribir un servidor escalable basado en Tcp / Ip


148

Estoy en la fase de diseño de escribir una nueva aplicación de servicio de Windows que acepte conexiones TCP / IP para conexiones de larga ejecución (es decir, esto no es como HTTP donde hay muchas conexiones cortas, sino que un cliente se conecta y permanece conectado durante horas o días o incluso semanas).

Estoy buscando ideas sobre la mejor manera de diseñar la arquitectura de red. Voy a necesitar iniciar al menos un hilo para el servicio. Estoy considerando usar la API de Asynch (BeginRecieve, etc.) ya que no sé cuántos clientes me habré conectado en un momento dado (posiblemente cientos). Definitivamente no quiero iniciar un hilo para cada conexión.

Los datos fluirán principalmente a los clientes desde mi servidor, pero ocasionalmente se enviarán algunos comandos desde los clientes. Esta es principalmente una aplicación de monitoreo en la cual mi servidor envía periódicamente datos de estado a los clientes.

¿Alguna sugerencia sobre la mejor manera de hacer esto lo más escalable posible? Flujo de trabajo básico? Gracias.

EDITAR: Para ser claros, estoy buscando soluciones basadas en .net (C # si es posible, pero cualquier lenguaje .net funcionará)

NOTA DE BONIFICACIÓN: Para recibir la recompensa, espero más que una simple respuesta. Necesitaría un ejemplo funcional de una solución, ya sea como un puntero a algo que podría descargar o un breve ejemplo en línea. Y debe estar basado en .net y Windows (cualquier lenguaje .net es aceptable)

EDITAR: Quiero agradecer a todos los que dieron buenas respuestas. Desafortunadamente, solo pude aceptar uno, y elegí aceptar el método Begin / End más conocido. La solución de Esac puede ser mejor, pero sigue siendo lo suficientemente nueva como para no estar seguro de cómo funcionará.

He votado a favor de todas las respuestas que pensé que eran buenas, desearía poder hacer más por ustedes. Gracias de nuevo.


1
¿Estás absolutamente seguro de que debe ser una conexión de larga duración? Es difícil saber a partir de la información limitada proporcionada, pero yo sólo lo haría si es absolutamente necesario ..
Markt

Sí, tiene que durar mucho tiempo. Los datos deben actualizarse en tiempo real, por lo que no puedo hacer encuestas periódicas, los datos deben enviarse al cliente a medida que ocurren, lo que significa una conexión constante.
Erik Funkenbusch

1
Esa no es una razón válida. Http admite conexiones de larga ejecución muy bien. Simplemente abra una conexión y espere una respuesta (encuesta estancada). Esto funciona bien para muchas aplicaciones de estilo AJAX, etc. ¿Cómo crees que funciona Gmail :-)
TFD

2
Gmail funciona sondeando periódicamente por correo electrónico, no mantiene una conexión de larga duración. Esto está bien para el correo electrónico, donde no se requiere una respuesta en tiempo real.
Erik Funkenbusch

2
Las encuestas o tirones se escalan bien, pero desarrollan latencia rápidamente. Empujar no escala demasiado, pero ayuda a reducir o eliminar la latencia.
andrewbadera

Respuestas:


92

He escrito algo similar a esto en el pasado. Desde mi investigación hace años, demostré que escribir su propia implementación de socket era la mejor opción, utilizando los sockets asíncronos. Esto significaba que los clientes que realmente no hacían nada realmente requerían relativamente pocos recursos. Todo lo que ocurre es manejado por el grupo de hilos .net.

Lo escribí como una clase que gestiona todas las conexiones para los servidores.

Simplemente utilicé una lista para mantener todas las conexiones del cliente, pero si necesita búsquedas más rápidas para listas más grandes, puede escribirla como quiera.

private List<xConnection> _sockets;

También necesita el socket realmente escuchando conexiones entrantes.

private System.Net.Sockets.Socket _serverSocket;

El método de inicio realmente inicia el socket del servidor y comienza a escuchar cualquier conexión entrante.

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

Solo me gustaría señalar que el código de manejo de excepciones se ve mal, pero la razón es que tenía un código de supresión de excepciones allí para que cualquier excepción se suprima y regrese falsesi se configuró una opción de configuración, pero quería eliminarlo por brevedad sake.

El _serverSocket.BeginAccept (nuevo AsyncCallback (acceptCallback)), _serverSocket) anterior configura esencialmente nuestro socket de servidor para llamar al método acceptCallback cada vez que un usuario se conecta. Este método se ejecuta desde el conjunto de hilos .Net, que maneja automáticamente la creación de hilos de trabajo adicionales si tiene muchas operaciones de bloqueo. Esto debería manejar de manera óptima cualquier carga en el servidor.

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

El código anterior esencialmente acaba de terminar de aceptar la conexión que entra, las colas, BeginReceiveque es una devolución de llamada que se ejecutará cuando el cliente envíe datos, y luego acceptCallbackpone en cola la siguiente que aceptará la próxima conexión del cliente que ingrese.

La BeginReceivellamada al método es lo que le dice al socket qué hacer cuando recibe datos del cliente. Para BeginReceive, debe darle una matriz de bytes, que es donde copiará los datos cuando el cliente envíe datos. Se ReceiveCallbackllamará al método, que es cómo manejamos la recepción de datos.

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

EDITAR: en este patrón olvidé mencionar que en esta área de código:

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

Lo que generalmente haría es en el código que desee, volver a ensamblar los paquetes en mensajes y luego crearlos como trabajos en el grupo de subprocesos. De esta manera, BeginReceive del siguiente bloque del cliente no se retrasa mientras se ejecuta el código de procesamiento de mensajes.

La devolución de llamada de aceptación finaliza la lectura del socket de datos llamando al final de la recepción. Esto llena el búfer proporcionado en la función de recepción de inicio. Una vez que haga lo que quiera donde dejé el comentario, llamaremos al siguiente BeginReceivemétodo que ejecutará la devolución de llamada nuevamente si el cliente envía más datos. Ahora aquí está la parte realmente complicada, cuando el cliente envía datos, su devolución de llamada de recepción solo se puede llamar con parte del mensaje. El reensamblaje puede volverse muy, muy complicado. Utilicé mi propio método y creé una especie de protocolo propietario para hacer esto. Lo dejé fuera, pero si lo solicita, puedo agregarlo. Este controlador fue en realidad el código más complicado que jamás haya escrito.

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

El método de envío anterior en realidad usa una Sendllamada síncrona , para mí eso estuvo bien debido al tamaño de los mensajes y la naturaleza multiproceso de mi aplicación. Si desea enviar a cada cliente, simplemente necesita recorrer la lista _sockets.

La clase xConnection que ves mencionada anteriormente es básicamente un contenedor simple para que un socket incluya el búfer de bytes, y en mi implementación, algunos extras.

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

También para referencia aquí están los usingmensajes que incluyo, ya que siempre me molesto cuando no están incluidos.

using System.Net.Sockets;

Espero que sea útil, puede que no sea el código más limpio, pero funciona. También hay algunos matices en el código que debería estar cansado de cambiar. Por un lado, solo tiene una sola BeginAcceptllamada en cualquier momento. Solía ​​haber un error .net muy molesto alrededor de esto, que fue hace años, así que no recuerdo los detalles.

Además, en el ReceiveCallbackcódigo, procesamos todo lo recibido del socket antes de poner en cola la próxima recepción. Esto significa que para un solo socket, en realidad solo estamos ReceiveCallbackuna vez en cualquier momento, y no necesitamos usar sincronización de subprocesos. Sin embargo, si reordena esto para llamar a la próxima recepción inmediatamente después de extraer los datos, lo que puede ser un poco más rápido, deberá asegurarse de sincronizar correctamente los hilos.

Además, corté mucho mi código, pero dejé la esencia de lo que está sucediendo en su lugar. Este debería ser un buen comienzo para su diseño. Deja un comentario si tienes más preguntas sobre esto.


1
Esta es una buena respuesta Kevin ... parece que estás en camino de obtener la recompensa. :)
Erik Funkenbusch

66
No sé por qué esta es la respuesta más votada. Begin * End * no es la forma más rápida de hacer redes en C #, ni la más escalable. ES MÁS rápido que síncrono, pero hay muchas operaciones que se llevan a cabo bajo el capó en Windows que realmente ralentizan esta ruta de red.
esac

66
Tenga en cuenta lo que esac escribió en el comentario anterior. El patrón de inicio-fin probablemente funcionará para usted hasta cierto punto, diablos, mi código actualmente está usando inicio-fin, pero hay mejoras en sus limitaciones en .net 3.5. No me importa la recompensa, pero le recomendaría que lea el enlace en mi respuesta, incluso si implementa este enfoque. "Mejoras de rendimiento de socket en la versión 3.5"
jvanderh

1
Solo quería incluirlos, ya que puede que no haya sido lo suficientemente claro, este es el código de la era .net 2.0, donde creo que este era un patrón muy viable. Sin embargo, la respuesta de esac parece ser algo más moderna si apunta a .net 3.5, lo único que tengo es el lanzamiento de eventos :) pero eso se puede cambiar fácilmente. Además, hice pruebas de rendimiento con este código y en un opteron de doble núcleo, 2Ghz fue capaz de maximizar un ethernet de 100Mbps, y eso agregó una capa de cifrado en la parte superior de este código.
Kevin Nisbet

1
@KevinNisbet Sé que esto es bastante tarde, pero para cualquiera que use esta respuesta para diseñar sus propios servidores, el envío también debe ser asíncrono, porque de lo contrario te abres ante la posibilidad de un punto muerto. Si ambos lados escriben datos que llenan sus memorias intermedias respectivas, los Sendmétodos se bloquearán indefinidamente en ambos lados, porque no hay nadie que lea los datos de entrada.
Luaan

83

Hay muchas formas de realizar operaciones de red en C #. Todos ellos utilizan diferentes mecanismos bajo el capó, y por lo tanto sufren grandes problemas de rendimiento con una alta concurrencia. Las operaciones Begin * son una de estas que muchas personas a menudo confunden con ser la forma más rápida de hacer redes.

Para resolver estos problemas, presentaron el conjunto de métodos * Async: Desde MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

La clase SocketAsyncEventArgs es parte de un conjunto de mejoras a la clase System.Net.Sockets .. ::. Socket que proporciona un patrón asíncrono alternativo que puede ser utilizado por aplicaciones especializadas de socket de alto rendimiento. Esta clase fue diseñada específicamente para aplicaciones de servidor de red que requieren un alto rendimiento. Una aplicación puede usar el patrón asincrónico mejorado exclusivamente o solo en áreas calientes específicas (por ejemplo, cuando recibe grandes cantidades de datos).

La característica principal de estas mejoras es evitar la asignación y sincronización repetidas de objetos durante la E / S de socket asíncrono de alto volumen. El patrón de diseño Begin / End implementado actualmente por la clase System.Net.Sockets .. ::. Socket requiere que se asigne un objeto System .. ::. IAsyncResult para cada operación de socket asíncrono.

Debajo de las cubiertas, la API * Async utiliza puertos de finalización de E / S, que es la forma más rápida de realizar operaciones de red, consulte http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

Y solo para ayudarlo, incluyo el código fuente de un servidor telnet que escribí usando la API * Async. Solo incluyo las porciones relevantes. También para tener en cuenta, en lugar de procesar los datos en línea, en su lugar opto por empujarlos a una cola sin bloqueo (sin esperar) que se procesa en un hilo separado. Tenga en cuenta que no incluyo la clase de grupo correspondiente, que es solo un grupo simple que creará un nuevo objeto si está vacío, y la clase de búfer, que es solo un búfer autoexpandible que realmente no es necesario a menos que esté recibiendo un indeterminista la cantidad de datos. Si desea más información, no dude en enviarme un 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
    }
}

Esto es bastante sencillo y un ejemplo simple. Gracias. Voy a tener que evaluar los pros y los contras de cada método.
Erik Funkenbusch

No he tenido oportunidad de probarlo, pero tengo la vaga sensación de una condición de carrera aquí por alguna razón. Primero, si recibe muchos mensajes, no sé si los eventos se procesarán en orden (puede que no sea importante para la aplicación de los usuarios, pero debe tenerse en cuenta) o podría estar equivocado y los eventos se procesarán en orden. En segundo lugar, es posible que lo haya perdido, pero ¿no existe el riesgo de que el búfer se sobrescriba mientras DataReceived aún se está ejecutando si tarda mucho tiempo? Si se abordan estas preocupaciones posiblemente injustificadas, creo que esta es una muy buena solución moderna.
Kevin Nisbet

1
En mi caso, para mi servidor telnet, 100%, SÍ están en orden. La clave es configurar el método de devolución de llamada adecuado antes de llamar a AcceptAsync, ReceiveAsync, etc. Será necesario modificarlo.
esac

1
El punto 2 también es algo que deberá tener en cuenta. Estoy almacenando mi objeto 'Conexión' en el contexto SocketAsyncEventArgs. Lo que esto significa es que solo tengo un búfer de recepción por conexión. No estoy publicando otra recepción con este SocketAsyncEventArgs hasta que DataReceived esté completo, por lo que no se pueden leer más datos sobre esto hasta que esté completo. ASESORO que no se realicen operaciones largas con estos datos. De hecho, muevo todo el búfer de todos los datos recibidos a una cola sin bloqueo y luego lo proceso en un hilo separado. Esto garantiza una baja latencia en la parte de la red.
esac

1
En una nota al margen, escribí pruebas unitarias y pruebas de carga para este código, y al aumentar la carga del usuario de 1 usuario a 250 usuarios (en un solo sistema de doble núcleo, 4 GB de RAM), el tiempo de respuesta para 100 bytes (1 paquete) y 10000 bytes (3 paquetes) permanecieron iguales en toda la curva de carga del usuario.
esac

46

Solía ​​haber una muy buena discusión sobre TCP / IP escalable usando .NET escrito por Chris Mullins de Coversant, desafortunadamente parece que su blog ha desaparecido de su ubicación anterior, por lo que intentaré juntar sus consejos de memoria (algunos comentarios útiles de sus aparecen en este hilo: C ++ vs. C #: Desarrollando un servidor IOCP altamente escalable )

En primer lugar, tenga en cuenta que tanto el uso Begin/Endcomo los Asyncmétodos en la Socketclase utilizan los puertos de finalización de E / S (IOCP) para proporcionar escalabilidad. Esto hace una diferencia mucho mayor (cuando se usa correctamente; ver más abajo) en la escalabilidad que cuál de los dos métodos que realmente elige para implementar su solución.

Las publicaciones de Chris Mullins se basaron en el uso Begin/End, que es con lo que personalmente tengo experiencia. Tenga en cuenta que Chris armó una solución basada en esto que amplió hasta 10,000s de conexiones de clientes concurrentes en una máquina de 32 bits con 2GB de memoria, y hasta 100,000s en una plataforma de 64 bits con suficiente memoria. Desde mi propia experiencia con esta técnica (aunque ni mucho menos cerca de este tipo de carga) no tengo motivos para dudar de estas cifras indicativas.

IOCP versus hilo por conexión o primitivas 'select'

La razón por la que desea usar un mecanismo que usa IOCP debajo del capó es que usa un grupo de subprocesos de Windows de muy bajo nivel que no activa ningún subproceso hasta que haya datos reales en el canal IO del que está tratando de leer ( tenga en cuenta que IOCP también se puede usar para el archivo IO). El beneficio de esto es que Windows no tiene que cambiar a un subproceso solo para descubrir que aún no hay datos, por lo que esto reduce la cantidad de cambios de contexto que su servidor tendrá que hacer al mínimo requerido.

El cambio de contexto es lo que definitivamente matará el mecanismo de 'hilo por conexión', aunque esta es una solución viable si solo se trata de unas pocas docenas de conexiones. Sin embargo, este mecanismo no es "escalable" de ninguna manera.

Consideraciones importantes al usar IOCP

Memoria

En primer lugar, es fundamental comprender que IOCP puede provocar fácilmente problemas de memoria en .NET si su implementación es demasiado ingenua. Cada BeginReceivellamada de IOCP dará como resultado "fijación" del búfer en el que está leyendo. Para obtener una buena explicación de por qué esto es un problema, consulte: Weblog de Yun Jin: OutOfMemoryException y Pinning .

Afortunadamente, este problema puede evitarse, pero requiere un poco de compensación. La solución sugerida es asignar un gran byte[]búfer al inicio de la aplicación (o cerca de ella), de al menos 90 KB más o menos (a partir de .NET 2, el tamaño requerido puede ser mayor en versiones posteriores). La razón para hacer esto es que las asignaciones de memoria grandes terminan automáticamente en un segmento de memoria no compactante (The Large Object Heap) que efectivamente se fija automáticamente. Al asignar un búfer grande al inicio, se asegura de que este bloque de memoria inamovible se encuentre en una 'dirección relativamente baja' donde no se interpondrá y causará fragmentación.

Luego puede usar compensaciones para segmentar este gran búfer en áreas separadas para cada conexión que necesita leer algunos datos. Aquí es donde entra en juego una compensación; Como este búfer debe asignarse previamente, tendrá que decidir cuánto espacio de búfer necesita por conexión y qué límite superior desea establecer en la cantidad de conexiones a las que desea escalar (o puede implementar una abstracción que puede asignar buffers fijados adicionales una vez que los necesite).

La solución más simple sería asignar a cada conexión un solo byte en un desplazamiento único dentro de este búfer. Luego puede hacer una BeginReceivellamada para que se lea un solo byte y realizar el resto de la lectura como resultado de la devolución de llamada que recibe.

Procesando

Cuando recibe la devolución de llamada de la Beginllamada que realizó, es muy importante darse cuenta de que el código en la devolución de llamada se ejecutará en el subproceso IOCP de bajo nivel. Es absolutamente esencial que evite operaciones largas en esta devolución de llamada. El uso de estos subprocesos para un procesamiento complejo eliminará su escalabilidad con la misma eficacia que el uso de 'subproceso por conexión'.

La solución sugerida es usar la devolución de llamada solo para poner en cola un elemento de trabajo para procesar los datos entrantes, que se ejecutarán en algún otro hilo. Evite cualquier operación de bloqueo potencial dentro de la devolución de llamada para que el subproceso IOCP pueda volver a su grupo lo más rápido posible. En .NET 4.0, sugeriría que la solución más fácil es generar un Task, dándole una referencia al socket del cliente y una copia del primer byte que ya fue leído por la BeginReceivellamada. Esta tarea es responsable de leer todos los datos del socket que representan la solicitud que está procesando, ejecutarla y luego realizar una nueva BeginReceivellamada para poner en cola el socket para IOCP una vez más. Antes de .NET 4.0, puede usar ThreadPool o crear su propia implementación de cola de trabajo con subprocesos.

Resumen

Básicamente, sugeriría usar el código de muestra de Kevin para esta solución, con las siguientes advertencias agregadas:

  • Asegúrate de que el búfer al que pasas BeginReceiveya esté "anclado"
  • Asegúrese de que la devolución de llamada que pasa BeginReceiveno haga más que poner en cola una tarea para manejar el procesamiento real de los datos entrantes

Cuando haga eso, no tengo dudas de que podría replicar los resultados de Chris para aumentar potencialmente a cientos de miles de clientes simultáneos (dado el hardware adecuado y una implementación eficiente de su propio código de procesamiento por supuesto;)


1
Para anclar un bloque de memoria más pequeño, el método Alloc del objeto GCHandle se puede usar para anclar el búfer. Una vez hecho esto, el UnsafeAddrOfPinnedArrayElement del objeto Marshal puede usarse para obtener un puntero al búfer. Por ejemplo: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan

@BobBryan A menos que pierda un punto sutil que está tratando de hacer, ese enfoque en realidad no ayuda con el problema que mi solución está tratando de resolver mediante la asignación de bloques grandes, es decir, el potencial de fragmentación dramática de la memoria inherente a la asignación repetida de pequeños bloques anclados de la memoria
jerryjvl

Bueno, el punto es que no tiene que asignar un bloque grande para mantenerlo anclado en la memoria. Puede asignar bloques más pequeños y usar la técnica anterior para anclarlos en la memoria para evitar que el gc los mueva. Puede mantener una referencia a cada uno de los bloques más pequeños, al igual que mantiene una referencia a un solo bloque más grande, y reutilizarlos según sea necesario. Cualquiera de los dos enfoques es válido: solo estaba señalando que no tiene que usar un búfer muy grande. Pero, habiendo dicho que a veces usar un búfer muy grande es la mejor manera de hacerlo, ya que el gc lo tratará de manera más eficiente.
Bob Bryan

@BobBryan ya que fijar el búfer ocurre automáticamente cuando llamas a BeginReceive, la fijación no es realmente el punto más destacado aquí; la eficiencia era;) ... y esto es especialmente preocupante cuando se trata de escribir un servidor escalable, de ahí la necesidad de asignar bloques grandes para usar para el espacio de búfer.
jerryjvl

@jerryjvl Lamento plantear una pregunta muy antigua, sin embargo, recientemente descubrí este problema exacto con los métodos de asincronización BeginXXX / EndXXX. Esta es una gran publicación, pero tomó mucho trabajo para encontrarla. Me gusta su solución sugerida, pero no entiendo una parte de ella: "Entonces puede hacer una llamada BeginReceive para que se lea un solo byte y realizar el resto de la lectura como resultado de la devolución de llamada". ¿Qué quiere decir con realizar el resto de la preparación como resultado de la devolución de llamada que recibe?
Mausimo

22

Ya obtuvo la mayor parte de la respuesta a través de los ejemplos de código anteriores. Usar la operación asincrónica de E / S es absolutamente el camino a seguir aquí. Async IO es la forma en que Win32 está diseñado internamente para escalar. El mejor rendimiento posible que puede obtener se logra utilizando Puertos de finalización, vinculando sus sockets a puertos de finalización y tiene un grupo de subprocesos esperando la finalización del puerto de finalización. La sabiduría común es tener 2-4 subprocesos por CPU (núcleo) esperando su finalización. Recomiendo leer estos tres artículos de Rick Vicik del equipo de rendimiento de Windows:

  1. Diseño de aplicaciones para el rendimiento - Parte 1
  2. Diseño de aplicaciones para el rendimiento - Parte 2
  3. Diseño de aplicaciones para el rendimiento - Parte 3

Dichos artículos cubren principalmente la API nativa de Windows, pero son una lectura obligada para cualquiera que intente comprender la escalabilidad y el rendimiento. También tienen algunos informes sobre el lado administrado de las cosas.

Lo segundo que debe hacer es asegurarse de revisar el libro Mejora del rendimiento y escalabilidad de aplicaciones .NET , que está disponible en línea. Encontrará consejos pertinentes y válidos sobre el uso de subprocesos, llamadas asincrónicas y bloqueos en el Capítulo 5. Pero las gemas reales se encuentran en el Capítulo 17, donde encontrará cosas buenas como orientación práctica para ajustar su grupo de subprocesos. Mis aplicaciones tuvieron algunos problemas serios hasta que ajusté maxIothreads / maxWorkerThreads según las recomendaciones de este capítulo.

Dices que quieres hacer un servidor TCP puro, así que mi siguiente punto es falso. Sin embargo , si te encuentras acorralado y utilizas la clase WebRequest y sus derivados, ten en cuenta que hay un dragón vigilando esa puerta: el ServicePointManager . Esta es una clase de configuración que tiene un propósito en la vida: arruinar su rendimiento. Asegúrese de liberar su servidor del ServicePoint.ConnectionLimit impuesto artificialmente o su aplicación nunca escalará (le permito descubrir cuál es el valor predeterminado ...). También puede reconsiderar la política predeterminada de enviar un encabezado Expect100Continue en las solicitudes http.

Ahora, sobre la API gestionada por socket central, las cosas son bastante fáciles en el lado de envío, pero son significativamente más complejas en el lado de recepción. Para lograr un alto rendimiento y escala, debe asegurarse de que el socket no esté controlado por el flujo porque no tiene un búfer publicado para recibir. Idealmente para un alto rendimiento, debe publicar con anticipación 3-4 buffers y publicar nuevos buffers tan pronto como recupere uno ( antes de procesar el que regresó) para asegurarse de que el socket siempre tenga un lugar para depositar los datos provenientes de la red. Verá por qué probablemente no podrá lograr esto en breve.

Una vez que haya terminado de jugar con la API BeginRead / BeginWrite y comience el trabajo serio, se dará cuenta de que necesita seguridad en su tráfico, es decir. La autenticación NTLM / Kerberos y el cifrado del tráfico, o al menos la protección contra la manipulación del tráfico. La forma de hacerlo es utilizar el System.Net.Security.NegotiateStream integrado (o SslStream si necesita cruzar dominios dispares). Esto significa que, en lugar de confiar en operaciones asincrónicas de socket recto, dependerá de las operaciones asincrónicas AuthenticatedStream. Tan pronto como obtenga un socket (ya sea desde connect en el cliente o desde accept en el servidor), cree un flujo en el socket y lo envíe para autenticación, llamando a BeginAuthenticateAsClient o BeginAuthenticateAsServer. Después de que se complete la autenticación (al menos su caja fuerte de la locura nativa de InitiateSecurityContext / AcceptSecurityContext ...) hará su autorización verificando la propiedad RemoteIdentity de su transmisión autenticada y haciendo cualquier verificación de ACL que su producto debe admitir. Después de eso, enviará mensajes utilizando BeginWrite y los recibirá con BeginRead. Este es el problema del que estaba hablando antes: no podrás publicar múltiples buffers de recepción, porque las clases AuthenticateStream no admiten esto. La operación BeginRead administra internamente todas las E / S hasta que haya recibido una trama completa, de lo contrario no podría manejar la autenticación del mensaje (descifrar la trama y validar la firma en la trama). Aunque en mi experiencia, el trabajo realizado por las clases AuthenticatedStream es bastante bueno y no debería tener ningún problema. Es decir. debería poder saturar la red GB con solo un 4-5% de CPU. Las clases AuthenticatedStream también le impondrán las limitaciones de tamaño de trama específicas del protocolo (16k para SSL, 12k para Kerberos).

Esto debería ayudarlo a comenzar en el camino correcto. No voy a publicar código aquí, hay un ejemplo perfectamente bueno en MSDN . He realizado muchos proyectos como este y pude escalar a unos 1000 usuarios conectados sin problemas. Por encima de eso, deberá modificar las claves de registro para permitir que el núcleo tenga más identificadores de socket. y asegúrese de implementar en un sistema operativo del servidor , que es W2K3 no XP o Vista (es decir, el sistema operativo del cliente), hace una gran diferencia.

Por cierto, asegúrate de que si tienes operaciones de bases de datos en el servidor o archivo IO, también usas el sabor asíncrono para ellas, o agotarás el grupo de subprocesos en poco tiempo. Para las conexiones de SQL Server, asegúrese de agregar el 'Procesamiento asincrónico = verdadero' a la cadena de conexión.


Hay una gran información aquí. Desearía poder recompensar a varias personas. Sin embargo, te he votado. Buenas cosas aquí, gracias.
Erik Funkenbusch

11

Tengo un servidor en ejecución en algunas de mis soluciones. Aquí hay una explicación muy detallada de las diferentes formas de hacerlo en .net: acérquese al cable con sockets de alto rendimiento en .NET

Últimamente he estado buscando formas de mejorar nuestro código y lo investigaré: " Mejoras de rendimiento de socket en la versión 3.5 " que se incluyó específicamente "para uso de aplicaciones que usan E / S de red asíncrona para lograr el mayor rendimiento".

"La característica principal de estas mejoras es evitar la asignación y sincronización repetidas de objetos durante la E / S de socket asíncrono de alto volumen. El patrón de diseño de inicio / fin implementado actualmente por la clase Socket para E / S de socket asíncrono requiere un sistema". El objeto IAsyncResult se asignará para cada operación de socket asíncrono ".

Puedes seguir leyendo si sigues el enlace. Personalmente, probaré su código de muestra mañana para compararlo con lo que tengo.

Editar: Aquí puede encontrar el código de trabajo tanto para el cliente como para el servidor utilizando el nuevo 3.5 SocketAsyncEventArgs para que pueda probarlo en un par de minutos e ir a través del código. Es un enfoque simple, pero es la base para comenzar una implementación mucho más grande. También este artículo de hace casi dos años en MSDN Magazine fue una lectura interesante.



9

¿Ha considerado usar un enlace TCP de red WCF y un patrón de publicación / suscripción? WCF le permitiría concentrarse [principalmente] en su dominio en lugar de fontanería.

Hay muchas muestras de WCF e incluso un marco de publicación / suscripción disponible en la sección de descargas de IDesign que puede ser útil: http://www.idesign.net


8

Me pregunto una cosa:

Definitivamente no quiero iniciar un hilo para cada conexión.

¿Porqué es eso? Windows podría manejar cientos de subprocesos en una aplicación desde al menos Windows 2000. Lo he hecho, es realmente fácil trabajar con ellos si no es necesario sincronizarlos. Especialmente dado que está haciendo muchas E / S (por lo que no está vinculado a la CPU y muchos hilos se bloquearían en el disco o la comunicación de red), no entiendo esta restricción.

¿Has probado la forma de subprocesos múltiples y descubriste que le falta algo? ¿Pretende tener también una conexión de base de datos para cada subproceso (eso mataría al servidor de la base de datos, por lo que es una mala idea, pero se resuelve fácilmente con un diseño de 3 niveles). ¿Te preocupa tener miles de clientes en lugar de cientos y luego realmente tendrás problemas? (Aunque probaría mil hilos o incluso diez mil si tuviera más de 32 GB de RAM, una vez más, dado que no está vinculado a la CPU, el tiempo de cambio de hilo debería ser absolutamente irrelevante).

Aquí está el código: para ver cómo se ve esto, vaya a http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html y haga clic en la imagen.

Clase de servidor:

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

Programa principal del servidor:

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

Clase de cliente:

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

Programa principal del cliente:

using System;
using System.Threading;

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

    private static void RunClient()
    {
      new Client();
    }
  }
}

Windows puede manejar muchos subprocesos, pero .NET no está realmente diseñado para manejarlos. Cada dominio de aplicación .NET tiene un grupo de subprocesos, y no desea agotar ese grupo de subprocesos. No estoy seguro si comienzas un hilo manualmente si proviene del grupo de hilos o no. Aún así, cientos de hilos que no hacen nada la mayor parte del tiempo es un gran desperdicio de recursos.
Erik Funkenbusch

1
Creo que tienes una visión incorrecta de los hilos. Los subprocesos solo provienen del grupo de subprocesos si realmente lo desea; los subprocesos normales no. Cientos de hilos que no hacen nada no desperdician nada :) (Bueno, un poco de memoria, pero la memoria es tan barata que ya no es realmente un problema). Voy a escribir un par de aplicaciones de muestra para esto, publicaré una URL en Una vez que haya terminado. Mientras tanto, le recomiendo que repase lo que escribí arriba nuevamente y trate de responder mis preguntas.
Marcel Popescu

1
Si bien estoy de acuerdo con el comentario de Marcel sobre la vista de los hilos en que los hilos creados no provienen del conjunto de hilos, el resto de la declaración no es correcta. La memoria no se trata de cuánto está instalado en una máquina, todas las aplicaciones en Windows se ejecutan en un espacio de direcciones virtual y en un sistema de 32 bits que le proporciona 2 GB de datos para su aplicación (no importa la cantidad de memoria RAM instalada en la caja). Todavía deben ser manejados por el tiempo de ejecución. Hacer el IO asíncrono no usa un hilo para esperar (usa IOCP que permite IO superpuesto) y es una mejor solución y escalará MUCHO mejor.
Brian ONeil

77
Cuando se ejecutan muchos subprocesos, el problema no es la memoria, sino la CPU. El cambio de contexto entre subprocesos es una operación relativamente costosa y cuanto más subprocesos activos tenga, más cambios de contexto van a ocurrir. Hace unos años realicé una prueba en mi PC con una aplicación de consola C # y con aprox. 500 hilos mi CPU era 100%, los hilos no estaban haciendo nada significativo. Para las comunicaciones de red, es mejor mantener baja la cantidad de hilos.
sipwiz

1
Iría con una solución de Tarea o usaría async / await. La solución de la Tarea parece más simple, mientras que el asíncrono / espera probablemente sea más escalable (específicamente para situaciones vinculadas a IO)
Marcel Popescu

5

Usar Async IO (.et) integrado de .NET BeginReades una buena idea si puede obtener todos los detalles correctos. Cuando configure correctamente sus manejadores de socket / archivo, usará la implementación IOCP subyacente del sistema operativo, permitiendo que sus operaciones se completen sin usar ningún subproceso (o, en el peor de los casos, usar un subproceso que creo que proviene del grupo de subprocesos IO del kernel en su lugar del grupo de subprocesos de .NET, que ayuda a aliviar la congestión del conjunto de subprocesos).

El problema principal es asegurarse de abrir sus sockets / archivos en modo sin bloqueo. La mayoría de las funciones de conveniencia predeterminadas (como File.OpenRead) no hacen esto, por lo que deberá escribir las suyas propias.

Una de las otras preocupaciones principales es el manejo de errores: manejar adecuadamente los errores al escribir código de E / S asíncrono es mucho, mucho más difícil que hacerlo en código síncrono. También es muy fácil terminar con condiciones de carrera y puntos muertos a pesar de que no estés usando hilos directamente, por lo que debes estar al tanto de esto.

Si es posible, debe intentar usar una biblioteca de conveniencia para facilitar el proceso de hacer E / S asíncronas escalables.

El tiempo de ejecución de coordinación de concurrencia de Microsoft es un ejemplo de una biblioteca .NET diseñada para aliviar la dificultad de hacer este tipo de programación. Se ve muy bien, pero como no lo he usado, no puedo comentar qué tan bien se escalaría.

Para mis proyectos personales que necesitan hacer una red asíncrona o E / S de disco, utilizo un conjunto de herramientas de concurrencia / IO de .NET que he creado durante el año pasado, llamado Squared.Task . Está inspirado en bibliotecas como imvu.task y twisted , y he incluido algunos ejemplos de trabajo en el repositorio que hacen E / S de red. También lo he usado en algunas aplicaciones que he escrito, la más grande lanzada públicamente es NDexer (que lo usa para E / S de disco sin hilos). La biblioteca se escribió en base a mi experiencia con imvu.task y tiene un conjunto de pruebas unitarias bastante completas, por lo que le recomiendo que lo pruebe. Si tiene algún problema, me complacerá ofrecerle ayuda.

En mi opinión, según mi experiencia en el uso de E / S asíncronas / sin hilos en lugar de hilos, es un esfuerzo valioso en la plataforma .NET, siempre y cuando esté listo para lidiar con la curva de aprendizaje. Le permite evitar las molestias de escalabilidad impuestas por el costo de los objetos Thread, y en muchos casos, puede evitar por completo el uso de bloqueos y mutexes haciendo un uso cuidadoso de primitivas de concurrencia como Futures / Promises.


Gran información, revisaré sus referencias y veré qué tiene sentido.
Erik Funkenbusch

3

Usé la solución de Kevin, pero él dice que la solución carece de código para el reensamblado de mensajes. Los desarrolladores pueden usar este código para reensamblar mensajes:

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

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

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

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

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

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

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

    ContinueReading(cInfo);
 }

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

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

    public Socket Soket { get; set; }

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

}

public static void AcceptCallback(IAsyncResult asyncResult)
{

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

    try
    {

        clntSock = servSock.EndAccept(asyncResult);

        ClientInfo cInfo = new ClientInfo(clntSock);

        Receive( cInfo );

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

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

        }

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

}


1

Podría intentar usar un marco llamado ACE (Adaptive Communications Environment) que es un marco genérico de C ++ para servidores de red. Es un producto muy sólido y maduro, y está diseñado para admitir aplicaciones de gran confiabilidad y alto volumen hasta grado telco.

El marco trata con una amplia gama de modelos de concurrencia y probablemente tenga uno adecuado para su aplicación lista para usar. Esto debería facilitar la depuración del sistema, ya que la mayoría de los desagradables problemas de concurrencia ya se han resuelto. La desventaja aquí es que el marco está escrito en C ++ y no es la base de código más cálida y esponjosa. Por otro lado, obtiene una infraestructura de red de grado industrial probada y una arquitectura altamente escalable lista para usar.


2
Esa es una buena sugerencia, pero por las etiquetas de la pregunta, creo que el OP usará C #
JPCosta

Me di cuenta que; La sugerencia fue que esto está disponible para C ++ y no estoy al tanto de nada equivalente para C #. La depuración de este tipo de sistema no es fácil en el mejor de los casos y puede obtener un retorno de ir a este marco a pesar de que significa cambiar a C ++.
ConcernedOfTunbridgeWells

Sí, esto es C #. Estoy buscando buenas soluciones basadas en .net. Debería haber sido más claro, pero supuse que la gente leería las etiquetas
Erik Funkenbusch


1

Bueno, los sockets .NET parecen proporcionar select () , que es lo mejor para manejar la entrada. Para la salida, tendría un grupo de hilos de escritura de socket que escucha en una cola de trabajo, aceptando descriptor / objeto de socket como parte del elemento de trabajo, por lo que no necesita un hilo por socket.


1

Usaría los métodos AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync que se agregaron en .Net 3.5. He hecho un punto de referencia y son aproximadamente un 35% más rápidos (tiempo de respuesta y tasa de bits) con 100 usuarios que envían y reciben datos constantemente.


1

para que la gente copie y pegue la respuesta aceptada, puede volver a escribir el método acceptCallback, eliminando todas las llamadas de _serverSocket.BeginAccept (nuevo AsyncCallback (acceptCallback), _serverSocket); y ponerlo en una cláusula finalmente {}, de esta manera:

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

incluso podría eliminar la primera captura, ya que su contenido es el mismo, pero es un método de plantilla y debe usar la excepción escrita para manejar mejor las excepciones y comprender qué causó el error, así que simplemente implemente esas capturas con algún código útil



-1

Para ser claros, estoy buscando soluciones basadas en .net (C # si es posible, pero cualquier lenguaje .net funcionará)

No obtendrá el nivel más alto de escalabilidad si utiliza puramente .NET. Las pausas de GC pueden dificultar la latencia.

Voy a necesitar iniciar al menos un hilo para el servicio. Estoy considerando usar la API de Asynch (BeginRecieve, etc.) ya que no sé cuántos clientes me habré conectado en un momento dado (posiblemente cientos). Definitivamente no quiero iniciar un hilo para cada conexión.

La IO superpuesta generalmente se considera la API más rápida de Windows para la comunicación de red. No sé si esto es lo mismo que su API de Asynch. No use select, ya que cada llamada necesita verificar cada socket que está abierto en lugar de tener devoluciones de llamada en sockets activos.


1
No entiendo su comentario de pausa de GC. Nunca he visto un sistema con problemas de escalabilidad que esté directamente relacionado con GC.
markt

44
Es mucho más probable que cree una aplicación que no pueda escalar debido a una arquitectura deficiente que porque existe GC. Se han construido enormes sistemas escalables + de rendimiento con .NET y Java. En los dos enlaces que proporcionó, la causa no fue directamente la recolección de basura ... sino relacionada con el intercambio de almacenamiento dinámico. Sospecharía que realmente es un problema con la arquitectura que podría haberse evitado. Si me puede mostrar un lenguaje que no es posible construir un sistema que no pueda escalar, con gusto lo usaré;)
Markt

1
No estoy de acuerdo con este comentario. Desconocido, las preguntas a las que hace referencia son Java, y se ocupan específicamente de asignaciones de memoria más grandes e intentan forzar manualmente gc. Realmente no voy a tener grandes cantidades de asignación de memoria aquí. Esto simplemente no es un problema. Pero gracias. Sí, el modelo de programación asincrónica se implementa típicamente sobre IO superpuesto.
Erik Funkenbusch

1
En realidad, la mejor práctica es no obligar constantemente al GC a recolectar. Esto bien podría hacer que su aplicación funcione peor. .NET GC es un GC generacional que se ajustará al uso de su aplicación. Si realmente cree que necesita estar llamando manualmente GC.Collect, diría que su código más probables necesita ser escrita de otra manera ..
Markt

1
@markt, ese es un comentario para las personas que realmente no saben nada sobre la recolección de basura. Si tiene tiempo de inactividad, no hay nada de malo en hacer una recolección manual. No va a empeorar su aplicación cuando termine. Los documentos académicos muestran que los GC generacionales funcionan porque es una aproximación de la vida útil de sus objetos. Obviamente, esta no es una representación perfecta. De hecho, existe una paradoja en la que la generación "más antigua" a menudo tiene la mayor proporción de basura porque nunca se recolecta basura.
Desconocido

-1

Puede usar el marco de código abierto Push Framework para el desarrollo de servidores de alto rendimiento. Está construido en IOCP y es adecuado para escenarios push y difusión de mensajes.

http://www.pushframework.com


1
Esta publicación fue etiquetada con C # y .net. ¿Por qué sugirió un marco C ++?
Erik Funkenbusch

Probablemente porque lo escribió. potatosoftware.com/…
quillbreaker

¿Pushframework soporta múltiples instancias de servidor? si no, ¿cómo se escala?
esskar
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.