2009-03-15 2 views
5

J'ai récemment écrit un serveur proxy de preuve de concept rapide et sale en C# dans le cadre d'un effort pour obtenir une application web Java pour communiquer avec une application VB6 héritée résidant sur un autre serveur. C'est ridiculement simple:Y a-t-il des modèles bien connus pour le code réseau asynchrone en C#?

Le serveur proxy et les clients utilisent tous deux le même format de message; dans le code que j'utilise une classe ProxyMessage pour représenter les demandes des clients et des réponses générées par le serveur:

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

Les messages sont aussi simples que pourrait être: la longueur du corps + le corps du message.

J'ai une classe distincte ProxyClient qui représente une connexion à un client. Il gère toutes les interactions entre le proxy et un seul client. Je me demande si ce sont des modèles de conception ou des pratiques exemplaires pour simplifier le code standard associé à la programmation de sockets asynchrones? Par exemple, vous devez prendre soin de gérer le tampon de lecture de manière à ne pas perdre accidentellement d'octets, et vous devez suivre jusqu'à quel point vous êtes dans le traitement du message en cours. Dans mon code actuel, je fais tout ce travail dans ma fonction de rappel pour TcpClient.BeginRead, et gère l'état de la mémoire tampon et l'état actuel du traitement des messages à l'aide de quelques variables d'instance.

Le code de ma fonction de rappel que je transmets à BeginRead est ci-dessous, ainsi que les variables d'instance pertinentes pour le contexte. Le code semble fonctionner correctement "tel quel", mais je me demande s'il est possible de le refactoriser un peu pour le rendre plus clair (ou peut-être qu'il l'est déjà?).

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 
    } 

} 

Jusqu'à présent, ma seule pensée réelle est d'extraire la substance liée tampon dans une classe MessageBuffer séparée et tout simplement mon rappel de lecture d'ajouter de nouvelles octets à ce qu'ils arrivent. Le MessageBuffer s'inquiéterait alors de choses comme le BufferState courant et déclencherait un événement quand il recevrait un message complet, que le ProxyClient pourrait alors propager plus loin jusqu'au code principal de serveur de mandat, où la demande peut être traitée.

+0

Vous ne disposez pas d'une version open source de ce que vous avez développé pour cela? – Maslow

Répondre

2

J'ai dû surmonter des problèmes similaires.Voici ma solution (modifiée pour s'adapter à votre propre exemple).

Nous créons une enveloppe autour de Stream (une superclasse de NetworkStream, qui est une superclasse de TcpClient ou autre). Il surveille les lectures. Lorsque certaines données sont lues, elles sont mises en mémoire tampon. Lorsque nous recevons un indicateur de longueur (4 octets), nous vérifions si nous avons un message complet (4 octets + longueur du corps du message). Lorsque nous le faisons, nous soulevons un événement MessageReceived avec le corps du message et supprimons le message du tampon. Cette technique gère automatiquement les messages fragmentés et les situations à plusieurs messages par paquet.

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

J'ai aimé toutes les réponses ici, donc +1 pour tout le monde qui a répondu, mais à la fin je suis allé avec quelque chose de très similaire à cette réponse. C'est simple et facile à comprendre, donc je me souviendrai de ce que je faisais dans quelques mois;) –

1

Je pense que le design que vous avez utilisé est bon, c'est à peu près comme je l'aurais fait et j'ai fait le même genre de chose. Je ne pense pas que vous gagneriez beaucoup en refactorisant dans des classes/structs supplémentaires et de ce que j'ai vu vous rendrez la solution plus complexe en faisant ainsi.

Le seul commentaire que je ferais est de savoir si les deux lectures où le premier est toujours la longueur de messgae et la seconde étant toujours le corps est assez robuste. Je me méfie toujours des approches comme celles-ci, comme si elles se désynchronisaient en raison d'une circonstance imprévue (comme l'autre extrémité envoyant la mauvaise longueur), c'est très difficile à récupérer. Au lieu de cela, je ferais une seule lecture avec un grand tampon de sorte que je récupère toujours toutes les données disponibles sur le réseau, puis que j'inspecte le tampon pour extraire les messages complets. De cette façon, si les choses tournent mal, le tampon courant peut simplement être jeté pour remettre les choses dans un état propre et seuls les messages actuels sont perdus au lieu d'arrêter le service entier. En fait, pour le moment vous auriez un problème si votre corps de message était grand et arrivait dans deux cas séparés et que le prochain message en ligne envoyait sa longueur en même temps que la seconde moitié du corps précédent. Si cela arrivait, la longueur de votre message finirait par être ajoutée au corps du message précédent et vous auriez été dans la situation décrite dans le paragraphe précédent.

+0

Hmm. Je suis d'accord qu'un client envoyant la mauvaise longueur va certainement gâcher les choses compte tenu de la façon dont le code est écrit, et j'ai débattu de la meilleure façon de le gérer. Je penche plus vers si le client n'envoie pas un message correctement formaté, alors dommage. Garbage in, garbage out ;-) –

+0

Bonne prise dans votre dernier paragraphe. En regardant à nouveau mon code, je pense que vous avez raison à propos de deux messages qui se chevauchent potentiellement. L'ironie est que je redimensionnais le tampon pour éviter ce problème en premier lieu, mais je ne pensais pas à ce qui se passerait si un gros message était partagé par le réseau. Oups –

+0

En fait, après y avoir réfléchi un peu plus, je pense que mon approche fonctionne toujours. Il est parfaitement possible que le tampon socket au niveau de l'OS puisse contenir la fin d'un message et le début de la suivante; cependant, BeginRead/EndRead garantit de ne lire que le nombre d'octets ... –

1

Vous pouvez utiliser yield return pour automatiser la génération d'une machine d'état pour les callbacks asynchrones. Jeffrey Richter promeut cette technique à travers sa classe AsyncEnumerator, et j'ai joué avec l'idée here.

1

Il n'y a rien de mal dans la façon dont vous l'avez fait. Pour moi, cependant, j'aime séparer la réception des données de leur traitement, ce que vous semblez penser avec votre classe MessageBuffer proposée. J'en ai discuté en détail here.

+0

Oui, c'est exactement ce que je pensais: vouloir séparer la réception de la logique de traitement des messages. –

+0

Ce que je n'ai pas mentionné dans ma question, c'est que le serveur proxy devra éventuellement traiter différents protocoles (il va fondamentalement envoyer des messages dans un format de message proxy commun), donc je pense que je cherchais une bonne logique basé sur le protocole étant tunnelisé. –

Questions connexes