it-swarm.com.de

So schreiben Sie einen skalierbaren Tcp / Ip-basierten Server

Ich bin in der Entwurfsphase des Schreibens einer neuen Windows-Dienstanwendung, die TCP/IP-Verbindungen für lange Verbindungen akzeptiert (dh dies ist nicht wie HTTP, wo es viele kurze Verbindungen gibt, sondern ein Client stellt eine Verbindung her und bleibt stunden- oder tagelang verbunden oder) sogar Wochen).

Ich bin auf der Suche nach Ideen, wie ich die Netzwerkarchitektur am besten gestalten kann. Ich muss mindestens einen Thread für den Dienst starten. Ich erwäge, die Asynch-API (BeginRecieve usw.) zu verwenden, da ich nicht weiß, wie viele Clients ich zu einem bestimmten Zeitpunkt verbunden habe (möglicherweise Hunderte). Ich möchte auf keinen Fall für jede Verbindung einen Thread starten.

Die Daten werden in erster Linie von meinem Server an die Clients weitergeleitet, es werden jedoch gelegentlich einige Befehle von den Clients gesendet. Dies ist in erster Linie eine Überwachungsanwendung, bei der mein Server regelmäßig Statusdaten an die Clients sendet.

Haben Sie Vorschläge, wie Sie dies am besten skalieren können? Grundlegender Workflow? Vielen Dank.

BEARBEITEN: Um klar zu sein, ich suche nach .net-basierten Lösungen (C #, wenn möglich, aber jede .net-Sprache wird funktionieren)

BOUNTY HINWEIS: Um die Prämie zu erhalten, erwarte ich mehr als eine einfache Antwort. Ich würde ein funktionierendes Beispiel für eine Lösung benötigen, entweder als Hinweis auf etwas, das ich herunterladen könnte, oder als kurzes Beispiel in der Zeile. Und es muss .net und Windows-basiert sein (jede .net-Sprache ist akzeptabel)

EDIT: Ich möchte mich bei allen bedanken, die gute Antworten gegeben haben. Leider konnte ich nur eine akzeptieren, und ich entschied mich für die bekanntere Begin/End-Methode. Esacs Lösung ist zwar besser, aber immer noch neu genug, dass ich nicht genau weiß, wie es funktionieren wird.

Ich habe alle Antworten, die ich für gut hielt, positiv bewertet. Ich wünschte, ich könnte mehr für euch tun. Danke noch einmal.

145

Ich habe in der Vergangenheit etwas Ähnliches geschrieben. Meine Untersuchungen vor Jahren haben gezeigt, dass das Schreiben einer eigenen Socket-Implementierung mit den asynchronen Sockets die beste Wahl ist. Dies bedeutete, dass Kunden, die eigentlich nichts taten, relativ wenig Ressourcen benötigten. Alles, was passiert, wird vom .net-Thread-Pool behandelt.

Ich habe es als eine Klasse geschrieben, die alle Verbindungen für die Server verwaltet.

Ich habe einfach eine Liste verwendet, um alle Client-Verbindungen zu speichern, aber wenn Sie schnellere Suchvorgänge für größere Listen benötigen, können Sie sie schreiben, wie Sie möchten.

private List<xConnection> _sockets;

Außerdem muss der Socket für eingehende Verbindungen tatsächlich empfangsbereit sein.

private System.Net.Sockets.Socket _serverSocket;

Die Startmethode startet tatsächlich den Server-Socket und beginnt, auf eingehende Verbindungen zu warten.

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

Ich möchte nur darauf hinweisen, dass der Code für die Ausnahmebehandlung schlecht aussieht, aber der Grund dafür ist, dass darin Ausnahmebehandlungscode enthalten war, damit alle Ausnahmen unterdrückt werden und false zurückgegeben werden, wenn eine Konfigurationsoption festgelegt wurde, aber Ich wollte es der Kürze halber entfernen.

Das _serverSocket.BeginAccept (neues AsyncCallback (acceptCallback)), _serverSocket) oben legt im Wesentlichen fest, dass unser Server-Socket die acceptCallback-Methode aufruft, wenn ein Benutzer eine Verbindung herstellt. Diese Methode wird über den .NET-Threadpool ausgeführt, der das automatische Erstellen zusätzlicher Arbeitsthreads verarbeitet, wenn Sie über viele Blockierungsvorgänge verfügen. Dies sollte die Belastung des Servers optimal bewältigen.

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

Der obige Code hat im Wesentlichen gerade das Akzeptieren der eingehenden Verbindung beendet. Er wartet auf BeginReceive, einen Rückruf, der ausgeführt wird, wenn der Client Daten sendet, und wartet dann auf den nächsten acceptCallback, der den nächsten Client akzeptiert Verbindung, die hereinkommt.

Der Methodenaufruf BeginReceive teilt dem Socket mit, was zu tun ist, wenn Daten vom Client empfangen werden. Für BeginReceive müssen Sie ein Byte-Array angeben, in das die Daten kopiert werden, wenn der Client Daten sendet. Die ReceiveCallback Methode wird aufgerufen, wie wir mit dem Empfang von Daten umgehen.

private void ReceiveCallback(IAsyncResult result)
{
  //get our connection from the callback
  xConnection conn = (xConnection)result.AsyncState;
  //catch any errors, we'd better not have any
  try
  {
    //Grab our buffer and count the number of bytes receives
    int bytesRead = conn.socket.EndReceive(result);
    //make sure we've read something, if we haven't it supposadly means that the client disconnected
    if (bytesRead > 0)
    {
      //put whatever you want to do when you receive data here

      //Queue the next receive
      conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
     }
     else
     {
       //Callback run but no data, close the connection
       //supposadly means a disconnect
       //and we still have to close the socket, even though we throw the event later
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
   catch (SocketException e)
   {
     //Something went terribly wrong
     //which shouldn't have happened
     if (conn.socket != null)
     {
       conn.socket.Close();
       lock (_sockets)
       {
         _sockets.Remove(conn);
       }
     }
   }
 }

EDIT: In diesem Muster habe ich vergessen zu erwähnen, dass in diesem Bereich des Codes:

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

Was ich im Allgemeinen tun würde, ist in dem Code, was auch immer Sie wollen, ist das Zusammensetzen von Paketen in Nachrichten und erstellen Sie sie dann als Jobs im Thread-Pool. Auf diese Weise wird das BeginReceive des nächsten Blocks vom Client nicht verzögert, während der Nachrichtenverarbeitungscode ausgeführt wird.

Der Accept Callback beendet das Lesen des Daten-Sockets durch Aufrufen von End Receive. Dies füllt den Puffer, der in der Startempfangsfunktion vorgesehen ist. Sobald Sie tun, was Sie wollen, wo ich den Kommentar hinterlassen habe, rufen wir die nächste BeginReceive -Methode auf, die den Rückruf erneut ausführt, wenn der Client weitere Daten sendet. Hier ist der wirklich knifflige Teil: Wenn der Client Daten sendet, wird Ihr Rückruf möglicherweise nur mit einem Teil der Nachricht aufgerufen. Der Zusammenbau kann sehr, sehr kompliziert werden. Ich benutzte meine eigene Methode und erstellte dazu eine Art proprietäres Protokoll. Ich habe es ausgelassen, aber wenn Sie es wünschen, kann ich es hinzufügen. Dieser Handler war tatsächlich das komplizierteste Stück Code, das ich je geschrieben habe.

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

Die obige Sendemethode verwendet tatsächlich einen synchronen Send -Aufruf, der für mich aufgrund der Nachrichtengrößen und der Multithread-Natur meiner Anwendung in Ordnung war. Wenn Sie an jeden Client senden möchten, müssen Sie nur die _sockets-Liste durchlaufen.

Die xConnection-Klasse, auf die oben verwiesen wird, ist im Grunde genommen ein einfacher Wrapper für einen Socket, der den Bytepuffer enthält, und in meiner Implementierung einige Extras.

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

Als Referenz dienen hier auch die usings, die ich einbeziehe, da ich mich immer ärgere, wenn sie nicht enthalten sind.

using System.Net.Sockets;

Ich hoffe das ist hilfreich, es ist vielleicht nicht der sauberste Code, aber es funktioniert. Es gibt auch einige Nuancen im Code, die Sie beim Ändern müde machen sollten. Zum einen kann immer nur ein einziger BeginAccept aufgerufen werden. Früher gab es einen sehr ärgerlichen .net-Bug, der vor Jahren aufgetreten ist, daher kann ich mich nicht an die Details erinnern.

Außerdem verarbeiten wir im Code ReceiveCallback alles, was vom Socket empfangen wird, bevor wir den nächsten Empfang in die Warteschlange stellen. Dies bedeutet, dass wir für einen einzelnen Socket zu einem bestimmten Zeitpunkt immer nur einmal in ReceiveCallback sind und keine Thread-Synchronisation verwenden müssen. Wenn Sie dies jedoch neu anordnen, um den nächsten Empfang unmittelbar nach dem Abrufen der Daten aufzurufen, was möglicherweise etwas schneller ist, müssen Sie sicherstellen, dass Sie die Threads ordnungsgemäß synchronisieren.

Außerdem habe ich einen Großteil meines Codes gehackt, aber die Essenz dessen, was passiert, an Ort und Stelle belassen. Dies sollte ein guter Anfang für Ihr Design sein. Hinterlasse einen Kommentar, wenn du weitere Fragen dazu hast.

91
Kevin Nisbet

Es gibt viele Möglichkeiten, Netzwerkvorgänge in C # auszuführen. Alle von ihnen verwenden unterschiedliche Mechanismen unter der Haube und leiden daher unter schwerwiegenden Leistungsproblemen mit einer hohen Nebenläufigkeit. Start * -Operationen sind eine davon, die von vielen Menschen häufig als die schnellste und schnellste Art der Vernetzung angesehen wird.

Um diese Probleme zu lösen, führten sie die * Async-Methoden ein: Von MSDN http://msdn.Microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

Die SocketAsyncEventArgs-Klasse ist Teil einer Reihe von Erweiterungen der System.Net.Sockets .. ::. Socket-Klasse, die ein alternatives asynchrones Muster bereitstellen, das von speziellen Hochleistungs-Socket-Anwendungen verwendet werden kann . Diese Klasse wurde speziell für Netzwerkserveranwendungen entwickelt, die eine hohe Leistung erfordern. Eine Anwendung kann das erweiterte asynchrone Muster ausschließlich oder nur in bestimmten Hotspots verwenden (z. B. beim Empfang großer Datenmengen).

Das Hauptmerkmal dieser Verbesserungen ist die Vermeidung der wiederholten Zuweisung und Synchronisierung von Objekten während der asynchronen Socket-E/A mit hohem Volumen. Das Begin/End-Entwurfsmuster, das derzeit von der System.Net.Sockets .. ::. Socket-Klasse implementiert wird, erfordert, dass jedem asynchronen Socket-Vorgang ein System .. ::. IAsyncResult-Objekt zugewiesen wird.

Im Hintergrund verwendet die * Async-API IO Completion-Ports, die die schnellste Methode zur Durchführung von Netzwerkoperationen darstellen (siehe http://msdn.Microsoft.com/en-us/). magazine/cc302334.aspx

Und um Ihnen zu helfen, füge ich den Quellcode für einen Telnet-Server hinzu, den ich mit der * Async-API geschrieben habe. Ich beziehe nur die relevanten Teile ein. Anstatt die Daten inline zu verarbeiten, entscheide ich mich stattdessen dafür, sie in eine sperrfreie (wartefreie) Warteschlange zu verschieben, die in einem separaten Thread verarbeitet wird. Beachten Sie, dass ich nicht die entsprechende Pool-Klasse einbeziehe, die nur ein einfacher Pool ist, der ein neues Objekt erstellt, wenn es leer ist, und die Buffer-Klasse, die nur ein sich selbst erweiternder Puffer ist, der nur dann wirklich benötigt wird, wenn Sie einen unbestimmten Wert erhalten Datenmenge. Wenn Sie weitere Informationen wünschen, senden Sie mir bitte eine PN.

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

Früher gab es eine wirklich gute Diskussion über skalierbares TCP/IP mit .NET von Chris Mullins von Coversant. Leider scheint sein Blog von seinem früheren Standort verschwunden zu sein, daher werde ich versuchen, seine Ratschläge aus dem Gedächtnis zusammenzufassen (einige nützliche Kommentare) von ihm erscheinen in diesem Thread: C++ vs. C #: Entwicklung eines hoch skalierbaren IOCP-Servers )

Beachten Sie in erster Linie, dass beide mit Begin/End und die Async -Methoden der Socket -Klasse verwenden IO Completion Ports (IOCP), um Skalierbarkeit zu gewährleisten Richtig angewendet (siehe unten), um die Skalierbarkeit zu verbessern, als mit welcher der beiden Methoden Sie Ihre Lösung tatsächlich implementieren.

Chris Mullins 'Posts basierten auf der Verwendung von Begin/End, mit der ich persönlich Erfahrung habe. Beachten Sie, dass Chris auf dieser Basis eine Lösung entwickelt hat, die auf einem 32-Bit-Computer mit 2 GB Arbeitsspeicher bis zu 10.000 gleichzeitige Clientverbindungen und auf einer 64-Bit-Plattform mit ausreichend Arbeitsspeicher bis zu 100.000 skaliert. Aus meiner eigenen Erfahrung mit dieser Technik (auch wenn diese Art von Ladung noch lange nicht in der Nähe ist) habe ich keinen Grund, an diesen Richtwerten zu zweifeln.

IOCP versus Thread-per-Connection- oder 'Select'-Primitive

Der Grund, warum Sie einen Mechanismus verwenden möchten, der IOCP unter der Haube verwendet, besteht darin, dass ein sehr untergeordneter Windows-Threadpool verwendet wird, der keine Threads aktiviert, bis tatsächliche Daten auf IO vorliegen Kanal, von dem Sie versuchen zu lesen (beachten Sie, dass IOCP auch für die Datei IO) verwendet werden kann. Der Vorteil davon ist, dass Windows nicht zu einem Thread wechseln muss, nur um diesen zu finden Es sind ohnehin noch keine Daten vorhanden. Dadurch wird die Anzahl der vom Server durchzuführenden Kontextwechsel auf das erforderliche Minimum reduziert.

Durch Kontextwechsel wird der "Thread-pro-Verbindung" -Mechanismus definitiv ausgeschaltet, obwohl dies eine praktikable Lösung ist, wenn Sie nur mit ein paar Dutzend Verbindungen zu tun haben. Dieser Mechanismus ist jedoch in keiner Weise "skalierbar".

Wichtige Überlegungen bei der Verwendung von IOCP

Speicher

In erster Linie ist es wichtig zu verstehen, dass IOCP unter .NET leicht zu Speicherproblemen führen kann, wenn Ihre Implementierung zu naiv ist. Jeder IOCP BeginReceive -Aufruf wird dazu führen, dass der Puffer, in den Sie lesen, "angeheftet" wird. Eine gute Erklärung, warum dies ein Problem ist, finden Sie unter: Yun Jins Weblog: OutOfMemoryException and Pinning .

Glücklicherweise kann dieses Problem vermieden werden, aber es erfordert einen Kompromiss. Die vorgeschlagene Lösung besteht darin, ein großes byte[] Puffer beim Start der Anwendung (oder in der Nähe davon) von mindestens 90 KB (ab .NET 2 kann die erforderliche Größe in späteren Versionen größer sein). Der Grund dafür ist, dass große Speicherzuordnungen automatisch in einem nicht komprimierenden Speichersegment (The Large Object Heap) enden, das effektiv automatisch fixiert wird. Durch Zuweisen eines großen Puffers beim Start stellen Sie sicher, dass sich dieser unbewegliche Speicherblock an einer relativ "niedrigen Adresse" befindet, an der er nicht im Weg ist und eine Fragmentierung verursacht.

Sie können dann Offsets verwenden, um diesen einen großen Puffer für jede Verbindung, die Daten lesen muss, in separate Bereiche zu unterteilen. Hier kommt ein Kompromiss ins Spiel; Da dieser Puffer vorab zugewiesen werden muss, müssen Sie entscheiden, wie viel Pufferplatz Sie pro Verbindung benötigen und welche Obergrenze Sie für die Anzahl der Verbindungen festlegen möchten, auf die Sie skalieren möchten (oder Sie können eine Abstraktion implementieren) das kann zusätzliche festgelegte Puffer zuordnen, sobald Sie sie benötigen).

Die einfachste Lösung wäre, jeder Verbindung ein einzelnes Byte mit einem eindeutigen Versatz innerhalb dieses Puffers zuzuweisen. Dann können Sie einen BeginReceive -Aufruf für ein einziges zu lesendes Byte ausführen und den Rest des Lesevorgangs als Ergebnis des Rückrufs ausführen, den Sie erhalten.

Verarbeitung

Wenn Sie den Rückruf von dem von Ihnen getätigten Begin -Aufruf erhalten, ist es sehr wichtig zu wissen, dass der Code im Rückruf auf dem IOCP-Thread auf niedriger Ebene ausgeführt wird. Es ist absolut wichtig , dass Sie langwierige Operationen in diesem Rückruf vermeiden. Wenn Sie diese Threads für eine komplexe Verarbeitung verwenden, wird Ihre Skalierbarkeit genauso beeinträchtigt wie bei der Verwendung von "Thread pro Verbindung".

Die vorgeschlagene Lösung besteht darin, den Rückruf nur dazu zu verwenden, ein Arbeitselement in die Warteschlange zu stellen, um die eingehenden Daten zu verarbeiten, die auf einem anderen Thread ausgeführt werden. Vermeiden Sie potenziell blockierende Vorgänge innerhalb des Rückrufs, damit der IOCP-Thread so schnell wie möglich in seinen Pool zurückkehren kann. In .NET 4.0 würde ich vorschlagen, dass die einfachste Lösung darin besteht, ein Task zu erzeugen, das einen Verweis auf den Client-Socket und eine Kopie des ersten Bytes enthält, das bereits vom Aufruf BeginReceive gelesen wurde . Diese Task ist dann dafür verantwortlich, alle Daten aus dem Socket zu lesen, die die von Ihnen verarbeitete Anforderung darstellen, sie auszuführen und anschließend einen neuen BeginReceive -Aufruf zu tätigen, um den Socket erneut für IOCP in die Warteschlange zu stellen. Vor .NET 4.0 können Sie den ThreadPool verwenden oder eine eigene Implementierung der Thread-Arbeitswarteschlange erstellen.

Zusammenfassung

Grundsätzlich würde ich vorschlagen, Kevins Beispielcode für diese Lösung mit den folgenden zusätzlichen Warnungen zu verwenden:

  • Stellen Sie sicher, dass der Puffer, den Sie an BeginReceive übergeben, bereits "angeheftet" ist.
  • Stellen Sie sicher, dass der Rückruf, den Sie an BeginReceive übergeben, nur eine Aufgabe in die Warteschlange stellt, um die eigentliche Verarbeitung der eingehenden Daten zu erledigen

Wenn Sie das tun, haben Sie zweifellos die Möglichkeit, die Ergebnisse von Chris zu reproduzieren und potenziell Hunderttausende von Clients gleichzeitig zu skalieren (vorausgesetzt, die richtige Hardware und eine effiziente Implementierung Ihres eigenen Verarbeitungscodes sind selbstverständlich).

46
jerryjvl

Den größten Teil der Antwort haben Sie bereits über die obigen Codebeispiele erhalten. Die Verwendung des asynchronen IO - Vorgangs ist hier absolut der richtige Weg. Async IO ist die Art und Weise, wie Win32 intern für die Skalierung ausgelegt ist. Die bestmögliche Leistung, die Sie erzielen können Dies wird durch die Verwendung von Completion-Ports erreicht, indem Sie Ihre Sockets an Completion-Ports binden und einen Thread-Pool auf die Fertigstellung des Completion-Ports warten lassen. Es wird empfohlen, 2-4 Threads pro CPU (Core) auf die Fertigstellung zu warten Artikel von Rick Vicik aus dem Windows Performance Team:

  1. Entwerfen von Anwendungen für die Leistung - Teil 1
  2. Entwerfen von Anwendungen für die Leistung - Teil 2
  3. Entwerfen von Anwendungen für die Leistung - Teil

Die genannten Artikel befassen sich hauptsächlich mit der nativen Windows-API, sind jedoch ein Muss für alle, die sich einen Überblick über Skalierbarkeit und Leistung verschaffen möchten. Sie haben auch ein paar Informationen über die verwaltete Seite der Dinge.

Als zweites müssen Sie sicherstellen, dass Sie das Buch Verbessern der Leistung und Skalierbarkeit von .NET-Anwendungen lesen, das online verfügbar ist. In Kapitel 5 finden Sie sachdienliche und gültige Ratschläge zur Verwendung von Threads, asynchronen Aufrufen und Sperren. Die wahren Juwelen finden Sie in Kapitel 17, in dem Sie nützliche Hinweise zum Optimieren Ihres Thread-Pools finden. Meine Apps hatten einige schwerwiegende Probleme, bis ich die maxIothreads/maxWorkerThreads gemäß den Empfehlungen in diesem Kapitel angepasst habe.

Sie sagen, Sie möchten einen reinen TCP) - Server ausführen, daher ist mein nächster Punkt falsch. Allerdings, wenn Sie in die Enge getrieben sind und die WebRequest-Klasse und verwenden Seien Sie gewarnt, dass es einen Drachen gibt, der diese Tür bewacht: den ServicePointManager . Dies ist eine Konfigurationsklasse, die einen Lebenszweck hat: Ihre Leistung zu ruinieren. Stellen Sie sicher, dass Sie Ihren Server von dem künstlich auferlegten ServicePoint.ConnectionLimit befreien. Andernfalls wird Ihre Anwendung niemals skaliert (ich lasse Sie selbst feststellen, was der Standardwert ist ...). Sie können auch die Standardrichtlinie zum Senden eines Expect100Continue-Headers in den http-Anforderungen überdenken.

In Bezug auf die Core-Socket-verwaltete API sind die Dinge auf der Sendeseite recht einfach, auf der Empfängerseite jedoch wesentlich komplexer. Um einen hohen Durchsatz und eine hohe Skalierung zu erzielen, müssen Sie sicherstellen, dass der Socket nicht durchflussgesteuert ist, da für den Empfang kein Puffer bereitgestellt ist. Idealerweise sollten Sie für eine hohe Leistung 3-4 Puffer vorausschicken und neue Puffer einschicken, sobald Sie einen zurückbekommen (vor Sie verarbeiten den zurückbekommenen), damit Sie sicherstellen, dass der Sockel immer einen Platz zum Einlagern hat die Daten aus dem Netzwerk. Sie werden sehen, warum Sie dies wahrscheinlich nicht in Kürze erreichen werden.

Wenn Sie mit der BeginRead/BeginWrite-API fertig sind und mit der ernsthaften Arbeit beginnen, werden Sie feststellen, dass Sie Sicherheit für Ihren Datenverkehr benötigen, d. H. NTLM/Kerberos-Authentifizierung und Datenverkehrsverschlüsselung oder zumindest Schutz vor Datenverkehrsmanipulation. Verwenden Sie dazu den integrierten System.Net.Security.NegotiateStream (oder SslStream, wenn Sie unterschiedliche Domänen verwenden müssen). Dies bedeutet, dass Sie sich nicht auf asynchrone Straight-Socket-Vorgänge verlassen, sondern auf asynchrone AuthenticatedStream-Vorgänge. Sobald Sie einen Socket erhalten (entweder von Connect auf dem Client oder von Accept auf dem Server), erstellen Sie einen Stream auf dem Socket und senden ihn zur Authentifizierung, indem Sie entweder BeginAuthenticateAsClient oder BeginAuthenticateAsServer aufrufen. Nachdem die Authentifizierung abgeschlossen ist (zumindest Ihre Sicherheit vor dem systemeigenen Verrücktsein von InitiateSecurityContext/AcceptSecurityContext ...), führen Sie Ihre Autorisierung durch, indem Sie die RemoteIdentity-Eigenschaft Ihres authentifizierten Streams überprüfen und die ACL-Überprüfung durchführen, die Ihr Produkt unterstützen muss. Danach senden Sie Nachrichten mit BeginWrite und erhalten sie mit BeginRead. Dies ist das Problem, von dem ich zuvor gesprochen habe, dass Sie nicht mehrere Empfangspuffer bereitstellen können, da die AuthenticateStream-Klassen dies nicht unterstützen. Die BeginRead-Operation verwaltet intern alle IO bis Sie einen gesamten Frame erhalten haben, andernfalls könnte sie die Nachrichtenauthentifizierung nicht verarbeiten (Frame entschlüsseln und Signatur auf Frame validieren) Die AuthenticatedStream-Klassen sind recht gut und sollten kein Problem damit haben, dh Sie sollten in der Lage sein, das GB-Netzwerk mit nur 4-5% CPU zu überlasten. Die AuthenticatedStream-Klassen werden Ihnen auch die protokollspezifischen Einschränkungen der Frame-Größe auferlegen (16 KB für SSL, 12 KB für Kerberos).

Damit sollten Sie auf dem richtigen Weg sein. Ich werde hier keine Postleitzahl veröffentlichen, es gibt ein perfektes Beispiel für MSDN . Ich habe viele Projekte wie dieses durchgeführt und konnte problemlos auf ca. 1000 verbundene Benutzer skalieren. Darüber hinaus müssen Sie Registrierungsschlüssel ändern, um dem Kernel mehr Socket-Handles zu ermöglichen. und stellen Sie sicher, dass Sie auf einem Server Betriebssystem bereitstellen, das heißt W2K3 nicht XP oder Vista (dh Client-Betriebssystem)), es macht einen großen Unterschied.

Übrigens: Stellen Sie sicher, dass auf dem Server oder in der Datei Datenbankvorgänge ausgeführt werden IO), und verwenden Sie auch die asynchrone Variante für diese Vorgänge Sie fügen der Verbindungszeichenfolge "Asyncronous Processing = true" hinzu.

22
Remus Rusanu

In einigen meiner Lösungen läuft ein solcher Server. Hier ist eine sehr detaillierte Erklärung der verschiedenen Möglichkeiten, dies in .net zu tun: Komm näher an den Draht mit Hochleistungs-Sockets in .NET

In letzter Zeit habe ich nach Möglichkeiten gesucht, unseren Code zu verbessern, und ich werde Folgendes untersuchen: " Socket Performance Enhancements in Version 3.5 " Das wurde speziell für Anwendungen aufgenommen, die asynchrone Netzwerk-E/A verwenden um die höchste Leistung zu erzielen ".

Das Hauptmerkmal dieser Verbesserungen ist die Vermeidung der wiederholten Zuweisung und Synchronisierung von Objekten während der asynchronen Socket-E/A mit hohem Volumen. Das von der Socket-Klasse für asynchrone Socket-E/A derzeit implementierte Entwurfsmuster für Anfang/Ende erfordert ein System. IAsyncResult-Objekt für jede asynchrone Socket-Operation zugewiesen werden. "

Sie können weiterlesen, wenn Sie dem Link folgen. Ich persönlich werde morgen ihren Beispielcode testen, um ihn mit dem zu vergleichen, was ich habe.

Bearbeiten: Hier Sie können Arbeitscode für Client und Server mit den neuen 3.5 SocketAsyncEventArgs finden, um ihn in einem zu testen ein paar Minuten und gehen Sie durch den Code. Es ist ein einfacher Ansatz, aber die Basis für den Start einer viel umfangreicheren Implementierung. Auch der this Artikel aus dem MSDN Magazine vor fast zwei Jahren war eine interessante Lektüre.

11
jvanderh

Haben Sie schon einmal darüber nachgedacht, ein WCF-Netz TCP=) und ein Publish/Subscribe-Muster zu verwenden? Mit WCF können Sie sich [hauptsächlich] auf Ihre Domain konzentrieren, anstatt sich auf die Installation zu konzentrieren.

Es gibt viele WCF-Beispiele und sogar ein Publish/Subscribe-Framework im Download-Bereich von IDesign, das nützlich sein kann: http://www.idesign.net

9
markt

Ich frage mich über eine Sache:

Ich möchte auf keinen Fall für jede Verbindung einen Thread starten.

Warum das? Windows konnte seit mindestens Windows 2000 Hunderte von Threads in einer Anwendung verarbeiten. Ich habe es geschafft, es ist wirklich einfach, damit zu arbeiten, wenn die Threads nicht synchronisiert werden müssen. Ich verstehe diese Einschränkung nicht, insbesondere, da Sie viel E/A ausführen (also nicht CPU-gebunden sind und viele Threads entweder auf der Festplatte oder in der Netzwerkkommunikation blockiert sind).

Haben Sie den Multithread-Weg getestet und festgestellt, dass ihm etwas fehlt? Beabsichtigen Sie, für jeden Thread auch eine Datenbankverbindung herzustellen (dies würde den Datenbankserver zum Erliegen bringen, ist also eine schlechte Idee, lässt sich jedoch mit einem dreistufigen Design problemlos lösen)? Haben Sie Angst, dass Sie Tausende von Kunden anstelle von Hunderten haben und dann wirklich Probleme? (Obwohl ich tausend oder sogar zehntausend Threads ausprobieren würde, wenn ich 32+ GB RAM - hätte, sollte die Threadwechselzeit absolut irrelevant sein, da Sie nicht an die CPU gebunden sind. )

Hier ist der Code - um zu sehen, wie dies aussieht, gehen Sie zu http://mdpopescu.blogspot.com/2009/05/multi-threaded-server.html und klicken Sie auf das Bild.

Serverklasse:

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

Server Hauptprogramm:

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

Kundenklasse:

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

Client-Hauptprogramm:

using System;
using System.Threading;

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

    private static void RunClient()
    {
      new Client();
    }
  }
}
8
Marcel Popescu

Die Verwendung von .NETs integriertem Async IO (BeginRead, etc) ist eine gute Idee, wenn Sie alle Details richtig machen können. Wenn Sie Ihre Socket-/Datei-Handles richtig einrichten, wird es Verwenden Sie die zugrunde liegende IOCP-Implementierung des Betriebssystems, damit Ihre Vorgänge ohne Verwendung von Threads abgeschlossen werden können (oder im schlimmsten Fall mit einem Thread, von dem ich glaube, dass er aus dem IO Thread-Pool des Kernels stammt, anstelle von .NETs) Thread-Pool, der die Überlastung des Thread-Pools verringert.)

Das Hauptproblem ist, sicherzustellen, dass Sie Ihre Sockets/Dateien im nicht blockierenden Modus öffnen. Die meisten Standard-Komfortfunktionen (wie File.OpenRead) Mach das nicht, also musst du deine eigene schreiben.

Eines der anderen Hauptprobleme ist die Fehlerbehandlung - die ordnungsgemäße Behandlung von Fehlern beim Schreiben von asynchronem E/A-Code ist sehr viel schwieriger als bei synchronem Code. Es ist auch sehr einfach, Race-Bedingungen und Deadlocks zu erleiden, auch wenn Sie Threads möglicherweise nicht direkt verwenden. Daher müssen Sie sich dessen bewusst sein.

Wenn möglich, sollten Sie versuchen, eine praktische Bibliothek zu verwenden, um den Prozess der skalierbaren asynchronen E/A zu vereinfachen.

Microsofts Concurrency Coordination Runtime ist ein Beispiel für eine .NET-Bibliothek, die entwickelt wurde, um diese Art der Programmierung zu vereinfachen. Es sieht gut aus, aber da ich es nicht benutzt habe, kann ich nicht beurteilen, wie gut es skalieren würde.

Für meine persönlichen Projekte, die asynchrone Netzwerk- oder Datenträger-E/A-Vorgänge ausführen müssen, verwende ich eine Reihe von .NET-Tools für parallele Zugriffe/Eingaben, die ich im letzten Jahr erstellt habe. Sie heißen Squared.Task . Es ist inspiriert von Bibliotheken wie imvu.task und twisted , und ich habe einige Arbeitsbeispiele in das Repository aufgenommen, die Netzwerk-E/A durchführen. Ich habe es auch in einigen Anwendungen verwendet, die ich geschrieben habe - die größte öffentlich veröffentlichte ist NDexer (die es für threadlose Datenträger-E/A verwendet). Die Bibliothek wurde auf der Grundlage meiner Erfahrungen mit imvu.task geschrieben und enthält eine Reihe ziemlich umfassender Komponententests. Ich empfehle Ihnen daher dringend, sie auszuprobieren. Wenn Sie irgendwelche Probleme damit haben, würde ich Sie gerne unterstützen.

Meiner Meinung nach ist die Verwendung von asynchronen/threadlosen IO anstelle von Threads ein lohnendes Unterfangen auf der .NET-Plattform, solange Sie bereit sind, sich mit der Lernkurve auseinanderzusetzen Ermöglicht es Ihnen, die durch die Kosten von Thread-Objekten verursachten Probleme mit der Skalierbarkeit zu vermeiden, und in vielen Fällen können Sie die Verwendung von Sperren und Mutexen vollständig vermeiden, indem Sie Parallelitätsprimitive wie Futures/Promises vorsichtig verwenden.

5
Katelyn Gadd

Ich habe Kevins Lösung verwendet, aber er sagt, dass es der Lösung an Code fehlt, um Nachrichten wieder zusammenzusetzen. Entwickler können diesen Code zum erneuten Zusammenstellen von Nachrichten verwenden:

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

}
2
Ahmet Arslan

Eine gute Übersicht über die Techniken finden Sie auf der Seite C10k-Problem .

2
zvrba

Ich würde die AcceptAsync/ConnectAsync/ReceiveAsync/SendAsync-Methoden verwenden, die in .NET 3.5 hinzugefügt wurden. Ich habe einen Benchmark durchgeführt und sie sind ca. 35% schneller (Antwortzeit und Bitrate), da 100 Benutzer ständig Daten senden und empfangen.

1
user110813

Sie können versuchen, ein Framework namens ACE (Adaptive Communications Environment) zu verwenden, das ein generisches C++ - Framework für Netzwerkserver ist. Es ist ein sehr solides, ausgereiftes Produkt, das hochzuverlässige Anwendungen mit hohen Stückzahlen bis zur Telekommunikationsqualität unterstützt.

Das Framework behandelt eine ganze Reihe von Parallelitätsmodellen und hat wahrscheinlich eines, das für Ihre Anwendung sofort geeignet ist. Dies sollte das Debuggen des Systems vereinfachen, da die meisten Probleme im Zusammenhang mit der Parallelität bereits behoben wurden. Der Nachteil hierbei ist, dass das Framework in C++ geschrieben ist und nicht die warmeste und flockigste Codebasis ist. Auf der anderen Seite erhalten Sie eine getestete, industrietaugliche Netzwerkinfrastruktur und eine hochgradig skalierbare Architektur, die sofort einsatzbereit ist.

Ich würde SEDA oder eine Lightweight-Threading-Bibliothek verwenden (erlang oder neueres Linux siehe NTPL-Skalierbarkeit auf der Serverseite ). Async-Codierung ist sehr umständlich, wenn Ihre Kommunikation nicht :)

1

Nun, .NET-Sockets bieten anscheinend select () - das ist am besten für die Verarbeitung von Eingaben geeignet. Für die Ausgabe würde ein Pool von Socket-Writer-Threads eine Arbeitswarteschlange abhören und Socket-Deskriptor/-Objekt als Teil des Arbeitselements akzeptieren, sodass Sie keinen Thread pro Socket benötigen.

1

um die akzeptierte Antwort einzufügen, können Sie die acceptCallback-Methode umschreiben und alle Aufrufe von _serverSocket.BeginAccept (neues AsyncCallback (acceptCallback), _serverSocket) entfernen. und setze es in eine finally {} -Klausel:

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

sie können sogar den ersten Fang entfernen, da sein Inhalt derselbe ist, es sich jedoch um eine Vorlagenmethode handelt. Verwenden Sie eine typisierte Ausnahme, um die Ausnahmen besser zu behandeln und zu verstehen, was den Fehler verursacht hat. Implementieren Sie diese Fänge also einfach mit nützlichem Code

1
Fabio Angela

Ich würde empfehlen, diese Bücher über ACE zu lesen

um Ideen über Muster zu erhalten, mit denen Sie einen effizienten Server erstellen können.

Obwohl ACE in C++ implementiert ist, decken die Bücher viele nützliche Muster ab, die in jeder Programmiersprache verwendet werden können.

0
lothar