So schreiben Sie einen skalierbaren Tcp / Ip-basierten Server

148

Ich bin in der Entwurfsphase des Schreibens einer neuen Windows-Dienstanwendung, die TCP / IP-Verbindungen für lang laufende 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 oder in Verbindung sogar Wochen).

Ich suche nach Ideen, wie die Netzwerkarchitektur am besten gestaltet werden kann. Ich muss mindestens einen Thread für den Dienst starten. Ich denke darüber nach, die Asynch-API (BeginRecieve usw.) zu verwenden, da ich nicht weiß, wie viele Clients ich zu einem bestimmten Zeitpunkt verbunden haben werde (möglicherweise Hunderte). Ich möchte definitiv nicht für jede Verbindung einen Thread starten.

Daten fließen hauptsächlich von meinem Server an die Clients, aber gelegentlich werden einige Befehle von den Clients gesendet. Dies ist in erster Linie eine Überwachungsanwendung, bei der mein Server regelmäßig Statusdaten an die Clients sendet.

Irgendwelche Vorschläge, wie dies am besten so skalierbar wie möglich gemacht werden kann? Grundlegender Workflow? Vielen Dank.

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

BOUNTY HINWEIS: Um das Kopfgeld zu erhalten, erwarte ich mehr als eine einfache Antwort. Ich würde ein funktionierendes Beispiel für eine Lösung benötigen, entweder als Zeiger auf etwas, das ich herunterladen könnte, oder als kurzes Beispiel inline. 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 entschied mich für die bekanntere Begin / End-Methode. Esacs Lösung mag zwar besser sein, aber sie ist noch neu genug, dass ich nicht genau weiß, wie sie 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.

Erik Funkenbusch
quelle
1
Sind Sie absolut sicher, dass es sich um eine langfristige Verbindung handeln muss? Es ist schwer zu sagen, von den begrenzten Informationen zur Verfügung gestellt, aber ich würde das nur tun, wenn absolut notwendig ..
Markt
Ja, es muss lange dauern. Die Daten müssen in Echtzeit aktualisiert werden, damit ich keine regelmäßigen Abfragen durchführen kann. Die Daten müssen sofort an den Client gesendet werden, was eine konstante Verbindung bedeutet.
Erik Funkenbusch
1
Das ist kein triftiger Grund. HTTP unterstützt lange laufende Verbindungen ganz gut. Sie öffnen einfach eine Verbindung und warten auf eine Antwort (blockierte Umfrage). Dies funktioniert gut für viele Apps im AJAX-Stil usw. Wie funktioniert Google Mail Ihrer Meinung nach :-)
TFD
2
Google Mail fragt regelmäßig nach E-Mails ab und hält keine lange Verbindung aufrecht. Dies ist in Ordnung für E-Mails, bei denen keine Echtzeitantwort erforderlich ist.
Erik Funkenbusch
2
Das Abrufen oder Ziehen lässt sich gut skalieren, entwickelt jedoch schnell eine Latenz. Pushing skaliert nicht so gut, hilft aber dabei, die Latenz zu verringern oder zu eliminieren.
Andrewbadera

Antworten:

92

Ich habe in der Vergangenheit etwas Ähnliches geschrieben. Meine Recherchen 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 Klasse geschrieben, die alle Verbindungen für die Server verwaltet.

Ich habe einfach eine Liste verwendet, um alle Clientverbindungen zu speichern. Wenn Sie jedoch schnellere Suchvorgänge für größere Listen benötigen, können Sie diese nach Belieben schreiben.

private List<xConnection> _sockets;

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

private System.Net.Sockets.Socket _serverSocket;

Die Startmethode startet tatsächlich den Server-Socket und wartet auf eingehende Verbindungen.

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 ich dort einen Code zur Unterdrückung von Ausnahmen hatte, damit alle Ausnahmen unterdrückt werden und zurückkehren, falsewenn eine Konfigurationsoption festgelegt wurde, aber ich wollte ihn entfernen Kürze.

Das oben beschriebene _serverSocket.BeginAccept (neues AsyncCallback (acceptCallback)), _serverSocket) setzt unseren Server-Socket im Wesentlichen so, dass die acceptCallback-Methode aufgerufen wird, wenn ein Benutzer eine Verbindung herstellt. Diese Methode wird aus dem .NET-Threadpool ausgeführt, der automatisch das Erstellen zusätzlicher Arbeitsthreads übernimmt, wenn Sie viele Blockierungsvorgänge ausführen. Dies sollte jede 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 die Annahme der eingehenden Verbindung beendet, Warteschlangen, bei BeginReceivedenen es sich um einen Rückruf handelt, der ausgeführt wird, wenn der Client Daten sendet, und dann die nächste Warteschlange, die acceptCallbackdie nächste eingehende Clientverbindung akzeptiert.

Der BeginReceiveMethodenaufruf teilt dem Socket mit, was zu tun ist, wenn er Daten vom Client empfängt. Für BeginReceive, müssen Sie es ein Byte - Array geben, das ist , wo er die Daten kopiert werden , wenn der Client Daten sendet. Die ReceiveCallbackMethode wird aufgerufen. So gehen wir mit dem Empfang von Daten um.

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, das in diesem Bereich des Codes zu erwähnen:

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

Im Allgemeinen würde ich in dem Code, den Sie möchten, Pakete wieder zu Nachrichten zusammenfügen und sie dann als Jobs im Thread-Pool erstellen. Auf diese Weise wird das BeginReceive des nächsten Blocks vom Client nicht verzögert, während der Nachrichtenverarbeitungscode ausgeführt wird.

Der Rückruf zum Akzeptieren beendet das Lesen des Daten-Sockets durch Aufrufen von Ende Empfangen. Dies füllt den Puffer, der in der Startempfangsfunktion bereitgestellt wird. Sobald Sie tun, was Sie wollen, wo ich den Kommentar hinterlassen habe, rufen wir die nächste BeginReceiveMethode auf, die den Rückruf erneut ausführt, wenn der Client weitere Daten sendet. Hier ist der wirklich schwierige Teil: Wenn der Client Daten sendet, wird Ihr Empfangsrückruf möglicherweise nur mit einem Teil der Nachricht aufgerufen. Der Zusammenbau kann sehr, sehr kompliziert werden. Ich habe meine eigene Methode verwendet und dazu eine Art proprietäres Protokoll erstellt. Ich habe es weggelassen, aber wenn Sie es wünschen, kann ich es hinzufügen. Dieser Handler war tatsächlich der komplizierteste Code, den ich jemals 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 SendAufruf, was für mich aufgrund der Nachrichtengröße und des Multithread-Charakters meiner Anwendung in Ordnung war. Wenn Sie an jeden Client senden möchten, müssen Sie lediglich die _sockets-Liste durchlaufen.

Die xConnection-Klasse, auf die oben verwiesen wird, ist im Grunde 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 einbinde, 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 sein sollten. Zum einen muss immer nur eine Person BeginAcceptangerufen werden. Früher gab es einen sehr nervigen .net-Fehler, der vor Jahren aufgetreten ist, sodass ich mich nicht an die Details erinnere.

Außerdem ReceiveCallbackverarbeiten wir im Code alles, was vom Socket empfangen wurde, bevor wir den nächsten Empfang in die Warteschlange stellen. Dies bedeutet, dass wir für einen einzelnen Socket ReceiveCallbackzu jedem Zeitpunkt tatsächlich immer nur einmal 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.

Kevin Nisbet
quelle
1
Dies ist eine gute Antwort, Kevin. Sieht so aus, als wären Sie auf dem richtigen Weg, um das Kopfgeld zu erhalten. :)
Erik Funkenbusch
6
Ich weiß nicht, warum dies die am höchsten bewertete Antwort ist. Begin * End * ist weder der schnellste Weg zum Networking in C # noch der am besten skalierbare. Es ist schneller als synchron, aber es gibt viele Vorgänge unter der Haube in Windows, die diesen Netzwerkpfad wirklich verlangsamen.
Esac
6
Denken Sie daran, was esac im vorherigen Kommentar geschrieben hat. Das Anfang-Ende-Muster wird wahrscheinlich bis zu einem gewissen Punkt für Sie funktionieren, obwohl mein Code derzeit Anfang-Ende verwendet, aber es gibt Verbesserungen bei den Einschränkungen in .net 3.5. Die Prämie ist mir egal, aber ich würde empfehlen, den Link in meiner Antwort zu lesen, auch wenn Sie diesen Ansatz implementieren. "Socket Performance Enhancements in Version 3.5"
jvanderh
1
Ich wollte sie nur einwerfen, da ich vielleicht nicht klar genug war. Dies ist der Code der .net 2.0-Ära, bei dem ich glaube, dass dies ein sehr praktikables Muster war. Die Antwort von esac scheint jedoch etwas moderner zu sein, wenn ich auf .net 3.5 ziele. Der einzige Trottel, den ich habe, ist das Auslösen von Ereignissen :), aber das kann leicht geändert werden. Außerdem habe ich Durchsatztests mit diesem Code durchgeführt und auf einem Dual-Core-Opteron mit 2 GHz konnte das Ethernet mit 100 Mbit / s maximal genutzt werden. Dadurch wurde eine Verschlüsselungsschicht über diesem Code hinzugefügt.
Kevin Nisbet
1
@ KevinNisbet Ich weiß, dass dies ziemlich spät ist, aber für jeden, der diese Antwort verwendet, um seine eigenen Server zu entwerfen, sollte das Senden auch asynchron sein, da Sie sich sonst für die Möglichkeit eines Deadlocks öffnen. Wenn beide Seiten Daten schreiben, die ihre jeweiligen Puffer Sendfüllen , werden die Methoden auf beiden Seiten auf unbestimmte Zeit blockiert, da niemand die Eingabedaten liest.
Luaan
83

Es gibt viele Möglichkeiten, Netzwerkoperationen in C # durchzuführen. Alle von ihnen verwenden unterschiedliche Mechanismen unter der Haube und leiden daher unter großen Leistungsproblemen bei hoher Parallelität. Begin * -Operationen sind eine davon, die viele Leute oft für die schnellere / schnellste Art der Vernetzung halten.

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 Verbesserungen an 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 heißen Bereichen verwenden (z. B. beim Empfang großer Datenmengen).

Das Hauptmerkmal dieser Verbesserungen ist die Vermeidung der wiederholten Zuordnung und Synchronisation von Objekten während asynchroner Socket-E / A mit hohem Volumen. Für das derzeit von der System.Net.Sockets .. ::. Socket-Klasse implementierte Begin / End-Entwurfsmuster muss für jede asynchrone Socket-Operation ein System .. ::. IAsyncResult-Objekt zugewiesen werden.

Unter dem Deckmantel verwendet die * Async-API E / A-Abschlussports, die die schnellste Methode zum Ausführen von Netzwerkvorgängen 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. Um die Daten nicht inline zu verarbeiten, entscheide ich mich stattdessen dafür, sie in eine Warteschlange ohne Sperren (ohne Wartezeiten) zu verschieben, die in einem separaten Thread verarbeitet wird. Beachten Sie, dass ich nicht die entsprechende Pool-Klasse einbeziehe, bei der es sich nur um einen einfachen Pool handelt, der ein neues Objekt erstellt, wenn es leer ist, und die Buffer-Klasse, bei der es sich nur um einen selbstexpandierenden Puffer handelt, der nur dann wirklich benötigt wird, wenn Sie eine Unbestimmtheit erhalten Datenmenge. Wenn Sie weitere Informationen wünschen, senden Sie mir bitte eine PM.

 public class Telnet
{
    private readonly Pool<SocketAsyncEventArgs> m_EventArgsPool;
    private Socket m_ListenSocket;

    /// <summary>
    /// This event fires when a connection has been established.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Connected;

    /// <summary>
    /// This event fires when a connection has been shutdown.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> Disconnected;

    /// <summary>
    /// This event fires when data is received on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataReceived;

    /// <summary>
    /// This event fires when data is finished sending on the socket.
    /// </summary>
    public event EventHandler<SocketAsyncEventArgs> DataSent;

    /// <summary>
    /// This event fires when a line has been received.
    /// </summary>
    public event EventHandler<LineReceivedEventArgs> LineReceived;

    /// <summary>
    /// Specifies the port to listen on.
    /// </summary>
    [DefaultValue(23)]
    public int ListenPort { get; set; }

    /// <summary>
    /// Constructor for Telnet class.
    /// </summary>
    public Telnet()
    {           
        m_EventArgsPool = new Pool<SocketAsyncEventArgs>();
        ListenPort = 23;
    }

    /// <summary>
    /// Starts the telnet server listening and accepting data.
    /// </summary>
    public void Start()
    {
        IPEndPoint endpoint = new IPEndPoint(0, ListenPort);
        m_ListenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        m_ListenSocket.Bind(endpoint);
        m_ListenSocket.Listen(100);

        //
        // Post Accept
        //
        StartAccept(null);
    }

    /// <summary>
    /// Not Yet Implemented. Should shutdown all connections gracefully.
    /// </summary>
    public void Stop()
    {
        //throw (new NotImplementedException());
    }

    //
    // ACCEPT
    //

    /// <summary>
    /// Posts a requests for Accepting a connection. If it is being called from the completion of
    /// an AcceptAsync call, then the AcceptSocket is cleared since it will create a new one for
    /// the new user.
    /// </summary>
    /// <param name="e">null if posted from startup, otherwise a <b>SocketAsyncEventArgs</b> for reuse.</param>
    private void StartAccept(SocketAsyncEventArgs e)
    {
        if (e == null)
        {
            e = m_EventArgsPool.Pop();
            e.Completed += Accept_Completed;
        }
        else
        {
            e.AcceptSocket = null;
        }

        if (m_ListenSocket.AcceptAsync(e) == false)
        {
            Accept_Completed(this, e);
        }
    }

    /// <summary>
    /// Completion callback routine for the AcceptAsync post. This will verify that the Accept occured
    /// and then setup a Receive chain to begin receiving data.
    /// </summary>
    /// <param name="sender">object which posted the AcceptAsync</param>
    /// <param name="e">Information about the Accept call.</param>
    private void Accept_Completed(object sender, SocketAsyncEventArgs e)
    {
        //
        // Socket Options
        //
        e.AcceptSocket.NoDelay = true;

        //
        // Create and setup a new connection object for this user
        //
        Connection connection = new Connection(this, e.AcceptSocket);

        //
        // Tell the client that we will be echo'ing data sent
        //
        DisableEcho(connection);

        //
        // Post the first receive
        //
        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;

        //
        // Connect Event
        //
        if (Connected != null)
        {
            Connected(this, args);
        }

        args.Completed += Receive_Completed;
        PostReceive(args);

        //
        // Post another accept
        //
        StartAccept(e);
    }

    //
    // RECEIVE
    //    

    /// <summary>
    /// Post an asynchronous receive on the socket.
    /// </summary>
    /// <param name="e">Used to store information about the Receive call.</param>
    private void PostReceive(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection != null)
        {
            connection.ReceiveBuffer.EnsureCapacity(64);
            e.SetBuffer(connection.ReceiveBuffer.DataBuffer, connection.ReceiveBuffer.Count, connection.ReceiveBuffer.Remaining);

            if (connection.Socket.ReceiveAsync(e) == false)
            {
                Receive_Completed(this, e);
            }              
        }
    }

    /// <summary>
    /// Receive completion callback. Should verify the connection, and then notify any event listeners
    /// that data has been received. For now it is always expected that the data will be handled by the
    /// listeners and thus the buffer is cleared after every call.
    /// </summary>
    /// <param name="sender">object which posted the ReceiveAsync</param>
    /// <param name="e">Information about the Receive call.</param>
    private void Receive_Completed(object sender, SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success || connection == null)
        {
            Disconnect(e);
            return;
        }

        connection.ReceiveBuffer.UpdateCount(e.BytesTransferred);

        OnDataReceived(e);

        HandleCommand(e);
        Echo(e);

        OnLineReceived(connection);

        PostReceive(e);
    }

    /// <summary>
    /// Handles Event of Data being Received.
    /// </summary>
    /// <param name="e">Information about the received data.</param>
    protected void OnDataReceived(SocketAsyncEventArgs e)
    {
        if (DataReceived != null)
        {                
            DataReceived(this, e);
        }
    }

    /// <summary>
    /// Handles Event of a Line being Received.
    /// </summary>
    /// <param name="connection">User connection.</param>
    protected void OnLineReceived(Connection connection)
    {
        if (LineReceived != null)
        {
            int index = 0;
            int start = 0;

            while ((index = connection.ReceiveBuffer.IndexOf('\n', index)) != -1)
            {
                string s = connection.ReceiveBuffer.GetString(start, index - start - 1);
                s = s.Backspace();

                LineReceivedEventArgs args = new LineReceivedEventArgs(connection, s);
                Delegate[] delegates = LineReceived.GetInvocationList();

                foreach (Delegate d in delegates)
                {
                    d.DynamicInvoke(new object[] { this, args });

                    if (args.Handled == true)
                    {
                        break;
                    }
                }

                if (args.Handled == false)
                {
                    connection.CommandBuffer.Enqueue(s);
                }

                start = index;
                index++;
            }

            if (start > 0)
            {
                connection.ReceiveBuffer.Reset(0, start + 1);
            }
        }
    }

    //
    // SEND
    //

    /// <summary>
    /// Overloaded. Sends a string over the telnet socket.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="s">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, string s)
    {
        if (String.IsNullOrEmpty(s) == false)
        {
            return Send(connection, Encoding.Default.GetBytes(s));
        }

        return false;
    }

    /// <summary>
    /// Overloaded. Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <returns>true if the data was sent successfully.</returns>
    public bool Send(Connection connection, byte[] data)
    {
        return Send(connection, data, 0, data.Length);
    }

    public bool Send(Connection connection, char c)
    {
        return Send(connection, new byte[] { (byte)c }, 0, 1);
    }

    /// <summary>
    /// Sends an array of data to the client.
    /// </summary>
    /// <param name="connection">Connection to send data on.</param>
    /// <param name="data">Data to send.</param>
    /// <param name="offset">Starting offset of date in the buffer.</param>
    /// <param name="length">Amount of data in bytes to send.</param>
    /// <returns></returns>
    public bool Send(Connection connection, byte[] data, int offset, int length)
    {
        bool status = true;

        if (connection.Socket == null || connection.Socket.Connected == false)
        {
            return false;
        }

        SocketAsyncEventArgs args = m_EventArgsPool.Pop();
        args.UserToken = connection;
        args.Completed += Send_Completed;
        args.SetBuffer(data, offset, length);

        try
        {
            if (connection.Socket.SendAsync(args) == false)
            {
                Send_Completed(this, args);
            }
        }
        catch (ObjectDisposedException)
        {                
            //
            // return the SocketAsyncEventArgs back to the pool and return as the
            // socket has been shutdown and disposed of
            //
            m_EventArgsPool.Push(args);
            status = false;
        }

        return status;
    }

    /// <summary>
    /// Sends a command telling the client that the server WILL echo data.
    /// </summary>
    /// <param name="connection">Connection to disable echo on.</param>
    public void DisableEcho(Connection connection)
    {
        byte[] b = new byte[] { 255, 251, 1 };
        Send(connection, b);
    }

    /// <summary>
    /// Completion callback for SendAsync.
    /// </summary>
    /// <param name="sender">object which initiated the SendAsync</param>
    /// <param name="e">Information about the SendAsync call.</param>
    private void Send_Completed(object sender, SocketAsyncEventArgs e)
    {
        e.Completed -= Send_Completed;              
        m_EventArgsPool.Push(e);
    }        

    /// <summary>
    /// Handles a Telnet command.
    /// </summary>
    /// <param name="e">Information about the data received.</param>
    private void HandleCommand(SocketAsyncEventArgs e)
    {
        Connection c = e.UserToken as Connection;

        if (c == null || e.BytesTransferred < 3)
        {
            return;
        }

        for (int i = 0; i < e.BytesTransferred; i += 3)
        {
            if (e.BytesTransferred - i < 3)
            {
                break;
            }

            if (e.Buffer[i] == (int)TelnetCommand.IAC)
            {
                TelnetCommand command = (TelnetCommand)e.Buffer[i + 1];
                TelnetOption option = (TelnetOption)e.Buffer[i + 2];

                switch (command)
                {
                    case TelnetCommand.DO:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                    case TelnetCommand.WILL:
                        if (option == TelnetOption.Echo)
                        {
                            // ECHO
                        }
                        break;
                }

                c.ReceiveBuffer.Remove(i, 3);
            }
        }          
    }

    /// <summary>
    /// Echoes data back to the client.
    /// </summary>
    /// <param name="e">Information about the received data to be echoed.</param>
    private void Echo(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            return;
        }

        //
        // backspacing would cause the cursor to proceed beyond the beginning of the input line
        // so prevent this
        //
        string bs = connection.ReceiveBuffer.ToString();

        if (bs.CountAfterBackspace() < 0)
        {
            return;
        }

        //
        // find the starting offset (first non-backspace character)
        //
        int i = 0;

        for (i = 0; i < connection.ReceiveBuffer.Count; i++)
        {
            if (connection.ReceiveBuffer[i] != '\b')
            {
                break;
            }
        }

        string s = Encoding.Default.GetString(e.Buffer, Math.Max(e.Offset, i), e.BytesTransferred);

        if (connection.Secure)
        {
            s = s.ReplaceNot("\r\n\b".ToCharArray(), '*');
        }

        s = s.Replace("\b", "\b \b");

        Send(connection, s);
    }

    //
    // DISCONNECT
    //

    /// <summary>
    /// Disconnects a socket.
    /// </summary>
    /// <remarks>
    /// It is expected that this disconnect is always posted by a failed receive call. Calling the public
    /// version of this method will cause the next posted receive to fail and this will cleanup properly.
    /// It is not advised to call this method directly.
    /// </remarks>
    /// <param name="e">Information about the socket to be disconnected.</param>
    private void Disconnect(SocketAsyncEventArgs e)
    {
        Connection connection = e.UserToken as Connection;

        if (connection == null)
        {
            throw (new ArgumentNullException("e.UserToken"));
        }

        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch
        {
        }

        connection.Socket.Close();

        if (Disconnected != null)
        {
            Disconnected(this, e);
        }

        e.Completed -= Receive_Completed;
        m_EventArgsPool.Push(e);
    }

    /// <summary>
    /// Marks a specific connection for graceful shutdown. The next receive or send to be posted
    /// will fail and close the connection.
    /// </summary>
    /// <param name="connection"></param>
    public void Disconnect(Connection connection)
    {
        try
        {
            connection.Socket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception)
        {
        }            
    }

    /// <summary>
    /// Telnet command codes.
    /// </summary>
    internal enum TelnetCommand
    {
        SE = 240,
        NOP = 241,
        DM = 242,
        BRK = 243,
        IP = 244,
        AO = 245,
        AYT = 246,
        EC = 247,
        EL = 248,
        GA = 249,
        SB = 250,
        WILL = 251,
        WONT = 252,
        DO = 253,
        DONT = 254,
        IAC = 255
    }

    /// <summary>
    /// Telnet command options.
    /// </summary>
    internal enum TelnetOption
    {
        Echo = 1,
        SuppressGoAhead = 3,
        Status = 5,
        TimingMark = 6,
        TerminalType = 24,
        WindowSize = 31,
        TerminalSpeed = 32,
        RemoteFlowControl = 33,
        LineMode = 34,
        EnvironmentVariables = 36
    }
}
esac
quelle
Dies ist ziemlich einfach und ein einfaches Beispiel. Vielen Dank. Ich muss die Vor- und Nachteile jeder Methode bewerten.
Erik Funkenbusch
Ich hatte keine Gelegenheit, es zu testen, aber ich habe hier aus irgendeinem Grund das vage Gefühl einer Rennbedingung. Erstens, wenn Sie viele Nachrichten erhalten, weiß ich nicht, dass die Ereignisse in der richtigen Reihenfolge verarbeitet werden (möglicherweise nicht wichtig für die Benutzer-App, sollte aber beachtet werden), oder ich könnte mich irren und die Ereignisse werden in der richtigen Reihenfolge verarbeitet. Zweitens habe ich es vielleicht verpasst, aber besteht nicht das Risiko, dass der Puffer überschrieben wird, während DataReceived noch ausgeführt wird, wenn es lange dauert? Wenn diese möglicherweise ungerechtfertigten Bedenken ausgeräumt werden, halte ich dies für eine sehr gute moderne Lösung.
Kevin Nisbet
1
In meinem Fall sind für meinen Telnet-Server 100% JA, sie sind in Ordnung. Der Schlüssel ist das Festlegen der richtigen Rückrufmethode vor dem Aufrufen von AcceptAsync, ReceiveAsync usw. In meinem Fall führe ich SendAsync in einem separaten Thread aus. Wenn dies also geändert wird, um ein Accept / Send / Receive / Send / Receive / Disconnect-Muster auszuführen, dann es muss geändert werden.
Esac
1
Punkt 2 ist auch etwas, das Sie berücksichtigen müssen. Ich speichere mein 'Connection'-Objekt im SocketAsyncEventArgs-Kontext. Dies bedeutet, dass ich nur einen Empfangspuffer pro Verbindung habe. Ich poste keinen weiteren Empfang mit diesen SocketAsyncEventArgs, bis DataReceived abgeschlossen ist, sodass keine weiteren Daten dazu gelesen werden können, bis sie abgeschlossen sind. Ich rate Ihnen, diese Daten nicht lange zu bearbeiten. Ich verschiebe tatsächlich den gesamten Puffer aller empfangenen Daten in eine Warteschlange ohne Sperre und verarbeite sie dann in einem separaten Thread. Dies stellt eine geringe Latenz im Netzwerkbereich sicher.
Esac
1
Nebenbei bemerkt, ich habe Unit-Tests und Lasttests für diesen Code geschrieben, und als ich die Benutzerlast von 1 Benutzer auf 250 Benutzer (auf einem Single-Dual-Core-System, 4 GB RAM) erhöhte, betrug die Antwortzeit 100 Byte (1) Paket) und 10000 Bytes (3 Pakete) blieben während der gesamten Benutzerlastkurve gleich.
Esac
46

Früher gab es eine wirklich gute Diskussion über skalierbares TCP / IP mit .NET, geschrieben von Chris Mullins von Coversant. Leider scheint sein Blog von seinem vorherigen Speicherort verschwunden zu sein, daher werde ich versuchen, seine Ratschläge aus dem Speicher 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 sowohl using Begin/Endals auch die AsyncMethoden in der SocketKlasse IOP Completion Ports (IOCP) verwenden, um Skalierbarkeit zu gewährleisten. Dies macht einen viel größeren Unterschied (bei korrekter Verwendung; siehe unten) zur Skalierbarkeit als die beiden Methoden, die Sie tatsächlich zur Implementierung Ihrer Lösung auswählen.

Die Beiträge von Chris Mullins basierten auf der Verwendung Begin/End, mit der ich persönlich Erfahrung habe. Beachten Sie, dass Chris eine darauf basierende Lösung zusammengestellt hat, die bis zu 10.000 gleichzeitige Clientverbindungen auf einem 32-Bit-Computer mit 2 GB Speicher und bis zu 100.000 auf einer 64-Bit-Plattform mit ausreichend Speicher skaliert. Aus meiner eigenen Erfahrung mit dieser Technik (obwohl bei weitem nicht in der Nähe dieser Art von Last) habe ich keinen Grund, an diesen indikativen Zahlen 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 Windows-Thread-Pool auf sehr niedriger Ebene verwendet wird, der keine Threads aufweckt, bis tatsächlich Daten auf dem E / A-Kanal vorhanden sind, aus dem Sie lesen möchten ( Beachten Sie, dass IOCP auch für Datei-E / A verwendet werden kann. Dies hat den Vorteil, dass Windows nicht nur zu einem Thread wechseln muss, um festzustellen, dass ohnehin noch keine Daten vorhanden sind. Dadurch wird die Anzahl der Kontextwechsel, die Ihr Server durchführen muss, auf das erforderliche Minimum reduziert.

Kontextwechsel sind definitiv das, was den "Thread-per-Connection" -Mechanismus zunichte macht, obwohl dies eine praktikable Lösung ist, wenn Sie nur mit ein paar Dutzend Verbindungen arbeiten. Dieser Mechanismus ist jedoch keineswegs "skalierbar".

Wichtige Überlegungen bei der Verwendung von IOCP

Erinnerung

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- BeginReceiveAufruf führt dazu, dass der Puffer, in den Sie lesen, " fixiert" 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 gewissen Kompromiss. Die vorgeschlagene Lösung besteht darin, eine große zuzuweisenbyte[] beim Start der Anwendung (oder in der Nähe davon) Puffer von mindestens 90 KB zuzuweisen (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) landen, das effektiv automatisch fixiert wird. Indem Sie beim Start einen großen Puffer zuweisen, stellen Sie sicher, dass sich dieser Block unbeweglichen Speichers 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 einige Daten lesen muss, in separate Bereiche zu segmentieren. 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 angeheftete Puffer zuweisen, sobald Sie sie benötigen).

Die einfachste Lösung wäre, jeder Verbindung ein einzelnes Byte mit einem eindeutigen Versatz innerhalb dieses Puffers zuzuweisen. Anschließend können Sie BeginReceiveein einzelnes zu lesendes Byte aufrufen und den Rest des Lesevorgangs als Ergebnis des erhaltenen Rückrufs ausführen.

wird bearbeitet

Wenn Sie den Rückruf von dem von BeginIhnen getätigten Anruf 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 notwendig , dass Sie lange Operationen in diesem Rückruf vermeiden. Die Verwendung dieser Threads für die komplexe Verarbeitung beeinträchtigt Ihre Skalierbarkeit genauso effektiv wie die Verwendung von "Thread pro Verbindung".

Die vorgeschlagene Lösung besteht darin, den Rückruf nur zum Einreihen eines Arbeitselements in die Warteschlange zu verwenden, um die eingehenden Daten zu verarbeiten, die in 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 zu seinem Pool zurückkehren kann. In .NET 4.0 würde ich vorschlagen, dass die einfachste Lösung darin besteht, a zu erzeugen Taskund ihm einen Verweis auf den Client-Socket und eine Kopie des ersten Bytes zu geben, das bereits vom BeginReceiveAufruf gelesen wurde . Diese Aufgabe ist dann dafür verantwortlich, alle Daten aus dem Socket zu lesen, die die von Ihnen verarbeitete Anforderung darstellen, sie auszuführen und dann eine neue zu erstellenBeginReceive 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, an den Sie übergeben, BeginReceivebereits fixiert ist.
  • Stellen Sie sicher, dass der Rückruf, an den Sie übergeben, BeginReceivelediglich eine Aufgabe in die Warteschlange stellt, um die eigentliche Verarbeitung der eingehenden Daten zu übernehmen

Wenn Sie das tun, können Sie ohne Zweifel die Ergebnisse von Chris replizieren und auf potenziell Hunderttausende gleichzeitiger Clients skalieren (vorausgesetzt, die richtige Hardware und eine effiziente Implementierung Ihres eigenen Verarbeitungscodes;).

jerryjvl
quelle
1
Um einen kleineren Speicherblock zu fixieren, kann der Puffer mit der GCHandle-Objektzuweisungsmethode fixiert werden. Sobald dies erledigt ist, kann das UnsafeAddrOfPinnedArrayElement des Marshal-Objekts verwendet werden, um einen Zeiger auf den Puffer zu erhalten. Zum Beispiel: GCHandle gchTheCards = GCHandle.Alloc (TheData, GCHandleType.Pinned); IntPtr pAddr = Marshal.UnsafeAddrOfPinnedArrayElement (TheData, 0); (sbyte *) pTheData = (sbyte *) pAddr.ToPointer ();
Bob Bryan
@BobBryan Sofern ich keinen subtilen Punkt verpasse, den Sie ansprechen möchten, hilft dieser Ansatz nicht bei dem Problem, das meine Lösung durch die Zuweisung großer Blöcke zu lösen versucht, nämlich das Potenzial für eine dramatische Speicherfragmentierung, die mit der wiederholten Zuweisung kleiner angehefteter Blöcke verbunden ist der Erinnerung.
Jerryjvl
Nun, der Punkt ist, dass Sie keinen großen Block zuweisen müssen, um ihn im Speicher zu halten. Sie können kleinere Blöcke zuweisen und die oben beschriebene Technik verwenden, um sie im Speicher zu fixieren, damit der GC sie nicht verschiebt. Sie können einen Verweis auf jeden der kleineren Blöcke beibehalten, ähnlich wie Sie einen Verweis auf einen einzelnen größeren Block beibehalten und ihn nach Bedarf wiederverwenden. Beide Ansätze sind gültig - ich habe nur darauf hingewiesen, dass Sie keinen sehr großen Puffer verwenden müssen. Allerdings ist die Verwendung eines sehr großen Puffers manchmal der beste Weg, da der GC ihn effizienter behandelt.
Bob Bryan
@BobBryan Da das Fixieren des Puffers automatisch erfolgt, wenn Sie BeginReceive aufrufen, ist das Fixieren hier nicht wirklich der hervorstechende Punkt. Die Effizienz war;) ... und dies ist besonders wichtig, wenn versucht wird, einen skalierbaren Server zu schreiben, weshalb große Blöcke für den Pufferplatz zugewiesen werden müssen.
Jerryjvl
@jerryjvl Es tut mir leid, eine wirklich alte Frage zu stellen, aber ich habe kürzlich genau dieses Problem mit Asynch-Methoden von BeginXXX / EndXXX entdeckt. Dies ist ein großartiger Beitrag, aber es hat viel Graben gekostet, ihn zu finden. Ich mag Ihre vorgeschlagene Lösung, verstehe aber einen Teil davon nicht: "Dann können Sie einen BeginReceive-Aufruf für ein einzelnes zu lesendes Byte durchführen und den Rest des Lesens als Ergebnis des Rückrufs ausführen, den Sie erhalten." Was meinen Sie damit, den Rest der Vorbereitung als Ergebnis des Rückrufs durchzuführen, den Sie erhalten?
Mausimo
22

Sie haben den größten Teil der Antwort bereits über die obigen Codebeispiele erhalten. Die Verwendung der asynchronen E / A-Operation ist hier absolut der richtige Weg. Async IO ist die Art und Weise, wie Win32 intern maßstabsgetreu konzipiert ist. Die bestmögliche Leistung wird durch die Verwendung von Abschlussports erzielt, indem Ihre Sockets an Abschlussports gebunden werden und ein Thread-Pool auf die Fertigstellung des Abschlussports wartet. Es ist allgemein bekannt, dass 2-4 Threads pro CPU (Kern) auf den Abschluss warten. Ich empfehle dringend, diese drei Artikel von Rick Vicik vom Windows Performance-Team zu lesen:

  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 3

Die genannten Artikel behandeln hauptsächlich die native Windows-API, sind jedoch ein Muss für jeden, der versucht, die Skalierbarkeit und Leistung zu verstehen. Sie haben auch einige Informationen über die verwaltete Seite der Dinge.

Als zweites müssen Sie sicherstellen , dass Sie das online verfügbare Buch zur Verbesserung der Leistung und Skalierbarkeit von .NET-Anwendungen lesen . In Kapitel 5 finden Sie sachdienliche und gültige Ratschläge zur Verwendung von Threads, asynchronen Aufrufen und Sperren. Die wahren Juwelen finden Sie jedoch in Kapitel 17, in dem Sie nützliche Informationen wie die praktische Anleitung 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, dass Sie einen reinen TCP-Server verwenden möchten, daher ist mein nächster Punkt falsch. Allerdings , wenn Sie finden sich die Enge getrieben und verwenden Sie die WebRequest - Klasse und deren Derivate, seien Sie gewarnt , dass es einen Drachen , die Tür ist bewacht: die Servicepoint . 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, da Ihre Anwendung sonst niemals skaliert wird (ich lasse Sie selbst herausfinden, was der Standardwert ist ...). Sie können auch die Standardrichtlinie zum Senden eines Expect100Continue-Headers in den http-Anforderungen überdenken.

Was nun die Core Socket Managed API betrifft, sind die Dinge auf der Sendeseite ziemlich einfach, auf der Empfangsseite jedoch wesentlich komplexer. Um einen hohen Durchsatz und eine hohe Skalierbarkeit zu erzielen, müssen Sie sicherstellen, dass der Socket nicht durchflussgesteuert ist, da kein Puffer für den Empfang gebucht ist. Ideal für eine hohe Leistung sollten Sie 3-4 Puffer vorab veröffentlichen und neue Puffer veröffentlichen, sobald Sie einen zurückerhalten ( bevor Sie den zurückerhaltenen verarbeiten), damit Sie sicherstellen, dass der Socket immer einen Ort hat, an dem die vom Netzwerk kommenden Daten abgelegt werden können. Sie werden sehen, warum Sie dies wahrscheinlich in Kürze nicht erreichen können.

Nachdem 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 Verkehrsverschlüsselung oder zumindest Schutz vor Verkehrsmanipulationen. Auf diese Weise verwenden Sie den integrierten System.Net.Security.NegotiateStream (oder SslStream, wenn Sie unterschiedliche Domänen überqueren müssen). Dies bedeutet, dass Sie sich nicht auf asynchrone Operationen mit geradem Socket, sondern auf asynchrone AuthenticatedStream-Operationen verlassen. Sobald Sie einen Socket erhalten (entweder von Connect on Client oder von Accept on 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 Ihr Safe vor dem nativen Wahnsinn 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 empfangen sie mit BeginRead. Dies ist das Problem, von dem ich zuvor gesprochen habe, dass Sie nicht mehrere Empfangspuffer veröffentlichen können, da die AuthenticateStream-Klassen dies nicht unterstützen. Der BeginRead-Vorgang verwaltet intern alle E / A-Vorgänge, bis Sie einen gesamten Frame erhalten haben. Andernfalls kann die Nachrichtenauthentifizierung (Frame entschlüsseln und Signatur im Frame validieren) nicht verarbeitet werden. Meiner Erfahrung nach ist die Arbeit der AuthenticatedStream-Klassen jedoch ziemlich gut und sollte kein Problem damit haben. Dh. Sie sollten in der Lage sein, das GB-Netzwerk mit nur 4-5% CPU zu sättigen. Die AuthenticatedStream-Klassen legen Ihnen auch die protokollspezifischen Frame-Größenbeschränkungen auf (16 KB für SSL, 12 KB für Kerberos).

Damit sollten Sie auf dem richtigen Weg beginnen. Ich werde hier keine Postleitzahl posten, es gibt ein perfektes Beispiel für MSDN . Ich habe viele Projekte wie dieses durchgeführt und konnte ohne Probleme auf etwa 1000 verbundene Benutzer skalieren. Darüber hinaus müssen Sie die Registrierungsschlüssel ändern, damit der Kernel mehr Socket-Handles erhält. und stellen Sie sicher, dass Sie auf einem Server- Betriebssystem bereitstellen , dh W2K3, nicht XP oder Vista (dh Client-Betriebssystem). Dies macht einen großen Unterschied.

Stellen Sie übrigens sicher, dass Sie bei Datenbankoperationen auf dem Server oder in der Datei-E / A auch die asynchrone Variante für diese verwenden, da sonst der Thread-Pool in kürzester Zeit entleert wird. Stellen Sie für SQL Server-Verbindungen sicher, dass Sie der Verbindungszeichenfolge 'Asyncronous Processing = true' hinzufügen.

Remus Rusanu
quelle
Hier gibt es einige großartige Informationen. Ich wünschte, ich könnte mehreren Menschen das Kopfgeld verleihen. Ich habe Sie jedoch positiv bewertet. Gutes Zeug hier, danke.
Erik Funkenbusch
11

In einigen meiner Lösungen läuft ein solcher Server. Hier finden Sie eine sehr detaillierte Erklärung der verschiedenen Möglichkeiten in .net: Mit Hochleistungs-Sockets in .NET näher am Kabel

In letzter Zeit habe ich nach Möglichkeiten gesucht, unseren Code zu verbessern, und werde dies untersuchen: " Verbesserungen der Socket-Leistung in Version 3.5 ", die speziell "für Anwendungen verwendet wurden, die asynchrone Netzwerk-E / A verwenden, um die höchste Leistung zu erzielen".

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

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

Bearbeiten: Hier finden Sie Arbeitscode für Client und Server mit den neuen 3.5 SocketAsyncEventArgs, sodass Sie ihn innerhalb weniger Minuten testen und den Code durchgehen können. Dies ist ein einfacher Ansatz, aber die Grundlage für den Start einer viel größeren Implementierung. Auch dieser Artikel von vor fast zwei Jahren im MSDN Magazine war eine interessante Lektüre.

jvanderh
quelle
9

Haben Sie darüber nachgedacht, nur eine WCF-Netz-TCP-Bindung und ein Publish / Subscribe-Muster zu verwenden? WCF würde es Ihnen ermöglichen, sich [meistens] auf Ihre Domain zu konzentrieren, anstatt Klempnerarbeiten durchzuführen.

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

markt
quelle
8

Ich wundere mich über eine Sache:

Ich möchte definitiv nicht für jede Verbindung einen Thread starten.

Warum ist das so? 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. Insbesondere angesichts der Tatsache, dass Sie viele E / A-Vorgänge ausführen (Sie sind also nicht an die CPU gebunden und viele Threads auf der Festplatten- oder Netzwerkkommunikation blockiert), verstehe ich diese Einschränkung nicht.

Haben Sie den Multithread-Weg getestet und festgestellt, dass ihm etwas fehlt? Beabsichtigen Sie, für jeden Thread auch eine Datenbankverbindung zu haben (dies würde den Datenbankserver zerstören, daher ist dies eine schlechte Idee, die jedoch mit einem dreistufigen Design leicht gelöst werden kann). Befürchten Sie, dass Sie Tausende von Kunden anstelle von Hunderten haben und dann wirklich Probleme haben? (Obwohl ich tausend Threads oder sogar zehntausend versuchen würde, wenn ich mehr als 32 GB RAM hätte - auch hier sollte die Thread-Wechselzeit 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();
    }
  }
}

Client-Klasse:

  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();
    }
  }
}
Marcel Popescu
quelle
Windows kann viele Threads verarbeiten, aber .NET ist nicht wirklich dafür ausgelegt. Jede .NET-Appdomain verfügt über einen Thread-Pool, und Sie möchten diesen Thread-Pool nicht erschöpfen. Ich bin mir nicht sicher, ob Sie einen Thread manuell starten, ob er aus dem Threadpool stammt oder nicht. Dennoch sind Hunderte von Threads, die die meiste Zeit nichts tun, eine enorme Ressourcenverschwendung.
Erik Funkenbusch
1
Ich glaube, Sie haben eine falsche Sicht auf Threads. Threads kommen nur aus dem Thread-Pool, wenn Sie das tatsächlich wollen - reguläre Threads nicht. Hunderte von Threads, die nichts tun, verschwenden genau nichts :) (Nun, ein bisschen Speicher, aber Speicher ist so billig, dass es nicht mehr wirklich ein Problem ist.) Ich werde ein paar Beispiel-Apps dafür schreiben, an die ich eine URL senden werde es, sobald ich fertig bin. In der Zwischenzeit empfehle ich Ihnen, das, was ich oben geschrieben habe, noch einmal durchzugehen und zu versuchen, meine Fragen zu beantworten.
Marcel Popescu
1
Ich stimme Marcel's Kommentar zur Ansicht von Threads darin zu, dass erstellte Threads nicht aus dem Threadpool stammen, aber der Rest der Aussage ist nicht korrekt. Beim Speicher geht es nicht darum, wie viel auf einem Computer installiert ist. Alle Anwendungen unter Windows werden im virtuellen Adressraum und auf einem 32-Bit-System ausgeführt, das Ihnen 2 GB Daten für Ihre App liefert (unabhängig davon, wie viel RAM auf der Box installiert ist). Sie müssen noch von der Laufzeit verwaltet werden. Beim Ausführen der asynchronen E / A wird kein Thread zum Warten verwendet (es wird IOCP verwendet, das überlappende E / A zulässt). Dies ist eine bessere Lösung und lässt sich VIEL besser skalieren.
Brian ONeil
7
Wenn viele Threads ausgeführt werden, ist nicht der Speicher das Problem, sondern die CPU. Der Kontextwechsel zwischen Threads ist eine relativ teure Operation. Je aktiver die Threads sind, desto mehr Kontextwechsel werden auftreten. Vor einigen Jahren habe ich auf meinem PC einen Test mit einer C # -Konsolen-App und mit ca. 500 Threads meine CPU war 100%, die Threads machten nichts Bedeutendes. Für Netzwerkkommunikation ist es besser, die Anzahl der Threads niedrig zu halten.
Sipwiz
1
Ich würde entweder mit einer Task-Lösung gehen oder async / await verwenden. Die Task-Lösung scheint einfacher zu sein, während Async / Warten wahrscheinlich skalierbarer sind (sie waren speziell für E / A-gebundene Situationen gedacht).
Marcel Popescu
5

Verwenden von .NETs integriertem Async IO (BeginRead usw.) ist eine gute Idee, wenn Sie alle Details richtig machen können. Wenn Sie Ihre Socket- / Datei-Handles ordnungsgemäß eingerichtet haben, wird die zugrunde liegende IOCP-Implementierung des Betriebssystems verwendet, sodass Ihre Vorgänge ohne Verwendung von Threads abgeschlossen werden können (oder im schlimmsten Fall mithilfe eines Threads, von dem ich glaube, dass er stattdessen aus dem IO-Thread-Pool des Kernels stammt des Thread-Pools von .NET, wodurch die Überlastung des Thread-Pools verringert wird.)

Das wichtigste Problem ist, sicherzustellen, dass Sie Ihre Sockets / Dateien im nicht blockierenden Modus öffnen. Die meisten Standardfunktionen (wie zFile.OpenRead ) tun dies nicht, daher müssen Sie Ihre eigenen 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, mit Rennbedingungen und Deadlocks zu enden, obwohl Sie möglicherweise keine Threads direkt verwenden. Daher müssen Sie sich dessen bewusst sein.

Wenn möglich, sollten Sie versuchen, eine Komfortbibliothek zu verwenden, um die Durchführung skalierbarer asynchroner E / A zu vereinfachen.

Die Concurrency Coordination Runtime von Microsoft ist ein Beispiel für eine .NET-Bibliothek, mit der die Schwierigkeit dieser Art der Programmierung verringert werden soll. Es sieht gut aus, aber da ich es nicht benutzt habe, kann ich nicht sagen, wie gut es skalieren würde.

Für meine persönlichen Projekte, die asynchrone Netzwerk- oder Festplatten-E / A ausführen müssen , verwende ich eine Reihe von .NET-Parallelitäts- / E / A-Tools namens Squared.Task , die ich im letzten Jahr erstellt habe . Es ist inspiriert von Bibliotheken wie imvu.task und twisted , und ich habe einige Arbeitsbeispiele in das Repository aufgenommen, die Netzwerk-E / A ausfü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 Threadless Disk I / O verwendet). Die Bibliothek wurde aufgrund meiner Erfahrungen mit imvu.task geschrieben und enthält eine Reihe ziemlich umfassender Komponententests. Ich empfehle Ihnen daher dringend, sie auszuprobieren. Wenn Sie Probleme damit haben, biete ich Ihnen gerne Unterstützung an.

Aufgrund meiner Erfahrung mit asynchronen / threadlosen E / A anstelle von Threads ist es meiner Meinung nach ein lohnendes Unterfangen auf der .NET-Plattform, solange Sie bereit sind, sich mit der Lernkurve zu befassen. Auf diese Weise können Sie die Skalierbarkeitsprobleme vermeiden, die durch die Kosten von Thread-Objekten entstehen. In vielen Fällen können Sie die Verwendung von Sperren und Mutexen vollständig vermeiden, indem Sie Parallelitätsprimitive wie Futures / Promises sorgfältig verwenden.

Katelyn Gadd
quelle
Tolle Infos, ich werde Ihre Referenzen überprüfen und sehen, was Sinn macht.
Erik Funkenbusch
3

Ich habe Kevins Lösung verwendet, aber er sagt, dass der Lösung Code zum Zusammensetzen von Nachrichten fehlt. Entwickler können diesen Code zum Zusammensetzen 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;
    }

}
Ahmet Arslan
quelle
2

Eine schöne Übersicht über die Techniken finden Sie auf der C10k-Problemseite .

zvrba
quelle
1

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 und wurde entwickelt, um hochzuverlässige Anwendungen mit hohem Volumen bis hin zu Telekommunikationsqualität zu unterstützen.

Das Framework befasst sich mit einer Vielzahl von Parallelitätsmodellen und verfügt wahrscheinlich über ein Modell, das sofort für Ihre Anwendung geeignet ist. Dies sollte das Debuggen des Systems erleichtern, da die meisten Probleme mit der Parallelität bereits behoben wurden. Der Nachteil hierbei ist, dass das Framework in C ++ geschrieben ist und nicht die warmeste und flauschigste Codebasis ist. Auf der anderen Seite erhalten Sie eine getestete Netzwerkinfrastruktur in Industriequalität und eine hoch skalierbare Architektur.

ConcernedOfTunbridgeWells
quelle
2
Das ist ein guter Vorschlag, aber von den Tags der Frage glaube ich, dass das OP C #
JPCosta
Ich habe bemerkt, dass; Der Vorschlag war, dass dies für C ++ verfügbar ist und mir nichts Äquivalentes für C # bekannt ist. Das Debuggen dieser Art von System ist im besten Fall nicht einfach, und Sie erhalten möglicherweise eine Rückkehr, wenn Sie zu diesem Framework wechseln, obwohl dies einen Wechsel zu C ++ bedeutet.
ConcernedOfTunbridgeWells
Ja, das ist C #. Ich suche nach guten .net-basierten Lösungen. Ich hätte klarer sein sollen, aber ich nahm an, dass die Leute die Tags lesen würden
Erik Funkenbusch
1

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

Alexander Torstling
quelle
1

Nun, .NET-Sockets scheinen select () bereitzustellen - das ist am besten für die Verarbeitung von Eingaben geeignet. Für die Ausgabe hätte ich einen Pool von Socket-Writer-Threads, die eine Arbeitswarteschlange abhören und den Socket-Deskriptor / das Socket-Objekt als Teil des Arbeitselements akzeptieren, sodass Sie keinen Thread pro Socket benötigen.

Nikolai Fetissov
quelle
1

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 ungefähr 35% schneller (Antwortzeit und Bitrate), da 100 Benutzer ständig Daten senden und empfangen.


quelle
1

Wenn Personen die akzeptierte Antwort einfügen möchten, können Sie die acceptCallback-Methode neu schreiben und alle Aufrufe von _serverSocket.BeginAccept (new AsyncCallback (acceptCallback), _serverSocket) entfernen. und füge es wie folgt in eine finally {} -Klausel ein:

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. Sie sollten eine typisierte Ausnahme verwenden, 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

Nicht wichtig
quelle
0

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

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

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

Lothar
quelle
-1

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

Sie erhalten nicht die höchste Skalierbarkeit, wenn Sie nur mit .NET arbeiten. GC-Pausen können die Latenz beeinträchtigen.

Ich muss mindestens einen Thread für den Dienst starten. Ich denke darüber nach, die Asynch-API (BeginRecieve usw.) zu verwenden, da ich nicht weiß, wie viele Clients ich zu einem bestimmten Zeitpunkt verbunden haben werde (möglicherweise Hunderte). Ich möchte definitiv nicht für jede Verbindung einen Thread starten.

Überlappende E / A gelten im Allgemeinen als die schnellste Windows-API für die Netzwerkkommunikation. Ich weiß nicht, ob dies mit Ihrer Asynch-API identisch ist. Verwenden Sie select nicht, da jeder Anruf jeden geöffneten Socket überprüfen muss, anstatt Rückrufe an aktiven Sockets zu haben.

Unbekannt
quelle
1
Ich verstehe Ihren Kommentar zur GC-Pause nicht. Ich habe noch nie ein System mit Skalierbarkeitsproblemen gesehen, das in direktem Zusammenhang mit GC stand.
markt
4
Es ist weitaus wahrscheinlicher, dass Sie eine App erstellen, die aufgrund einer schlechten Architektur nicht skaliert werden kann, als weil GC vorhanden ist. Sowohl mit .NET als auch mit Java wurden riesige skalierbare und leistungsfähige Systeme erstellt. In beiden von Ihnen angegebenen Links war die Ursache nicht die direkte Speicherbereinigung, sondern der Heap-Austausch. Ich würde vermuten, dass es wirklich ein Problem mit der Architektur ist, das hätte vermieden werden können. Wenn Sie mir eine Sprache zeigen können, dass es nicht möglich ist, ein System zu erstellen, das nicht skalierbar ist, werde ich es gerne verwenden;)
markt
1
Ich bin mit diesem Kommentar nicht einverstanden. Unbekannt, die Fragen, auf die Sie verweisen, sind Java. Sie befassen sich speziell mit größeren Speicherzuordnungen und dem Versuch, gc manuell zu erzwingen. Ich werde hier nicht wirklich große Mengen an Speicherzuweisung haben. Dies ist einfach kein Problem. Aber danke. Ja, das asynchrone Programmiermodell wird normalerweise zusätzlich zu überlappenden E / A implementiert.
Erik Funkenbusch
1
Tatsächlich besteht die beste Vorgehensweise darin, den GC nicht ständig manuell zum Sammeln zu zwingen. Dies könnte sehr gut dazu führen, dass Ihre App schlechter abschneidet. Der .NET GC ist ein Generations-GC, der sich an die Nutzung Ihrer App anpasst. Wenn Sie wirklich glauben , dass Sie manuell GC.Collect werden müssen fordern, würde ich sagen , dass Ihr Code höchstwahrscheinliche Bedürfnisse , um eine andere Art und Weise geschrieben werden ..
markt
1
@markt, das ist ein Kommentar für Leute, die nichts über Müllabfuhr wissen. Wenn Sie Leerlaufzeiten haben, ist eine manuelle Erfassung nicht falsch. Es wird Ihre Anwendung nicht verschlimmern, wenn sie abgeschlossen ist. Akademische Arbeiten zeigen, dass GCs von Generationen funktionieren, da sie eine Annäherung an die Lebensdauer Ihrer Objekte darstellen. Offensichtlich ist dies keine perfekte Darstellung. In der Tat gibt es ein Paradoxon, bei dem die "älteste" Generation häufig den höchsten Müllanteil aufweist, weil niemals Müll gesammelt wird.
Unbekannt
-1

Sie können das Open Source-Framework Push Framework für die Entwicklung von Hochleistungsservern verwenden. Es basiert auf IOCP und eignet sich für Push-Szenarien und Nachrichtenübertragungen.

http://www.pushframework.com

charfeddine.ahmed
quelle
1
Dieser Beitrag wurde mit C # und .net markiert. Warum haben Sie ein C ++ - Framework vorgeschlagen?
Erik Funkenbusch
Wahrscheinlich, weil er es geschrieben hat. potatosoftware.com/…
Quillbreaker
Unterstützt Pushframework mehrere Serverinstanzen? Wenn nicht, wie skaliert es?
Esskar