检测"忙碌"的WebSocket

时间:2016-04-26 22:35:56

标签: c# websocket aspnet-websocket

我最近遇到了一个问题,尝试使用SendAsync方法广播到开放的WebSocket,接收带有消息&#34的InvalidOperationException;发送操作已在进行中"

深入研究WebSocket类的源代码,内部跟踪繁忙状态:

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
}

理想情况下,如果我要将更新广播到WebSocket连接,我想提前知道它忙碌并执行其他一些处理(例如对消息进行排队)

这个状态被标记为内部似乎很奇怪 - 它是一个公共财产我可以简单地检查

context.WebSocket.SendState == ChannelState.Ready

WebSocket上SendAsync的正确模式是什么,可以防止抛出此异常?

我不愿通过反思破解对该财产的访问。

编辑以澄清:

WebSocket.State属性不会帮助这种情况。该属性使用此枚举:

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

打开套接字连接后,此语句将评估为" true"无论是否忙于发送:

context.WebSocket.State == WebSocketState.Open

1 个答案:

答案 0 :(得分:1)

我已经实施了一个似乎可以满足我需求的解决方案。

基本问题是当两个线程试图在同一个WebSocket上发送时。

我的解决方案有一些部分,并且取决于它在AspNetWebSocketContect下运行的事实,因此我可以使用“Items”字典来存储有关当前连接的属性。

  1. “发送”属性用于跟踪WebSocket是否正忙。
  2. “queue”属性是要发送的ArraySegments列表。
  3. 锁定WebSocket对象并同步运行Send方法。
  4. 发送完成后处理队列中的所有项目。
  5. 防止阻塞的方法开始时的收益。
  6. 这是我目前在开发环境中使用的代码 - 我会监视它的扩展程度:

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

    我对这种方法的一些考虑因素:

    1. 我认为最好锁定一个简单的对象,而不是像我上面那样更复杂的对象。我不确定使用“context”和“context.WebSocket”对象进行锁定的后果是什么。
    2. 从理论上讲,我不需要锁定WebSocket,因为我已经在测试“发送”属性了。但是,测试导致WebSocket在重负载的几秒钟内无响应。一旦我实现了锁定模式,这就消失了。
    3. 我通过同一个WebSocket连接对10个并发线程(每个发送1000条消息)进行了测试。每条消息都已编号,因此我可以将其与客户端一起记录下来,并且每个消息都通过了。所以队列系统似乎有效。