2016-04-26 5 views
1

ich vor kurzem in ein Problem lief mit dem Versuch, zu einem offenen WebSocket mit der SendAsync Methode auszustrahlen, eine InvalidOperationException mit der Meldung „Ein Sendevorgang läuft bereits“ Empfangein „Besetzt“ WebSocket Erkennen

die Graben durch Quellcode für die WebSocket-Klasse, ein Belegtzustand wird intern verfolgt:

internal ChannelState _sendState; 

// Represents the state of a single channel; send and receive have their own states 
internal enum ChannelState { 
    Ready, // this channel is available for transmitting new frames 
    Busy, // the channel is already busy transmitting frames 
    Closed // this channel has been closed 
} 

Idealfall, wenn ich über ein Update auf eine WebSocket-Verbindung zu übertragen, ich möchte im voraus wissen, dass es voll ist und Führen Sie eine andere Behandlung durch (z. B. die Nachricht in die Warteschlange stellen).

Es scheint seltsam, dass dieser Zustand als internes markiert ist - war es ein öffentliches Eigentum ich einfach für

context.WebSocket.SendState == ChannelState.Ready 

Was das richtige Muster auf einem WebSocket zu SendAsync ist überprüfen könnte, die diese Ausnahme zu werfen verhindern?

Ich hasse es, den Zugang zu dieser Eigenschaft durch Reflexion zu hacken.

bearbeiten zu klären:

Die WebSocket.State Eigenschaft diese Situation nicht helfen. Die Eigenschaft verwendet diese Aufzählung:

public enum WebSocketState 
{ 
    None, 
    Connecting, 
    Open, 
    CloseSent, 
    CloseReceived, 
    Closed, 
    Aborted 
} 

Sobald der Socket-Verbindung geöffnet wurde, wertet diese Aussage auf „true“, unabhängig davon, ob es besetzt zu senden:

context.WebSocket.State == WebSocketState.Open 
+0

Verwenden Sie einen 'AspNetWebSocket'? Wenn ja, hat es eine "State" -Eigenschaft, die Sie überprüfen können. https://msdn.microsoft.com/en-us/library/system.web.websockets.aspnetwebsocket.state(v=vs.110).aspx – dharms

+0

Leider ändert sich die State-Eigenschaft nicht, wenn der Socket "beschäftigt" ist - Es bleibt "offen". Wenn Sie versuchen, während der Besetztphase ein "SendAsync" zu initiieren, wird eine Ausnahme ausgelöst. –

+0

Ändern Sie möglicherweise Ihren Code so, dass nur ein Thread die Sendevorgänge ausführt. Dieser Prozess kann gegen eine Warteschlange funktionieren, die von Ihrer Anwendung ausgefüllt wird, wenn sie eine Nachricht senden möchte. – dharms

Antwort

0

I implementiert haben eine Lösung, die für meine Bedürfnisse zu funktionieren scheint.

Das grundlegende Problem ist, wenn zwei Threads versuchen, auf demselben WebSocket zu senden.

Meine Lösung hat ein paar Teile und hängt davon ab, dass dies unter dem AspNetWebSocketContect ausgeführt wird, so dass ich das Wörterbuch "Items" verwenden kann, um Eigenschaften über die aktuelle Verbindung zu speichern.

  1. Eine "sendende" Eigenschaft wird verwendet, um zu verfolgen, ob der WebSocket aktiv ist.
  2. Eine Eigenschaft "queue" ist eine Liste der zu sendenden ArraySegments.
  3. Sperren Sie das WebSocket-Objekt und führen Sie die Send-Methode synchron aus.
  4. Behandeln Sie alle Elemente in der Warteschlange, sobald der Sendevorgang abgeschlossen ist.
  5. Ausbeute am Anfang der Methode, um Blockierung zu verhindern.

Dies ist der Code, den ich zur Zeit in einem Entwickler-Umgebung bin - ich werde sehen überwachen, wie gut es skaliert:

/// <summary> 
/// Send a message to a specific client. 
/// </summary> 
/// <param name="context"></param> 
/// <param name="buffer"></param> 
/// <returns></returns> 
private static async Task SendMessage(AspNetWebSocketContext context, ArraySegment<byte> buffer) 
{ 
    // Return control to the calling method immediately. 
    await Task.Yield(); 

    // Make sure we have data. 
    if (buffer.Count == 0) 
     return; 

    // The state of the connection is contained in the context Items dictionary. 
    bool sending; 
    lock (context) 
    { 
     // Are we already in the middle of a send? 
     sending = (bool)context.Items["sending"]; 

     // If not, we are now. 
     if (!sending) 
      context.Items["sending"] = true; 
    } 

    if (!sending) 
    { 
     // Lock with a timeout, just in case. 
     if (!Monitor.TryEnter(context.WebSocket, 1000)) 
     { 
      // If we couldn't obtain exclusive access to the socket in one second, something is wrong. 
      await context.WebSocket.CloseAsync(WebSocketCloseStatus.InternalServerError, string.Empty, CancellationToken.None); 
      return; 
     } 

     try 
     { 
      // Send the message synchronously. 
      var t = context.WebSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); 
      t.Wait(); 
     } 
     finally 
     { 
      Monitor.Exit(context.WebSocket); 
     } 

     // Note that we've finished sending. 
     lock (context) 
     { 
      context.Items["sending"] = false; 
     } 

     // Handle any queued messages. 
     await HandleQueue(context); 
    } 
    else 
    { 
     // Add the message to the queue. 
     lock (context) 
     { 
      var queue = context.Items["queue"] as List<ArraySegment<byte>>; 
      if (queue == null) 
       context.Items["queue"] = queue = new List<ArraySegment<byte>>(); 
      queue.Add(buffer); 
     } 
    } 
} 

/// <summary> 
/// If there was a message in the queue for this particular web socket connection, send it. 
/// </summary> 
/// <param name="context"></param> 
/// <returns></returns> 
private static async Task HandleQueue(AspNetWebSocketContext context) 
{ 
    var buffer = new ArraySegment<byte>(); 
    lock (context) 
    { 
     // Check for an item in the queue. 
     var queue = context.Items["queue"] as List<ArraySegment<byte>>; 
     if (queue != null && queue.Count > 0) 
     { 
      // Pull it off the top. 
      buffer = queue[0]; 
      queue.RemoveAt(0); 
     } 
    } 

    // Send that message. 
    if (buffer.Count > 0) 
     await SendMessage(context, buffer); 
} 

Einige Überlegungen ich auf diesem Ansatz haben:

  1. Ich glaube, es ist besser, ein einfaches Objekt anstelle eines komplexeren Objekts zu sperren, wie ich oben mache. Ich bin mir nicht sicher, welche Konsequenzen die Verwendung der Objekte "context" und "context.WebSocket" für das Sperren haben.
  2. Theoretisch sollte ich den WebSocket nicht sperren müssen, da ich bereits die "sendende" Eigenschaft teste. Tests führten jedoch dazu, dass der WebSocket nach einigen Sekunden schwerer Last nicht mehr reagierte. Das ging weg, nachdem ich das Sperrmuster implementiert hatte.
  3. Ich habe einen Test mit 10 gleichzeitigen Threads (Senden von jeweils 1000 Nachrichten) über die gleiche WebSocket-Verbindung. Jede Nachricht wurde nummeriert, damit ich sie mit dem Client protokollieren konnte, und jeder hat es geschafft. Das Warteschlangensystem scheint also zu funktionieren.