Como escrever um servidor escalável baseado em TCP / IP


148

Estou na fase de design de escrever um novo aplicativo de serviço do Windows que aceite conexões TCP / IP para conexões de longa execução (isto é, não é como HTTP, onde há muitas conexões curtas, mas um cliente se conecta e permanece conectado por horas ou dias ou semanas).

Estou procurando idéias para a melhor maneira de projetar a arquitetura de rede. Vou precisar iniciar pelo menos um thread para o serviço. Estou pensando em usar a API assíncrona (BeginRecieve, etc.), pois não sei quantos clientes terei conectado a qualquer momento (possivelmente centenas). Definitivamente, não quero iniciar um thread para cada conexão.

Os dados fluirão principalmente para os clientes do meu servidor, mas haverá alguns comandos enviados pelos clientes ocasionalmente. Esse é principalmente um aplicativo de monitoramento no qual meu servidor envia dados de status periodicamente para os clientes.

Alguma sugestão sobre a melhor maneira de tornar isso o mais escalável possível? Fluxo de trabalho básico? Obrigado.

EDIT: Para ficar claro, estou procurando soluções baseadas em .net (c #, se possível, mas qualquer idioma .net funcionará)

NOTA DE RECONHECIMENTO: Para receber a recompensa, espero mais do que uma resposta simples. Eu precisaria de um exemplo prático de solução, como um ponteiro para algo que eu poderia baixar ou um pequeno exemplo em linha. E deve ser baseado em .net e Windows (qualquer idioma .net é aceitável)

EDIT: Quero agradecer a todos que deram boas respostas. Infelizmente, eu só podia aceitar um e optei por aceitar o método Begin / End mais conhecido. A solução da Esac pode muito bem ser melhor, mas ainda é nova o suficiente para que eu não saiba ao certo como vai funcionar.

Eu votei todas as respostas que achei boas, gostaria de poder fazer mais por vocês. Obrigado novamente.


1
Você tem certeza absoluta de que precisa ser uma conexão de longa duração? É difícil dizer a partir da informação limitada fornecida, mas eu só faria isso se for absolutamente necessário ..
Markt

Sim, tem que ser demorado. Os dados devem ser atualizados em tempo real; portanto, não posso fazer pesquisas periódicas; os dados devem ser enviados ao cliente à medida que ocorrem, o que significa uma conexão constante.
Erik Funkenbusch

1
Essa não é uma razão válida. Suporte a conexões longas de longa duração. Você acabou de abrir uma conexão e aguardar uma nova reimpressão (votação paralisada). Isso funciona bem para muitos estilo AJAX aplicativos etc. Como você acha que funciona gmail :-)
TFD

2
O Gmail trabalha pesquisando periodicamente por e-mail; ele não mantém uma conexão de longa duração. Isso é bom para email, onde a resposta em tempo real não é necessária.
Erik Funkenbusch

2
A pesquisa, ou puxar, é bem dimensionada, mas desenvolve a latência rapidamente. O envio não é escalável também, mas ajuda a reduzir ou eliminar a latência.
andrewbadera

Respostas:


92

Eu escrevi algo semelhante a isso no passado. De minha pesquisa, anos atrás, mostrei que escrever sua própria implementação de soquete era a melhor aposta, usando os soquetes assíncronos. Isso significava que os clientes que realmente não estavam fazendo nada realmente exigiam relativamente poucos recursos. Tudo o que ocorre é tratado pelo pool de threads .net.

Eu escrevi como uma classe que gerencia todas as conexões para os servidores.

Simplesmente usei uma lista para armazenar todas as conexões do cliente, mas se você precisar de pesquisas mais rápidas para listas maiores, poderá escrevê-la como quiser.

private List<xConnection> _sockets;

Além disso, você precisa que o soquete esteja realmente ouvindo as conexões recebidas.

private System.Net.Sockets.Socket _serverSocket;

O método start realmente inicia o soquete do servidor e começa a escutar as conexões recebidas.

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

Gostaria apenas de observar que o código de tratamento de exceções parece ruim, mas o motivo é que eu tinha um código de supressão de exceção lá para que quaisquer exceções fossem suprimidas e retornassem falsese uma opção de configuração fosse definida, mas eu queria removê-lo para amor de brevidade.

O _serverSocket.BeginAccept (novo AsyncCallback (acceptCallback)), _serverSocket) acima define essencialmente o soquete do servidor para chamar o método acceptCallback sempre que um usuário se conectar. Esse método é executado no pool de threads .Net, que manipula automaticamente a criação de threads de trabalho adicionais se você tiver muitas operações de bloqueio. Isso deve lidar de maneira ideal com qualquer carga no 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);
       }
     }

O código acima basicamente acabou de aceitar a conexão que entra, enfileira BeginReceiveque é um retorno de chamada que será executado quando o cliente envia dados e, em seguida, enfileira o próximo acceptCallbackque aceitará a próxima conexão do cliente que entrar.

A BeginReceivechamada de método é o que diz ao soquete o que fazer quando recebe dados do cliente. Para BeginReceive, você precisa fornecer uma matriz de bytes, que é onde ele copiará os dados quando o cliente enviar dados. O ReceiveCallbackmétodo será chamado, e é assim que lidamos com o recebimento de dados.

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

Edição: Neste padrão eu esqueci de mencionar que nesta á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);

O que eu geralmente faria é no código que você quiser, é remontar pacotes em mensagens e, em seguida, criá-los como trabalhos no pool de threads. Dessa forma, o BeginReceive do próximo bloco do cliente não é atrasado enquanto qualquer código de processamento de mensagens estiver em execução.

O retorno de chamada de aceitação termina de ler o soquete de dados chamando o recebimento final. Isso preenche o buffer fornecido na função de recebimento inicial. Depois que você fizer o que quiser, onde deixei o comentário, chamamos o próximo BeginReceivemétodo que executará o retorno de chamada novamente se o cliente enviar mais dados. Agora, aqui está a parte realmente complicada: quando o cliente envia dados, seu retorno de chamada de recebimento pode ser chamado apenas com parte da mensagem. A remontagem pode se tornar muito, muito complicada. Eu usei meu próprio método e criei uma espécie de protocolo proprietário para fazer isso. Eu o deixei de fora, mas se você solicitar, eu posso adicioná-lo. Esse manipulador foi, na verdade, o código mais complicado que eu já escrevi.

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

O método de envio acima, na verdade, usa uma Sendchamada síncrona , para mim, devido aos tamanhos das mensagens e à natureza multithread do meu aplicativo. Se você deseja enviar para todos os clientes, basta percorrer a lista _sockets.

A classe xConnection que você vê acima referenciada é basicamente um invólucro simples para um soquete incluir o buffer de bytes e, na minha implementação, alguns extras.

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

Também para referência, aqui estão os que usingeu incluo, pois sempre fico irritado quando eles não estão incluídos.

using System.Net.Sockets;

Espero que seja útil, pode não ser o código mais limpo, mas funciona. Existem também algumas nuances no código que você deve estar cansado de mudar. Por um lado, tenha apenas uma BeginAcceptchamada por vez. Costumava haver um bug .net muito irritante em torno disso, que foi anos atrás, então não me lembro dos detalhes.

Além disso, no ReceiveCallbackcódigo, processamos tudo o que é recebido do soquete antes de enfileirar o próximo recebimento. Isso significa que, para um soquete único, na verdade, estamos apenas ReceiveCallbackuma vez a qualquer momento e não precisamos usar a sincronização de threads. No entanto, se você reordenar isso para chamar a próxima recepção imediatamente após extrair os dados, o que pode ser um pouco mais rápido, será necessário sincronizar corretamente os encadeamentos.

Além disso, cortei muito do meu código, mas deixei a essência do que está acontecendo no lugar. Este deve ser um bom começo para o seu design. Deixe um comentário se tiver mais alguma dúvida sobre isso.


1
Esta é uma boa resposta, Kevin ... parece que você está no caminho certo para receber a recompensa. :)
Erik Funkenbusch

6
Não sei por que essa é a resposta mais votada. Begin * End * não é a maneira mais rápida de criar redes em C #, nem a mais altamente escalável. É mais rápido que o síncrono, mas existem muitas operações ocultas no Windows que realmente diminuem esse caminho de rede.
esac

6
Lembre-se do que esac escreveu no comentário anterior. O padrão de início-fim provavelmente funcionará para você até certo ponto, diabos, meu código está usando o início-fim, mas há melhorias nas limitações do .net 3.5. Não me importo com a recompensa, mas recomendo que você leia o link na minha resposta, mesmo que implemente essa abordagem. "Aprimoramentos no desempenho do soquete na versão 3.5"
jvanderh 21/05/2009

1
Eu só queria jogar o deles, já que talvez não tenha sido claro o suficiente, este é o código da era .net 2.0 onde acredito que esse era um padrão muito viável. No entanto, a resposta da esac parece ser um pouco mais moderna se visar o .net 3.5, o único detalhe que tenho é o lançamento de eventos :), mas isso pode ser facilmente alterado. Além disso, eu testei a taxa de transferência com esse código e, em um opteron 2Ghz de núcleo duplo, consegui maximizar a Ethernet de 100Mbps, e isso adicionou uma camada de criptografia no topo desse código.
22611 Kevin Nisbet

1
@KevinNisbet Eu sei que é tarde demais, mas para quem usa esta resposta para projetar seus próprios servidores - o envio também deve ser assíncrono, porque, caso contrário, você se abre para a possibilidade de um impasse. Se os dois lados gravarem dados que preenchem seus respectivos buffers, os Sendmétodos serão bloqueados indefinidamente nos dois lados, porque não há ninguém lendo os dados de entrada.
precisa saber é

83

Existem várias maneiras de executar operações de rede em C #. Todos eles usam mecanismos diferentes sob o capô e, portanto, sofrem grandes problemas de desempenho com alta simultaneidade. As operações Begin * são uma delas que muitas pessoas confundem por ser a maneira mais rápida / rápida de fazer networking.

Para resolver esses problemas, eles introduziram o conjunto de métodos * Async: No MSDN http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

A classe SocketAsyncEventArgs faz parte de um conjunto de aprimoramentos da classe System.Net.Sockets .. ::. Socket que fornece um padrão assíncrono alternativo que pode ser usado por aplicativos de soquete de alto desempenho especializados. Esta classe foi projetada especificamente para aplicativos de servidor de rede que exigem alto desempenho. Um aplicativo pode usar o padrão assíncrono aprimorado exclusiva ou apenas em áreas quentes de destino (por exemplo, ao receber grandes quantidades de dados).

O principal recurso desses aprimoramentos é evitar a alocação e sincronização repetidas de objetos durante E / S de soquete assíncrono de alto volume. O padrão de design Begin / End implementado atualmente pela classe System.Net.Sockets .. ::. Socket requer que um objeto System .. ::. IAsyncResult seja alocado para cada operação de soquete assíncrona.

Nos bastidores, a * API Async usa portas de conclusão de E / S, que é a maneira mais rápida de executar operações de rede, consulte http://msdn.microsoft.com/en-us/magazine/cc302334.aspx

E apenas para ajudá-lo, estou incluindo o código fonte de um servidor de telnet que escrevi usando a API * Async. Estou incluindo apenas as partes relevantes. Observe também que, em vez de processar os dados em linha, optei por enviá-los para uma fila sem bloqueio (sem espera) que é processada em um encadeamento separado. Observe que eu não estou incluindo a classe Pool correspondente, que é apenas um pool simples, que criará um novo objeto se estiver vazio, e a classe Buffer, que é apenas um buffer auto-expansível que não é realmente necessário, a menos que você esteja recebendo um indeterminado quantidade de dados. Se você quiser mais informações, sinta-se à vontade para me enviar uma MP.

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

Isso é bem simples e um exemplo simples. Obrigado. Vou ter que avaliar os prós e contras de cada método.
Erik Funkenbusch

Eu não tive a chance de testá-lo, mas estou tendo a vaga sensação de uma condição de corrida aqui por algum motivo. Primeiro, se você receber muitas mensagens, não sei se os eventos serão processados ​​em ordem (pode não ser importante para o aplicativo dos usuários, mas deve ser observado) ou posso estar errado e os eventos serão processados ​​em ordem. Segundo, posso ter esquecido, mas não há risco de o buffer ser sobrescrito limpo enquanto o DataReceived ainda estiver em execução se demorar muito? Se essas preocupações possivelmente injustificadas forem abordadas, acho que essa é uma solução moderna muito boa.
22611 Kevin Nisbet

1
No meu caso, para o meu servidor de telnet, 100%, SIM, eles estão em ordem. A chave está definindo o método de retorno de chamada adequado antes de chamar AcceptAsync, ReceiveAsync, etc. No meu caso, faço o SendAsync em um thread separado, portanto, se ele for modificado para executar um padrão Aceitar / Enviar / Receber / Enviar / Receber / Receber / Desconectar, precisará ser modificado.
ESAC

1
O ponto 2 também é algo que você precisará levar em consideração. Estou armazenando meu objeto 'Connection' no contexto SocketAsyncEventArgs. O que isso significa é que eu tenho apenas um buffer de recebimento por conexão. Não estou postando outro recebimento com este SocketAsyncEventArgs até que DataReceived seja concluído, para que nenhum outro dado possa ser lido sobre ele até que seja concluído. Eu aconselho que não sejam feitas operações longas nesses dados. Na verdade, movo todo o buffer de todos os dados recebidos para uma fila sem bloqueio e, em seguida, processo-o em um thread separado. Isso garante baixa latência na parte da rede.
ESAC

1
Em uma nota lateral, escrevi testes de unidade e testes de carga para esse código e, à medida que aumentava a carga do usuário de 1 para 250 usuários (em um único sistema dual core, 4 GB de RAM), o tempo de resposta para 100 bytes (1 pacote) e 10000 bytes (3 pacotes) permaneceram os mesmos durante toda a curva de carga do usuário.
ESAC

46

Costumava haver uma discussão muito boa sobre TCP / IP escalável usando .NET, escrita por Chris Mullins, da Coversant, infelizmente parece que seu blog desapareceu de sua localização anterior, então tentarei reunir seus conselhos de memória (alguns comentários úteis dele aparecem neste segmento: C ++ vs. C #: desenvolvendo um servidor IOCP altamente escalável )

Em primeiro lugar, observe que o uso Begin/Ende os Asyncmétodos na Socketclasse utilizam as portas de conclusão de E / S (IOCP) para fornecer escalabilidade. Isso faz uma diferença muito maior (quando usada corretamente; veja abaixo) na escalabilidade do que qual dos dois métodos você escolhe para implementar sua solução.

As postagens de Chris Mullins foram baseadas no uso Begin/End, que é o que eu pessoalmente tenho experiência. Observe que Chris montou uma solução com base nisso, que escalou até 10.000 milhões de conexões simultâneas de clientes em uma máquina de 32 bits com 2 GB de memória e até 100.000 em uma plataforma de 64 bits com memória suficiente. De minha própria experiência com essa técnica (embora nem de longe esse tipo de carga) não tenho motivos para duvidar desses números indicativos.

IOCP versus encadeamento por conexão ou primitivas 'select'

O motivo pelo qual você deseja usar um mecanismo que usa o IOCP sob o capô é que ele usa um pool de encadeamentos de nível muito baixo do Windows que não ativa nenhum encadeamento até que haja dados reais no canal de E / S que você está tentando ler ( observe que o IOCP também pode ser usado para E / S de arquivo). A vantagem disso é que o Windows não precisa alternar para um encadeamento apenas para descobrir que ainda não existem dados. Isso reduz o número de alternâncias de contexto que seu servidor precisará fazer ao mínimo necessário.

Switches de contexto é o que definitivamente matará o mecanismo 'thread por conexão', embora essa seja uma solução viável se você estiver lidando apenas com algumas dezenas de conexões. Esse mecanismo, no entanto, não é extensivo à imaginação 'escalável'.

Considerações importantes ao usar o IOCP

Memória

Em primeiro lugar, é essencial entender que o IOCP pode facilmente resultar em problemas de memória no .NET se a sua implementação for muito ingênua. Toda BeginReceivechamada IOCP resultará na "fixação" do buffer que você está lendo. Para uma boa explicação de por que isso é um problema, consulte: Weblog de Yun Jin: OutOfMemoryException and Pinning .

Felizmente, esse problema pode ser evitado, mas exige um pouco de troca. A solução sugerida é alocar um grande byte[]buffer na inicialização do aplicativo (ou próximo a ele), de pelo menos 90 KB ou mais (no .NET 2, o tamanho necessário pode ser maior nas versões posteriores). O motivo para fazer isso é que grandes alocações de memória terminam automaticamente em um segmento de memória não compactável (The Large Object Heap) efetivamente fixado automaticamente. Ao alocar um buffer grande na inicialização, você garante que esse bloco de memória imóvel esteja em um "endereço relativamente baixo", onde não atrapalhará e causará fragmentação.

Você pode usar compensações para segmentar esse grande buffer em áreas separadas para cada conexão que precisa ler alguns dados. É aqui que um trade-off entra em cena; como esse buffer precisa ser pré-alocado, você terá que decidir quanto espaço de buffer você precisa por conexão e qual limite superior você deseja definir no número de conexões para as quais deseja dimensionar (ou implementar uma abstração que podem alocar buffers fixados adicionais quando você precisar).

A solução mais simples seria atribuir a cada conexão um único byte em um deslocamento exclusivo dentro desse buffer. Em seguida, você pode fazer uma BeginReceivechamada para que um único byte seja lido e executar o restante da leitura como resultado do retorno de chamada recebido.

Em processamento

Quando você recebe o retorno de chamada da Beginchamada feita, é muito importante perceber que o código no retorno de chamada será executado no encadeamento IOCP de baixo nível. É absolutamente essencial que você evite operações demoradas nesse retorno de chamada. O uso desses encadeamentos para processamento complexo reduzirá sua escalabilidade com a mesma eficácia que o uso de 'encadeamento por conexão'.

A solução sugerida é usar o retorno de chamada apenas para enfileirar um item de trabalho para processar os dados recebidos, que serão executados em algum outro encadeamento. Evite operações potencialmente bloqueadas dentro do retorno de chamada, para que o encadeamento IOCP possa retornar ao seu pool o mais rápido possível. No .NET 4.0, sugiro que a solução mais fácil seja gerar um Task, fornecendo uma referência ao soquete do cliente e uma cópia do primeiro byte que já foi lido pela BeginReceivechamada. Essa tarefa é responsável por ler todos os dados do soquete que representam a solicitação que você está processando, executando-os e fazendo uma nova BeginReceivechamada para enfileirar o soquete para IOCP mais uma vez. Antes do .NET 4.0, você pode usar o ThreadPool ou criar sua própria implementação de fila de trabalho encadeada.

Resumo

Basicamente, sugiro usar o código de exemplo de Kevin para esta solução, com os seguintes avisos adicionados:

  • Verifique se o buffer para o qual você passa BeginReceivejá está 'fixado'
  • Certifique-se de que o retorno de chamada para o qual você passa BeginReceivenão faça nada além de enfileirar uma tarefa para lidar com o processamento real dos dados recebidos

Quando você faz isso, não tenho dúvida de que você poderia replicar os resultados de Chris ao aumentar potencialmente centenas de milhares de clientes simultâneos (dado o hardware certo e uma implementação eficiente do seu próprio código de processamento;)


1
Para fixar um bloco menor de memória, o método Aloc do objeto GCHandle pode ser usado para fixar o buffer. Feito isso, o UnsafeAddrOfPinnedArrayElement do objeto Marshal pode ser usado para obter um ponteiro para o buffer. Por exemplo: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan

@BobBryan A menos que eu perca um ponto sutil que você está tentando fazer, essa abordagem não ajuda com o problema que minha solução está tentando resolver alocando blocos grandes, ou seja, o potencial de fragmentação dramática da memória inerente à alocação repetida de pequenos blocos fixados de memória.
precisa saber é o seguinte

Bem, o ponto é que você não precisa alocar um bloco grande para mantê-lo fixo na memória. Você pode alocar blocos menores e usar a técnica acima para fixá-los na memória para evitar que o gc os mova. Você pode manter uma referência a cada um dos blocos menores, assim como mantém uma referência a um único bloco maior e reutilizá-los conforme necessário. Qualquer uma das abordagens é válida - eu estava apenas apontando que você não precisa usar um buffer muito grande. Mas, tendo dito que, às vezes, usar um buffer muito grande é o melhor caminho a seguir, já que o gc o tratará com mais eficiência.
Bob Bryan

@BobBryan, já que a fixação do buffer acontece automaticamente quando você chama BeginReceive, a fixação não é realmente o ponto mais importante aqui; a eficiência foi;) ... e isso é especialmente uma preocupação ao tentar escrever um servidor escalável, daí a necessidade de alocar blocos grandes para usar no espaço em buffer.
precisa saber é o seguinte

@jerryjvl Desculpe por fazer uma pergunta muito antiga, mas recentemente descobri esse problema exato com os métodos de assíncrono BeginXXX / EndXXX. Este é um ótimo post, mas foi preciso muito trabalho para encontrar. Gosto da sua solução sugerida, mas não entendo parte dela: "Então você pode fazer uma chamada BeginReceive para que um único byte seja lido e executar o restante da leitura como resultado do retorno de chamada recebido". O que você quer dizer com executar o restante da preparação como resultado do retorno de chamada recebido?
Mausimo 4/13

22

Você já obteve a maior parte da resposta através dos exemplos de código acima. Usar a operação de E / S assíncrona é absolutamente o caminho a percorrer aqui. Async IO é a maneira como o Win32 é projetado internamente para escalar. O melhor desempenho possível que você pode obter é alcançado usando portas de conclusão, vinculando seus soquetes às portas de conclusão e tendo um conjunto de encadeamentos aguardando a conclusão da porta de conclusão. O senso comum é ter 2-4 threads por CPU (núcleo) aguardando a conclusão. Eu recomendo revisar esses três artigos de Rick Vicik da equipe de Desempenho do Windows:

  1. Criando aplicativos para desempenho - parte 1
  2. Criando aplicativos para desempenho - parte 2
  3. Criando aplicativos para desempenho - parte 3

Os artigos mencionados abrangem principalmente a API nativa do Windows, mas são uma leitura obrigatória para quem tenta ter uma noção da escalabilidade e desempenho. Eles também têm alguns resumos do lado gerenciado.

A segunda coisa que você precisa fazer é verificar o livro Melhorando o Desempenho e Escalabilidade de Aplicativos .NET , disponível on-line. Você encontrará conselhos pertinentes e válidos sobre o uso de encadeamentos, chamadas assíncronas e bloqueios no Capítulo 5. Mas as verdadeiras jóias estão no Capítulo 17, onde você encontrará itens como orientações práticas para ajustar seu pool de encadeamentos. Meus aplicativos tiveram alguns problemas sérios até ajustar os maxIothreads / maxWorkerThreads conforme as recomendações deste capítulo.

Você diz que deseja criar um servidor TCP puro, então meu próximo ponto é falso. No entanto , se você se encontrar encurralado e usar a classe WebRequest e seus derivados, esteja avisado de que existe um dragão vigiando essa porta: o ServicePointManager . Esta é uma classe de configuração que tem um propósito na vida: arruinar seu desempenho. Certifique-se de liberar seu servidor do ServicePoint.ConnectionLimit imposto artificialmente ou seu aplicativo nunca será dimensionado (eu deixo você descobrir qual é o valor padrão ...). Você também pode reconsiderar a política padrão de enviar um cabeçalho Expect100Continue nas solicitações http.

Agora, sobre a API gerenciada por soquete principal, as coisas são bastante fáceis no lado de envio, mas são significativamente mais complexas no lado de recebimento. Para obter alta taxa de transferência e escala, você deve garantir que o soquete não seja controlado por fluxo, porque você não possui um buffer publicado para recebimento. Idealmente, para obter alto desempenho, você deve colocar à frente de 3 a 4 buffers e colocar novos buffers assim que receber um ( antes de processar o que recebeu) para garantir que o soquete sempre tenha um local para depositar os dados provenientes da rede. Você verá por que provavelmente não conseguirá isso em breve.

Depois de terminar de jogar com a API BeginRead / BeginWrite e iniciar o trabalho sério, você perceberá que precisa de segurança no seu tráfego, por exemplo. Autenticação NTLM / Kerberos e criptografia de tráfego, ou pelo menos proteção contra adulteração de tráfego. A maneira como você faz isso é usar o System.Net.Security.NegotiateStream (ou SslStream, se você precisar passar por domínios diferentes). Isso significa que, em vez de depender de operações assíncronas de soquete direto, você confiará nas operações assíncronas de AuthenticatedStream. Assim que você obtém um soquete (da conexão no cliente ou da aceitação no servidor), você cria um fluxo no soquete e o envia para autenticação, chamando BeginAuthenticateAsClient ou BeginAuthenticateAsServer. Depois que a autenticação for concluída (pelo menos, você estará seguro da loucura nativa InitiateSecurityContext / AcceptSecurityContext ...), você fará sua autorização verificando a propriedade RemoteIdentity do seu fluxo autenticado e fazendo a verificação da ACL que seu produto deve suportar. Depois disso, você enviará mensagens usando o BeginWrite e as receberá com o BeginRead. Esse é o problema que eu estava falando antes e que você não poderá postar vários buffers de recebimento, porque as classes AuthenticateStream não suportam isso. A operação BeginRead gerencia internamente todo o IO até que você receba um quadro inteiro; caso contrário, ele não pode manipular a autenticação da mensagem (descriptografar o quadro e validar a assinatura no quadro). Embora, na minha experiência, o trabalho realizado pelas classes AuthenticatedStream seja bastante bom e não deva ter nenhum problema. Ou seja. você poderá saturar a rede de GB com apenas 4-5% da CPU. As classes AuthenticatedStream também impõem a você as limitações de tamanho de quadro específico do protocolo (16k para SSL, 12k para Kerberos).

Isso deve começar no caminho certo. Não vou postar código aqui, há um exemplo perfeitamente bom no MSDN . Já fiz muitos projetos como esse e pude escalar para cerca de 1000 usuários conectados sem problemas. Acima disso, você precisará modificar as chaves do Registro para permitir ao kernel mais identificadores de soquete. e certifique-se de implantar em um sistema operacional de servidor , ou seja, W2K3, não XP ou Vista (ou seja, sistema operacional cliente), isso faz uma grande diferença.

BTW, verifique se você possui operações de bancos de dados no servidor ou no pedido de veiculação de arquivo. Você também usa o sabor assíncrono para eles ou drena o pool de threads rapidamente. Para conexões do SQL Server, adicione o 'Asyncronous Processing = true' à seqüência de conexão.


Há algumas ótimas informações aqui. Eu gostaria de poder premiar várias pessoas a recompensa. No entanto, eu te votei. Coisas boas aqui, obrigado.
214 Erik Funkenbusch

11

Eu tenho esse servidor em execução em algumas das minhas soluções. Aqui está uma explicação muito detalhada das diferentes maneiras de fazer isso no .net: Aproxime-se do fio com soquetes de alto desempenho no .NET

Ultimamente, tenho procurado maneiras de melhorar nosso código e analisarei isso: " Aprimoramentos de desempenho do soquete na versão 3.5 ", que foram incluídos especificamente "para uso em aplicativos que usam E / S de rede assíncrona para obter o melhor desempenho".

"O principal recurso desses aprimoramentos é evitar a alocação e sincronização repetidas de objetos durante E / S de soquete assíncrono de alto volume. O padrão de design Begin / End atualmente implementado pela classe Socket para E / S de soquete assíncrono requer um sistema. O objeto IAsyncResult seja alocado para cada operação de soquete assíncrona. "

Você pode continuar lendo se seguir o link. Pessoalmente, testarei seu código de amostra amanhã para compará-lo com o que tenho.

Edit: Aqui você pode encontrar o código de trabalho para o cliente e o servidor usando o novo 3.5 SocketAsyncEventArgs, para que você possa testá-lo em alguns minutos e percorrer o código. É uma abordagem simples, mas é a base para iniciar uma implementação muito maior. Também este artigo de quase dois anos atrás na MSDN Magazine foi uma leitura interessante.



9

Você já pensou em usar apenas uma ligação TCP da rede WCF e um padrão de publicação / assinatura? O WCF permitiria que você se concentrasse principalmente no seu domínio, em vez de no encanamento.

Existem muitas amostras do WCF e até uma estrutura de publicação / assinatura disponível na seção de download do IDesign que pode ser útil: http://www.idesign.net


8

Estou pensando em uma coisa:

Definitivamente, não quero iniciar um thread para cada conexão.

Por que é que? O Windows pode lidar com centenas de threads em um aplicativo desde pelo menos o Windows 2000. Eu já fiz isso, é realmente fácil trabalhar com eles se os threads não precisarem ser sincronizados. Especialmente considerando que você está realizando muitas E / S (para que você não seja vinculado à CPU e que muitos encadeamentos sejam bloqueados na comunicação de disco ou de rede), não entendo essa restrição.

Você já testou a maneira multiencadeada e achou que falta alguma coisa? Você pretende também ter uma conexão com o banco de dados para cada encadeamento (isso mataria o servidor do banco de dados, por isso é uma má idéia, mas é facilmente resolvido com um design de três camadas). Você está preocupado com o fato de ter milhares de clientes em vez de centenas e realmente ter problemas? (Embora eu tentasse mil threads ou até dez mil se tivesse mais de 32 GB de RAM - novamente, considerando que você não está vinculado à CPU, o tempo de troca de threads deve ser absolutamente irrelevante.)

Aqui está o código - para ver como isso funciona, acesse http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html e clique na imagem.

Classe 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 do servidor:

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

Classe 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 do 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();
    }
  }
}

O Windows pode lidar com muitos threads, mas o .NET não foi projetado para lidar com eles. Cada domínio de aplicativo .NET possui um pool de threads e você não deseja esgotar esse pool de threads. Não tenho certeza se você iniciar um Thread manualmente, se ele vem do pool de threads ou não. Ainda assim, centenas de threads que não fazem nada durante a maior parte do tempo são um enorme desperdício de recursos.
21411 Erik Funkenbusch

1
Acredito que você tenha uma visão incorreta dos threads. Os encadeamentos só vêm do conjunto de encadeamentos se você realmente quiser - os encadeamentos regulares não. Centenas de threads que não fazem nada desperdiçam exatamente nada :) (Bem, um pouco de memória, mas a memória é tão barata que não é mais um problema.) Vou escrever alguns aplicativos de exemplo para isso, publicarei um URL para assim que eu terminar. Enquanto isso, recomendo que você revise o que escrevi acima novamente e tente responder minhas perguntas.
Marcel Popescu

1
Embora eu concorde com o comentário de Marcel sobre a exibição de threads em que os threads criados não provêm do pool de threads, o restante da instrução não está correto. A memória não se refere à quantidade instalada em uma máquina, todos os aplicativos no Windows são executados no espaço de endereço virtual e em um sistema de 32 bits que fornece 2 GB de dados para seu aplicativo (não importa a quantidade de memória RAM instalada na caixa). Eles ainda devem ser gerenciados pelo tempo de execução. Fazer a E / S assíncrona não usa um encadeamento para aguardar (ele usa a IOCP que permite a E / S sobreposta) e é uma solução melhor e será dimensionada MUITO melhor.
22909 Brian ONeil

7
Ao executar muitos threads, não é a memória que é o problema, mas a CPU. A alternância de contexto entre os segmentos é uma operação relativamente cara e, quanto mais ativos você tiver, mais comutadores de contexto ocorrerão. Alguns anos atrás, eu fiz um teste no meu PC com um aplicativo de console C # e com aprox. 500 threads minha CPU era 100%, os threads não estavam fazendo nada de significativo. Para comunicações de rede, é melhor manter o número de threads baixo.
Sipwiz 22/05/09

1
Eu iria com uma solução de tarefas ou usaria async / waitit. A solução de tarefas parece mais simples, enquanto o assíncrono / espera é provavelmente mais escalável (eles foram feitos especificamente para situações ligadas a E / S).
Marcel Popescu

5

Usar o Async IO integrado do .NET ( BeginReadetc) é uma boa idéia, se você conseguir todos os detalhes corretamente. Quando você configura adequadamente os manipuladores de soquete / arquivo, ele usará a implementação IOCP subjacente do SO, permitindo que suas operações sejam concluídas sem o uso de threads (ou, na pior das hipóteses, usando um thread que eu acredito que seja proveniente do pool de threads de E / S do kernel. do pool de threads do .NET, que ajuda a aliviar o congestionamento do pool de threads.)

A principal dica é garantir que você abra seus soquetes / arquivos no modo sem bloqueio. A maioria das funções de conveniência padrão (como File.OpenRead) não faz isso, então você precisará escrever suas próprias.

Uma das outras preocupações principais é o tratamento de erros - o tratamento adequado de erros ao escrever código de E / S assíncrono é muito, muito mais difícil do que fazê-lo no código síncrono. Também é muito fácil acabar com as condições de corrida e os impasses, mesmo que você não esteja usando os threads diretamente, portanto, você precisa estar ciente disso.

Se possível, você deve tentar usar uma biblioteca de conveniência para facilitar o processo de execução de E / S assíncrona e escalável.

O tempo de execução de coordenação de concorrência da Microsoft é um exemplo de uma biblioteca .NET projetada para diminuir a dificuldade de executar esse tipo de programação. Parece ótimo, mas como eu não o usei, não posso comentar sobre o tamanho da escala.

Para meus projetos pessoais que precisam realizar E / S de rede ou disco assíncrona, eu uso um conjunto de ferramentas de simultaneidade / IO do .NET que eu criei no ano passado, chamado Squared.Task . É inspirado em bibliotecas como imvu.task e twisted , e incluí alguns exemplos de trabalho no repositório que fazem E / S de rede. Também o usei em alguns aplicativos que escrevi - o maior lançado publicamente sendo o NDexer (que o utiliza para E / S de disco sem rosca). A biblioteca foi escrita com base na minha experiência com o imvu.task e possui um conjunto de testes de unidade bastante abrangentes, por isso, recomendo fortemente que você experimente. Se você tiver algum problema, ficarei feliz em oferecer-lhe alguma ajuda.

Na minha opinião, com base na minha experiência usando E / S assíncronas / sem thread em vez de threads, é um esforço interessante na plataforma .NET, desde que você esteja pronto para lidar com a curva de aprendizado. Ele permite evitar os aborrecimentos de escalabilidade impostos pelo custo dos objetos Thread e, em muitos casos, você pode evitar completamente o uso de bloqueios e mutexes, fazendo uso cuidadoso de primitivas de simultaneidade como Futuros / Promessas.


Ótimas informações, vou verificar suas referências e ver o que faz sentido.
Erik Funkenbusch

3

Eu usei a solução de Kevin, mas ele diz que essa solução não possui código para remontagem de mensagens. Os desenvolvedores podem usar este código para remontagem de mensagens:

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

Você pode tentar usar uma estrutura chamada ACE (Adaptive Communications Environment), que é uma estrutura C ++ genérica para servidores de rede. É um produto maduro, muito sólido e foi projetado para suportar aplicativos de alta confiabilidade e grande volume até a classe de telecomunicações.

A estrutura lida com uma ampla variedade de modelos de simultaneidade e provavelmente possui um adequado para sua aplicação pronta para uso. Isso deve facilitar a depuração do sistema, pois a maioria dos problemas desagradáveis ​​de simultaneidade já foram resolvidos. A desvantagem aqui é que a estrutura é escrita em C ++ e não é a mais quente e fofa das bases de código. Por outro lado, você recebe uma infraestrutura de rede de nível industrial testada e uma arquitetura altamente escalável pronta para uso.


2
Essa é uma boa sugestão, mas a partir das etiquetas da pergunta que eu acredito que o OP estará usando C #
JPCosta

Eu percebi isso; a sugestão era que isso estivesse disponível para C ++ e não estou ciente de nada equivalente a C #. Depurar esse tipo de sistema não é fácil na melhor das hipóteses e você pode obter um retorno de ir para essa estrutura, mesmo que isso signifique mudar para C ++.
ConcernedOfTunbridgeWells

Sim, isso é c #. Estou procurando boas soluções baseadas em .net. Eu deveria ter sido mais claro, mas eu assumi as pessoas iriam ler as tags
Erik Funkenbusch


1

Bem, os soquetes .NET parecem fornecer select () - o melhor para lidar com entradas. Para saída, eu teria um pool de threads de gravador de soquete ouvindo em uma fila de trabalho, aceitando o descritor / objeto de soquete como parte do item de trabalho, para que você não precise de um encadeamento por soquete.


1

Eu usaria os métodos AcceptAsync / ConnectAsync / ReceiveAsync / SendAsync adicionados no .NET 3.5. Fiz uma referência e eles são aproximadamente 35% mais rápidos (tempo de resposta e taxa de bits) com 100 usuários constantemente enviando e recebendo dados.


1

para que as pessoas copiem colando a resposta aceita, é possível reescrever o método acceptCallback, removendo todas as chamadas de _serverSocket.BeginAccept (novo AsyncCallback (acceptCallback), _serverSocket); e coloque-o em uma cláusula finalmente {}, desta maneira:

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

você pode até remover a primeira captura, já que o conteúdo é o mesmo, mas é um método de modelo, e você deve usar a exceção digitada para lidar melhor com as exceções e entender o que causou o erro, então implemente essas capturas com algum código útil



-1

Para ser claro, estou procurando soluções baseadas em .net (c #, se possível, mas qualquer idioma .net funcionará)

Você não obterá o nível mais alto de escalabilidade se usar exclusivamente o .NET. Pausas no GC podem prejudicar a latência.

Vou precisar iniciar pelo menos um thread para o serviço. Estou pensando em usar a API assíncrona (BeginRecieve, etc.), pois não sei quantos clientes terei conectado a qualquer momento (possivelmente centenas). Definitivamente, não quero iniciar um thread para cada conexão.

A E / S sobreposta é geralmente considerada a API mais rápida do Windows para comunicação em rede. Não sei se é o mesmo que sua API assíncrona. Não use select, pois cada chamada precisa verificar todos os soquetes abertos em vez de ter retornos de chamada nos soquetes ativos.


1
Não entendo o seu comentário de pausa no GC. Nunca vi um sistema com problemas de escalabilidade diretamente relacionado ao GC.
1616

4
É muito mais provável que você crie um aplicativo que não possa ser dimensionado por causa da arquitetura ruim do que porque o GC existe. Grandes sistemas escaláveis ​​e de alto desempenho foram criados com .NET e Java. Nos dois links que você forneceu, a causa não foi diretamente a coleta de lixo ... mas relacionada à troca de heap. Eu suspeitaria que é realmente um problema com a arquitetura que poderia ter sido evitado. Se você puder me mostrar uma linguagem que não é possível construir um sistema que não pode ser dimensionado, eu o usarei com prazer;)
markt

1
Não concordo com este comentário. Desconhecidas, as perguntas que você faz referência são Java e estão lidando especificamente com alocações de memória maiores e tentando forçar manualmente o gc. Eu realmente não vou ter grandes quantidades de alocação de memória acontecendo aqui. Isso simplesmente não é um problema. Mas obrigada. Sim, o modelo de programação assíncrona geralmente é implementado sobre E / S sobrepostas.
Erik Funkenbusch

1
Na verdade, a melhor prática é não forçar manualmente manualmente o GC a coletar. Isso pode muito bem melhorar o desempenho do seu aplicativo. O .NET GC é um GC geracional que se ajustará ao uso do seu aplicativo. Se você realmente acha que precisa ligar manualmente para GC.Collect, eu diria que seu código provavelmente precisará ser escrito de outra maneira.
markt

1
@markt, esse é um comentário para pessoas que realmente não sabem nada sobre coleta de lixo. Se você tiver um tempo ocioso, não há nada errado em fazer uma coleta manual. Não vai piorar a sua aplicação quando terminar. Trabalhos acadêmicos mostram que GCs geracionais funcionam porque é uma aproximação da vida útil de seus objetos. Obviamente, essa não é uma representação perfeita. De fato, existe um paradoxo em que a geração "mais antiga" costuma ter a maior proporção de lixo, porque nunca é coletada.
Desconhecido

-1

Você pode usar a estrutura de código-fonte aberto do Push Framework para o desenvolvimento de servidores de alto desempenho. Ele é construído no IOCP e é adequado para cenários push e transmissão de mensagens.

http://www.pushframework.com


1
Esta postagem foi marcada com C # e .net. Por que você sugeriu uma estrutura C ++?
Erik Funkenbusch

Provavelmente porque ele escreveu. potatosoftware.com/…
quillbreaker

O pushframework suporta várias instâncias do servidor? caso contrário, como é dimensionado?
esskar 17/05
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.