2009-03-15 8 views
5

Recentemente ho scritto un server proxy proof-of-concept veloce e sporco in C# come parte di uno sforzo per ottenere un'applicazione Web Java per comunicare con un'applicazione VB6 legacy che risiede su un altro server. È ridicolmente semplice:Esistono modelli ben noti per il codice di rete asincrono in C#?

Sia il server proxy che i client utilizzano lo stesso formato di messaggio; nel codice che uso una classe ProxyMessage per rappresentare entrambe le richieste da parte dei clienti e le risposte generati dal server:

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) { ... } 
} 

I messaggi sono semplici come potrebbe essere: la lunghezza del corpo del messaggio corpo +.

Ho una classe separata ProxyClient che rappresenta una connessione a un client. Gestisce tutte le interazioni tra il proxy e un singolo client.

Quello che mi chiedo sono sono schemi di progettazione o best practice per la semplificazione del codice boilerplate associato alla programmazione di socket asincroni? Ad esempio, è necessario fare molta attenzione a gestire il buffer di lettura in modo da non perdere accidentalmente byte, e occorre tenere traccia di quanto tempo ci si trova nell'elaborazione del messaggio corrente. Nel mio codice corrente, eseguo tutto questo lavoro nella mia funzione di callback per TcpClient.BeginRead e gestisco lo stato del buffer e lo stato di elaborazione del messaggio corrente con l'aiuto di alcune variabili di istanza.

Il codice per la funzione di richiamata che sto passando a BeginRead è al di sotto, insieme alle variabili di istanza rilevanti per il contesto. Il codice sembra funzionare bene "così com'è", ma mi chiedo se può essere rifattorizzato un po 'per renderlo più chiaro (o forse lo è già?).

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 
    } 

} 

Finora, il mio unico pensiero è vera per estrarre il materiale tampone legati in una classe separata MessageBuffer e semplicemente la mia lettura di callback aggiungere i nuovi byte ad esso man mano che arrivano. Lo MessageBuffer si preoccuperebbe quindi di cose come l'attuale BufferState e attiverebbe un evento quando riceveva un messaggio completo, che lo ProxyClient poteva quindi propagare ulteriormente fino al codice del server proxy principale, dove la richiesta può essere elaborata.

+0

non si dispone di una versione open source di ciò che si sta sviluppando per questo disponibile, vero? – Maslow

risposta

2

Ho dovuto superare problemi simili.Ecco la mia soluzione (modificata per adattarsi al tuo esempio).

Creiamo un wrapper attorno a Stream (una superclasse di NetworkStream, che è una superclasse di TcpClient o qualsiasi altra cosa). Controlla le letture. Quando alcuni dati vengono letti, vengono memorizzati nel buffer. Quando riceviamo un indicatore di lunghezza (4 byte) controlliamo se abbiamo un messaggio completo (4 byte + lunghezza del corpo del messaggio). Quando lo facciamo, solleviamo un evento MessageReceived con il corpo del messaggio e rimuoviamo il messaggio dal buffer. Questa tecnica gestisce automaticamente i messaggi frammentati e le situazioni a più messaggi per pacchetto.

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

Mi sono piaciute tutte le risposte qui, quindi +1 per tutti gli altri che hanno risposto, ma alla fine sono andato con qualcosa di molto simile a questa risposta. È semplice e facile da capire, quindi mi ricorderò di quello che stavo facendo tra qualche mese;) –

1

Penso che il design che hai usato sia bello, è più o meno come avrei fatto e ho fatto lo stesso genere di cose. Non penso che guadagneresti molto rifattorizzando in classi/strutture aggiuntive e da quello che ho visto, in effetti, renderebbe la soluzione più complessa.

L'unico commento che vorrei è se le due letture in cui la prima sia sempre la lunghezza del messge e la seconda sia sempre il corpo è abbastanza robusto. Sono sempre diffidente nei confronti di approcci come quello, come se in qualche modo venissero fuori sincrono a causa di una circostanza imprevista (come l'altra estremità che invia la lunghezza sbagliata) è molto difficile da recuperare. Invece farei una singola lettura con un grande buffer in modo da ottenere sempre tutti i dati disponibili dalla rete e quindi ispezionare il buffer per estrarre i messaggi completi. In questo modo, se le cose vanno male, il buffer corrente può essere semplicemente gettato via per riportare le cose allo stato pulito e solo i messaggi correnti vengono persi piuttosto che fermare l'intero servizio.

In questo momento si avrebbe un problema se il messaggio del corpo era grande e arrivato in due ricevitori separati e il messaggio successivo in linea ha inviato la sua lunghezza allo stesso tempo della seconda metà del corpo precedente. Se ciò accadesse, la lunghezza del messaggio verrebbe aggiunta al corpo del messaggio precedente e ti saresti trovato nella situazione come descritto nel paragrafo precedente.

+0

Hmm. Sono d'accordo sul fatto che un client che invia la lunghezza sbagliata rovinerà sicuramente le cose dato il modo in cui il codice è stato scritto, e ho discusso sul modo migliore per gestirlo. Mi sto orientando più verso se il client non sta inviando un messaggio formattato correttamente, quindi troppo male. Immondizia, spazzatura ;-) –

+0

Buona cattura nel tuo ultimo paragrafo. Guardando di nuovo il mio codice, penso che tu abbia ragione su due messaggi potenzialmente sovrapposti. L'ironia è che stavo ridimensionando il buffer per evitare quel problema in primo luogo, ma non ho pensato a cosa sarebbe successo se un grosso messaggio fosse stato diviso dalla rete. Oops –

+0

In realtà, dopo averci pensato un po 'di più, penso che il mio approccio funzioni ancora. È perfettamente possibile che il buffer del socket a livello di sistema operativo possa contenere la fine di un messaggio e l'inizio del successivo; tuttavia, BeginRead/EndRead garantisce di leggere solo il numero di byte che si adatta a ... –

1

È possibile utilizzare yield return per automatizzare la generazione di una macchina a stati per callback asincroni. Jeffrey Richter promuove questa tecnica attraverso la sua classe AsyncEnumerator e ho giocato con l'idea here.

1

Non c'è niente di sbagliato nel modo in cui l'hai fatto. Per quanto mi riguarda, però, mi piace separare la ricezione dei dati dall'elaborazione di questo, che è ciò che sembra pensare con la tua classe MessageBuffer proposta. Ne ho parlato nel dettaglio here.

+0

Sì, questo è esattamente quello che stavo pensando: voler separare la ricezione dalla logica di elaborazione dei messaggi effettiva. –

+0

Ciò che non ho menzionato nella mia domanda è che il server proxy dovrà eventualmente elaborare protocolli diversi (fondamentalmente farà il tunnel dei messaggi in un formato di messaggio proxy comune), quindi suppongo che stavo cercando un buon modo per inviare una logica diversa basato sul protocollo che viene scavato nel tunnel. –

Problemi correlati