Home > Software engineering >  WebSocket simplex stream - how can the client close the connection?
WebSocket simplex stream - how can the client close the connection?

Time:11-17

I'm using WebSockets to stream values from server to client. The connection should be closed when the stream of values is completed (server-side termination), or when the client stops subscribing (client-side termination).

How can the client gracefully close the connection?

A rough sample to demonstrate the issue in AspNetCore; server pushes a (potentially) infinite stream of values; client subscribes to the first 5 values, and should then close the connection.

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/read")
    {
        var client = new ClientWebSocket();
        await client.ConnectAsync(new Uri(@"wss://localhost:7125/write"), CancellationToken.None);

        for (int i = 0; i < 5; i  )
            await client.ReceiveAsync(new ArraySegment<byte>(new byte[sizeof(int)]), CancellationToken.None);
        
        // This does not seem to have any particular effect on writer
        // await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
        
        // This freezes in AspNetcore on .NET6 core (because the implementation waits for the connection to close, which never happens)
        // In AspNet (non-core, .Net Framework 4.8), this seems to throw an exception that data cannot be read after the connection has been closed (i.e. the socket seems to only be closeable if no data is pending to be read)
        await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
    }

    if (context.Request.Path == "/write")
    {
        var ws = await context.WebSockets.AcceptWebSocketAsync();
            
        await foreach (var number in GetNumbers())
        {
            var bytes = BitConverter.GetBytes(number);
                
            if (ws.State != WebSocketState.Open)
                throw new Exception("I wish we'd hit this branch, but we never do!");
                
            await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, true, CancellationToken.None);
        }
    }
});

static async IAsyncEnumerable<int> GetNumbers()
{
    for (int i = 0; i <= int.MaxValue; i  )
    {
        yield return i;
        await Task.Delay(25);
    }
}

The general issue seems to be that the close message isn't picked up by the /write method, i.e. ws.State remains WebSocketState.Open. I'm assuming that onle receive operations update the connection status?

Is there any good way to handle this situation / for the server to pick up the client's request to close the connection?

I would quite like to avoid the client having to send any explicit messages to the server / for the server to have to read the stream explicitly. I'm increasingly wondering if that is possible, though.

CodePudding user response:

Way the WebSocket protocol works is similar to TCP - via connection establishment, the only difference - initiation is done via http[s]. One send action from one side matches one receive action from another, and vice versa.
You can notice this detail (if i am not mistaken) in remarks of documentation:

Exactly one send and one receive is supported on each WebSocket object in parallel.

So, you should receive at least one data segment and recognize CloseIntention message from client. And the same on client side.

How to receive message, recognize close intention, and properly react to it - see here.
How to send close intention message - see here.

Suspect you should call webSocket.ReceiveAsync at least once in background on your server. Then, in ContinueWith task call CancellationTokenSource.Cancel() for current server socket session.
That repo is working, except of docker-compose. - I am kinda newcomer in complex DevOps things. )

UPDATE

Remarks part of docs is not about matching of send-receive actions on different sides of conversation. Just wanted you to notice how this TCP-concept works, i.e you should receive data at least once.

  • Related