unosquare / embedio

A tiny, cross-platform, module based web server for .NET
http://unosquare.github.io/embedio
Other
1.45k stars 175 forks source link

Refactor WebSockets support #547

Open rdeago opened 2 years ago

rdeago commented 2 years ago

Part of the ongoing work on EmbedIO v4.0, as documented in #546, is a near-complete refactor of WebSockets support.

Easier reception callbacks with automatic edge-case handling

Up to EmbedIO v3, in order to subclass WebSocketModule you had to override this method:

/// <summary>
/// Called when this WebSocket server receives a full message (EndOfMessage) from a client.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="result">The result.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
protected abstract Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result);

In the overridden method, first you would first have to assess the type of data received. Then, in the case of textual data, you had to convert buffer to a string. Furthermore, the result parameter was quite confusing: apparently you could receive a partial message, or even a close request. OK, you couldn't actually, but then why did you need an IWebSocketReceiveResult interface where a bool (or an enum with two possible values) was enough?

Now you have to override one or both of these methods instead:

/// <summary>
/// Called when this WebSocket server receives a full message (EndOfMessage) from a client.
/// </summary>
/// <param name="context">An instance of <see cref="IWebSocketContext"/>.</param>
/// <param name="text">The text contained in the message.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
protected virtual Task OnTextMessageReceivedAsync(IWebSocketContext context, string text)
    => throw new WebSocketException(CloseStatusCode.UnsupportedData, "This endpoint does not accept text messages.");

/// <summary>
/// Called when this WebSocket server receives a full message (EndOfMessage) from a client.
/// </summary>
/// <param name="context">An instance of <see cref="IWebSocketContext"/>.</param>
/// <param name="data">The binary data contained in the message.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
protected virtual Task OnBinaryMessageReceivedAsync(IWebSocketContext context, byte[] data)
    => throw new WebSocketException(CloseStatusCode.UnsupportedData, "This endpoint does not accept binary messages.");

Textual data is converted to string for you; the connection is closed with an appropriate status code, as per RFC6455, if the received data is not valid UTF-8 text.

If you override only one of the two methods above, the reception of a kind of data you don't expect causes the connection to be closed, as per RFC6455, with a status code of 1003 (the value of CloseStatusCode.UnsupportedData).

The "frame" reception callback has been removed

Method WebSocketModule.OnFrameReceivedAsync has been removed because it complicated message reception code to support a feature that hardly any application needs. WebSocket applications should never rely on internals details of the protocol implementation anyway: the unit of data exchanged on a WebSocket is the message, not the frame.

In addition, OnFrameReceivedAsync had never worked as advertised anyway when using HttpListenerMode.Microsoft. Their WebSocket class does not yield single frames; instead, it aggregates them into complete messages and may then yield partial messages according to the size of the buffer allocated for reception.

WebSocketException now goes both ways

WebSocketException used to be thrown by EmbedIO to signal an anomalous condition or the attempt to perform an operation on a WebSocket that was not supported (typically writing to a closed socket).

Now you may also throw a WebSocketException from a callback method (OnBinaryMessageReceivedAsync, OnTextMessageReceivedAsync, OnClientConnectedAsync) to command the connection to be closed. This lets you write simpler code, as previously you had to call Close and return.

Internal changes

Code has been optimized to perform as few allocations as possible.

Support for data compression in WebSockets (not part of RFC6455) has been removed. It had never been enabled anyway.

The message reception loop has been unified between HTTP listener modes. One consequence is the creation of one less parallel Task for each WebSocket connection.


Comments are very welcome. Expect to be able to try the modified WebSocket support in v4.0-preview.1 soon.