2009-03-15 5 views
5

Kürzlich habe ich in C# einen schnellen Proof-of-Concept-Proxy-Server geschrieben, um eine Java-Webanwendung für die Kommunikation mit einer älteren VB6-Anwendung auf einem anderen Server zu erhalten. Es ist lächerlich einfach:Gibt es bekannte Muster für asynchronen Netzwerkcode in C#?

Der Proxy-Server und die Clients verwenden beide das gleiche Nachrichtenformat; im Code verwende ich eine ProxyMessage Klasse beiden Anfragen von Kunden und Antworten vom Server generierten darzustellen:

public class ProxyMessage 
{ 
    int Length; // message length (not including the length bytes themselves) 
    string Body; // an XML string containing a request/response 

    // writes this message instance in the proper network format to stream 
    // (helper for response messages) 
    WriteToStream(Stream stream) { ... } 
} 

Die Nachrichten sind so einfach wie sein könnte: die Länge des Körpers + Nachrichtentextes.

Ich habe eine separate ProxyClient Klasse, die eine Verbindung zu einem Client darstellt. Es behandelt die gesamte Interaktion zwischen dem Proxy und einem einzelnen Client.

Was ich frage mich, ist, sind sie Design-Muster oder Best Practices zur Vereinfachung der Boilerplate Code mit asynchronen Socket-Programmierung zugeordnet? Sie müssen zum Beispiel sorgfältig den Lesepuffer verwalten, damit Sie nicht versehentlich Bytes verlieren, und Sie müssen verfolgen, wie weit Sie in der Verarbeitung der aktuellen Nachricht sind. In meinem aktuellen Code mache ich all dies in meiner Callback-Funktion für TcpClient.BeginRead, und den Zustand des Puffers und den aktuellen Status der Nachrichtenverarbeitung mit Hilfe einiger Instanzvariablen verwalten.

Der Code für meine Rückruffunktion, die ich an BeginRead übergebe, ist unten, zusammen mit den relevanten Instanzvariablen für den Kontext. Der Code scheint gut zu funktionieren, "wie er ist", aber ich frage mich, ob er etwas überarbeitet werden kann, um ihn klarer zu machen (oder vielleicht schon?).

private enum BufferStates 
{ 
    GetMessageLength, 
    GetMessageBody 
} 
// The read buffer. Initially 4 bytes because we are initially 
// waiting to receive the message length (a 32-bit int) from the client 
// on first connecting. By constraining the buffer length to exactly 4 bytes, 
// we make the buffer management a bit simpler, because 
// we don't have to worry about cases where the buffer might contain 
// the message length plus a few bytes of the message body. 
// Additional bytes will simply be buffered by the OS until we request them. 
byte[] _buffer = new byte[4]; 

// A count of how many bytes read so far in a particular BufferState. 
int _totalBytesRead = 0; 

// The state of the our buffer processing. Initially, we want 
// to read in the message length, as it's the first thing 
// a client will send 
BufferStates _bufferState = BufferStates.GetMessageLength; 

// ...ADDITIONAL CODE OMITTED FOR BREVITY... 

// This is called every time we receive data from 
// the client. 

private void ReadCallback(IAsyncResult ar) 
{ 
    try 
    { 
     int bytesRead = _tcpClient.GetStream().EndRead(ar); 

     if (bytesRead == 0) 
     { 
      // No more data/socket was closed. 
      this.Dispose(); 
      return; 
     } 

     // The state passed to BeginRead is used to hold a ProxyMessage 
     // instance that we use to build to up the message 
     // as it arrives. 
     ProxyMessage message = (ProxyMessage)ar.AsyncState; 

     if(message == null) 
      message = new ProxyMessage(); 

     switch (_bufferState) 
     { 
      case BufferStates.GetMessageLength: 

       _totalBytesRead += bytesRead; 

       // if we have the message length (a 32-bit int) 
       // read it in from the buffer, grow the buffer 
       // to fit the incoming message, and change 
       // state so that the next read will start appending 
       // bytes to the message body 

       if (_totalBytesRead == 4) 
       { 
        int length = BitConverter.ToInt32(_buffer, 0); 
        message.Length = length; 
        _totalBytesRead = 0; 
        _buffer = new byte[message.Length]; 
        _bufferState = BufferStates.GetMessageBody; 
       } 

       break; 

      case BufferStates.GetMessageBody: 

       string bodySegment = Encoding.ASCII.GetString(_buffer, _totalBytesRead, bytesRead); 
       _totalBytesRead += bytesRead; 

       message.Body += bodySegment; 

       if (_totalBytesRead >= message.Length) 
       { 
        // Got a complete message. 
        // Notify anyone interested. 

        // Pass a response ProxyMessage object to 
        // with the event so that receivers of OnReceiveMessage 
        // can send a response back to the client after processing 
        // the request. 
        ProxyMessage response = new ProxyMessage(); 
        OnReceiveMessage(this, new ProxyMessageEventArgs(message, response)); 
        // Send the response to the client 
        response.WriteToStream(_tcpClient.GetStream()); 

        // Re-initialize our state so that we're 
        // ready to receive additional requests... 
        message = new ProxyMessage(); 
        _totalBytesRead = 0; 
        _buffer = new byte[4]; //message length is 32-bit int (4 bytes) 
        _bufferState = BufferStates.GetMessageLength; 
       } 

       break; 
     } 

     // Wait for more data... 
     _tcpClient.GetStream().BeginRead(_buffer, 0, _buffer.Length, this.ReadCallback, message); 
    } 
    catch 
    { 
     // do nothing 
    } 

} 

Bisher mein einziger Gedanke ist, die Puffer-bezogenes in eine separate MessageBuffer Klasse zu extrahieren und einfach meine Lese Rückruf neue Bytes anhang müssen, wie sie ankommen. Die MessageBuffer würde sich dann über Dinge wie die aktuelle BufferState Gedanken machen und ein Ereignis auslösen, wenn sie eine komplette Nachricht erhalten hat, die dann weiter bis zum Haupt-Proxy-Server-Code propagieren könnte, wo die Anfrage bearbeitet werden kann.

+0

Sie haben nicht zufällig eine Open-Source-Version von dem, was Sie für diese Entwicklung entwickelt haben, würden Sie? – Maslow

Antwort

2

Ich hatte ähnliche Probleme zu überwinden.Hier ist meine Lösung (modifiziert, um Ihrem eigenen Beispiel zu entsprechen).

Wir erstellen einen Wrapper um Stream (eine Oberklasse von NetworkStream, die eine Oberklasse von TcpClient oder was auch immer ist). Es überwacht Lesevorgänge. Wenn einige Daten gelesen werden, ist es gepuffert. Wenn wir einen Längenindikator (4 Bytes) erhalten, prüfen wir, ob wir eine vollständige Nachricht haben (4 Bytes + Nachrichtenkörperlänge). Wenn wir dies tun, lösen wir ein MessageReceived Ereignis mit dem Nachrichtentext aus und entfernen die Nachricht aus dem Puffer. Diese Technik behandelt automatisch fragmentierte Nachrichten und Mehrfachnachrichten pro Paket.

public class MessageStream : IMessageStream, IDisposable 
{ 
    public MessageStream(Stream stream) 
    { 
     if(stream == null) 
      throw new ArgumentNullException("stream", "Stream must not be null"); 

     if(!stream.CanWrite || !stream.CanRead) 
      throw new ArgumentException("Stream must be readable and writable", "stream"); 

     this.Stream = stream; 
     this.readBuffer = new byte[512]; 
     messageBuffer = new List<byte>(); 
     stream.BeginRead(readBuffer, 0, readBuffer.Length, new AsyncCallback(ReadCallback), null); 
    } 

    // These belong to the ReadCallback thread only. 
    private byte[] readBuffer; 
    private List<byte> messageBuffer; 

    private void ReadCallback(IAsyncResult result) 
    { 
     int bytesRead = Stream.EndRead(result); 
     messageBuffer.AddRange(readBuffer.Take(bytesRead)); 

     if(messageBuffer.Count >= 4) 
     { 
      int length = BitConverter.ToInt32(messageBuffer.Take(4).ToArray(), 0); // 4 bytes per int32 

      // Keep buffering until we get a full message. 

      if(messageBuffer.Count >= length + 4) 
      { 
       messageBuffer.Skip(4); 
       OnMessageReceived(new MessageEventArgs(messageBuffer.Take(length))); 
       messageBuffer.Skip(length); 
      } 
     } 

     // FIXME below is kinda hacky (I don't know the proper way of doing things...) 

     // Don't bother reading again. We don't have stream access. 
     if(disposed) 
      return; 

     try 
     { 
      Stream.BeginRead(readBuffer, 0, readBuffer.Length, new AsyncCallback(ReadCallback), null); 
     } 
     catch(ObjectDisposedException) 
     { 
      // DO NOTHING 
      // Ends read loop. 
     } 
    } 

    public Stream Stream 
    { 
     get; 
     private set; 
    } 

    public event EventHandler<MessageEventArgs> MessageReceived; 

    protected virtual void OnMessageReceived(MessageEventArgs e) 
    { 
     var messageReceived = MessageReceived; 

     if(messageReceived != null) 
      messageReceived(this, e); 
    } 

    public virtual void SendMessage(Message message) 
    { 
     // Have fun ... 
    } 

    // Dispose stuff here 
} 
+1

Ich mochte alle Antworten hier, also +1 für alle anderen, die antworteten, aber am Ende ging ich mit etwas sehr ähnlich zu dieser Antwort. Es ist einfach und leicht zu verstehen, also werde ich mich daran erinnern, was ich in ein paar Monaten getan habe;) –

1

Ich denke, das Design, das Sie verwendet haben, ist in Ordnung, das ist ungefähr so, wie ich die gleiche Sache gemacht hätte und getan hätte. Ich glaube nicht, dass Sie durch das Refactoring in zusätzliche Klassen/Strukturen viel gewinnen würden, und aus dem, was ich gesehen habe, würden Sie die Lösung dadurch tatsächlich komplexer machen.

Der einzige Kommentar, den ich hätte, ist, ob die beiden lesen, wo der erste immer die Messgae Länge ist und der zweite immer der Körper ist robust genug. Ich bin immer vorsichtig bei solchen Ansätzen, als ob sie aufgrund eines unvorhergesehenen Umstandes (zB wenn das andere Ende die falsche Länge sendet) irgendwie nicht mehr synchron sind. Es ist sehr schwierig, es wiederherzustellen. Stattdessen würde ich einen einzelnen Lesevorgang mit einem großen Puffer durchführen, so dass ich immer alle verfügbaren Daten aus dem Netzwerk abrufen und dann den Puffer untersuchen würde, um vollständige Nachrichten zu extrahieren. Auf diese Weise kann der aktuelle Puffer einfach weggeworfen werden, um die Dinge in einen sauberen Zustand zu versetzen, und nur die aktuellen Nachrichten gehen verloren, anstatt den gesamten Dienst anzuhalten.

Eigentlich hätten Sie ein Problem, wenn Ihr Nachrichtentext groß war und in zwei separaten Empfängen angekommen ist und die nächste Nachricht in der Zeile gesendet wurde, ist die Länge gleichzeitig mit der zweiten Hälfte des vorherigen Nachrichtentextes. Wenn dies geschehen wäre, würde Ihre Nachrichtenlänge am Ende der vorherigen Nachricht angehängt werden und Sie wären in der Situation gewesen, wie sie im vorherigen Absatz beschrieben wurde.

+0

Hmm. Ich stimme zu, dass ein Kunde, der die falsche Länge sendet, die Dinge auf jeden Fall durcheinander bringt, da der Code geschrieben ist, und ich habe darüber debattiert, wie ich das am besten handhaben soll. Ich stehe mehr darauf, wenn der Client keine korrekt formatierte Nachricht sendet, dann schade. Müll rein, Müll raus ;-) –

+0

Guter Fang in deinem letzten Absatz. Wenn ich meinen Code noch einmal betrachte, denke ich, dass Sie Recht haben, wenn zwei Nachrichten sich möglicherweise überschneiden. Die Ironie ist, dass ich die Größe des Puffers änderte, um dieses Problem zu vermeiden, aber ich habe nicht darüber nachgedacht, was passieren würde, wenn eine große Nachricht vom Netzwerk geteilt würde. Oops –

+0

Eigentlich, nachdem ich darüber nachgedacht habe, denke ich, dass mein Ansatz immer noch funktioniert. Es ist durchaus möglich, dass der Socket-Puffer auf Betriebssystemebene das Ende einer Nachricht und den Anfang der nächsten enthält. BeginRead/EndRead garantieren jedoch, dass nur so viele Bytes gelesen werden, wie passen ... –

1

können Sie yield return verwenden, um die Erzeugung einer Zustandsmaschine für asynchrone Rückrufe zu automatisieren. Jeffrey Richter fördert diese Technik durch seine AsyncEnumerator Klasse, und ich habe mit der Idee here gespielt.

1

Es ist nichts falsch daran, wie Sie es gemacht haben. Für mich jedoch möchte ich das Empfangen der Daten von der Verarbeitung trennen, was Sie mit Ihrer vorgeschlagenen MessageBuffer-Klasse zu denken scheinen. Ich habe das ausführlich besprochen here.

+0

Ja, genau das habe ich mir gedacht: den Empfang von der eigentlichen Nachrichtenverarbeitungslogik trennen zu wollen. –

+0

Was ich in meiner Frage nicht erwähnt habe, ist, dass der Proxy-Server schließlich verschiedene Protokolle verarbeiten muss (er wird im Grunde Nachrichten in einem üblichen Proxy-Nachrichtenformat tunneln), also denke ich, dass ich einen guten Weg suchte, unterschiedliche Logik zu versenden basierend auf dem Protokoll, das getunnelt wird. –

Verwandte Themen