I am out of ideas, how this may be implemented. Here is the problem, and what I have already tried.
Problem
There is a message queue with messages coming from the external system. A client must open websocket connection to the server and receive these messages from the queue as soon as they are inserted. In addition to that, the client may send a command to the server over the same websocket connection (the communication between the client and server must have as low latency as possible).
Tools
I prefer to use the lowest level, but still practical, official libraries, so the choice is Microsoft.AspNetCore.WebSockets (1.0.0). The app runs on Azure App Service as a web application with websockets (WSS) enabled. The application is netcoreapp1.1. There is no requirement of SSE/long polling/etc fallbacks (SignalR).
Solution #1 - already tested
The server should run a loop to:
That may be implemented like this:
while (_request_not_aborted_) // The condition is simplified.
{
    if (_there_is_a_new_message_in_message_queue_)
    {
        // Send all available messages from the message queue.
        await SendAsync();
    }
    using (var timeoutCancellationTokenSource = new CancellationTokenSource())
    {
        var timeoutCancellationToken = timeoutCancellationTokenSource.Token;
        var receiveTask = ReceiveAsync(timeoutCancellationToken, webSocket);
        // Wait for any incoming message for 1000ms and cancel the task if
        // the client hasn't sent any yet.
        if (await Task.WhenAny(receiveTask, Task.Delay(1000, timeoutCancellationToken)) ==
            receiveTask)
        {
            var result = await receiveTask;
            // Process the message.
        }
        timeoutCancellationTokenSource.Cancel();
    }
}
Solution #1 - what's wrong?
Once the timeoutCancellationTokenSource is canceled, the websocket connection enters "Aborted" state and the client is disconnected.
Possibly related issue: https://github.com/aspnet/WebSockets/issues/68
Solution #2 - already tested
Use SSE for streaming data from server to a client and use regular HTTPS request to send messages back.
Solution #2 - what's wrong?
The HTTPS request latency is unacceptable and SSE is not yet supported in IE/Edge.
Solution #3
Use multiple websocket connections, one for server->client and another for client->server communication.
Solution #3 - what's wrong?
But that would increase the chance of error of broken connection, and in general, sounds bad, because websocket technology should provide bi-directional communication.
Solution #4
Use Solution #2 with timeout logic removed, but send NOOP messages from the client to the server every 1000ms to allow the ReceiveAsync to complete normally.
Solution #4 - what's wrong?
Sounds like a nasty workaround.
Request for help/ideas
Maybe know any other solution how the server may listen for incoming messages and send messages to the client at relatively the same time?
Additional findings
Writing down the problem helps to see things from the other perspective, so I am answering my own question with the Solution #5. I haven't found any issues with it yet.
"Exactly one send and one receive is supported on each WebSocket object in parallel." drawn my attention and I decided to run both send and receive tasks at the same time keep restarting the completed one.
The loop should be:
var sendTask = SendAsync(cancellationToken, webSocket);
var receiveTask = ReceiveAsync(cancellationToken, webSocket);
while (!cancellationToken.IsCancellationRequested)
{
    if (await Task.WhenAny(sendTask, receiveTask) == sendTask)
    {
        // The server has finished sending to the client or it had nothing to send.
        await sendTask;
        sendTask = SendAsync(cancellationToken, webSocket);
        continue;
    }
    var message = await receiveTask;
    // TODO: Process message here
    receiveTask = ReceiveAsync(cancellationToken, webSocket);
}
await webSocket.CloseAsync(
    WebSocketCloseStatus.NormalClosure,
    string.Empty,
    cancellationToken);
And the SendAsync() method should contain:
while(_there_is_a_messages_in_queue_)
{     
    var data = "message"; // Simplified. Should be taken from the message queue.
    await webSocket.SendAsync(
        new System.ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes(data)),
        WebSocketMessageType.Text,
        true,
        cancellationToken);
}
await Task.Delay(1000);
Having a Delay task allows:
I am sure I might have left some edge cases uncovered, so if you will be reading this post in the future, let me know if you will see any obvious issue with this solution.
P.S. all ConfigureAwait(false) has been removed for better readability.
P.P.S. I am not accepting my own answer to allow you to add any useful information regarding any possible solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With