dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.99k stars 4.67k forks source link

WebSockets could be more abstracted #77060

Open alexandrehtrb opened 1 year ago

alexandrehtrb commented 1 year ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I recently began to learn WebSockets and the idea of bidirectional communication over HTTP, and I am liking it. I read the some examples online, such as this and managed to make a test server, using ASP.NET Core.

I noticed that writing code for WebSockets, at least in ASP.NET Core, is very "close to the metal". The developer needs to write code for receiving and sending the messages, translating them from byte arrays, worry about timeouts, and other considerations.

ASP.NET Core could have a friendlier programming approach to WebSockets, like Actions in Controllers, that exist for HTTP.

Describe the solution you'd like

I do not have a built idea, but I thought on an abstract class, like a "WebSocketController", with three abstract methods, for connecting, and for sending and receiving messages.

The send message method could return objects, that are converted to JSON strings and then to byte arrays, sent through the WebSocket. This conversion from object to JSON byte array would be done by the ASP.NET, taking this responsibility away from the developer.

The receive message method could have as parameters objects that are already pre-converted from the JSON received in the WebSocket message.

Additional context

No response

davidfowl commented 1 year ago

The problem with the idea of a controller is that it requires a way to dispatch messages to a individual actions. WebSockets is like TCP. If you do JSON over websockets then the best we can do is buffer the message and de-serialize it on your behalf. There's no dispatch because the JSON message itself isn't a specific protocol. When you do JSON over websockets, you need to build the dispatching mechanism yourself as there's no standard to do this. An API on the WebSocket type a read a JSON message might be enough for this simple use case though.

Then there's the idea of tracking connections in order to do broadcasts etc. We've already built this, it's called SignalR. It creates a protocol over WebSockets that allows dispatching like a controller. It also creates a model for sending messages to groups of connections.

alexandrehtrb commented 1 year ago

I read about SignalR and I liked its programming style, and how it decouples the bidirectional communication details.

However, I felt that SignalR is almost a protocol on its own, since it requires specific client-side libraries, not just a regular WebSocket communication. This StackOverflow question is related to this same issue.

If SignalR could act as an agnostic WebSocket server, it would be a solution for this.

Edit: The StackOverflow answer for the question says that SignalR could act like that, but I have not found the documentation for that; is it possible?

davidfowl commented 1 year ago

I think I clarified that in the previous comment, specifically:

There's no dispatch because the JSON message itself isn't a specific protocol. When you do JSON over websockets, you need to build the dispatching mechanism yourself as there's no standard to do this. An API on the WebSocket type a read a JSON message might be enough for this simple use case though.

davidfowl commented 1 year ago

Here are some ideas on what could be simplified:

As a strawman:

public abstract class WebSocketHandler 
{
    public virtual Task OnConnectedAsync();
    public virtual Task OnMessageAsync(WebSocketReceiveResult receiveResult);
    public virtual Task OnClosedAsync();
}

OR we could embrace a pull model with IAsyncEnumerable<WebSocketReceiveResult>. Of course there are questions about buffer ownership.

foreach (WebSocketReceiveResult result in webSocket.GetMessagesAsync())
{
    // Do something with the message
}

cc @BrennanConroy

BrennanConroy commented 1 year ago

I'm not sure how useful WebSocketHandler would actually be. It's a very thin wrapper around the existing WebSocket API. The main thing it looks like it would provide is the main loop and calling close async.

while (ws.Status == WebSocketState.Open)
{
    await ws.ReceiveAsync(...);
}
await ws.CloseAsync();

The user would still need to handle sending messages separately and closing the connection manually. I'm not sure how that would look with the abstraction since the abstraction tries to move the code to be callback based, but SendAsync/CloseAsync wouldn't want to be callback based.

And like you mention, the buffer ownership would likely be inefficient or easy to mishandle because the framework would need to assume the user will use the buffer outside of OnMessageAsync or assume the user knows they can't use it outside of that method.

  • Handling of the individual message buffering

Extension methods on WebSocket may be able to help here and would apply to both server and client.

e.g. ws.ReadTextMessageAsync() could read text frames until an end of message flag was received, and then give back something that combines the frames. And ws.ReadBinaryMessageAsync() would do the same but for binary messages. Those APIs would be really inefficient though, as you mentioned, how do we handle buffer ownership in an efficient way? But I think those APIs are a good potential path towards simplifying WebSocket consumption logic.

IAsyncEnumerable<WebSocketReceiveResult>

This is also interesting, it still leaves the user in control of the logic, but would handle combining frames.

gitlsl commented 1 year ago

https://devblogs.microsoft.com/dotnet/announcing-grpc-json-transcoding-for-dotnet may be this is what author want , signalr(websocket) -json-transcoding-for-dotnet

alexandrehtrb commented 1 year ago

There are two main points that I think could be improved:

1) The existing WebSocket and ClientWebSocket classes are quite difficult to work with if the programmer wants to do bidirectional communication. Taking from this example, if I understood it correctly, the code has to start both sending and receiving processes at the same time, check which comes first, then complete it; pseudo-code below:

while (true)
{
    var beganSending = HasMessageToSendAsync();
    var beganReceiving = HasMessageToReceiveAsync();

    var first = await Task.WhenAny(beganSending, beganReceiving);
    if (first == beganSending)
        await SendMessageAsync();
    else
        await ReceiveMessageAsync();
}

An event-based approach would be easier and simpler. Check the JavaScript WebSocket API, for this example:

socket = new WebSocket(connectionUrl.value);
socket.onopen = function (event) {
    updateState();
    /*code*/
};
socket.onclose = function (event) {
    updateState();
    /*code*/
};
socket.onerror = updateState;
socket.onmessage = function (event) {
    /*code*/
};

2) In ASP.NET Endpoints and Actions, the code returns an object that is automatically converted to a JSON and then to a byte array, sent through the connection. In current WebSocket classes, however, the programmer has to do all that - accumulate the received bytes, convert them to string, then convert this string to an object. That is quite cumbersome.

davidfowl commented 1 year ago

The existing WebSocket and ClientWebSocket classes are quite difficult to work with if the programmer wants to do bidirectional communication. Taking from this example, if I understood it correctly, the code has to start both sending and receiving processes at the same time, check which comes first, then complete it; pseudo-code below:

This example is more complex because we're introducing another layer of abstraction (the PipeReader/PipeWriter) instead of directly interacting with the WebSocket APIs. Notice in your examples the focus is on receiving, I don't see the same complaints about sending messages.

See this for the reason we're not a fan of the websocket APIs in the browser.

In ASP.NET Endpoints and Actions, the code returns an object that is automatically converted to a JSON and then to a byte array, sent through the connection.

This is easy because HTTP has 2 parts that are used separately, the path for routing (dispatch) and the body for the payload. We don't have the same with websockets, you only have the body.

In current WebSocket classes, however, the programmer has to do all that - accumulate the received bytes, convert them to string, then convert this string to an object. That is quite cumbersome.

byte[] -> object, there's no need to make a string even though you see lots of samples inefficiently doing that.

There are definitely some small things we can do here to improve the experience, the current API is very low level. However, I think this small improvement should be done for both client and server side websockets.

ASP.NET Core already has a higher-level programming model on top of any transport including WebSockets. I still think we should focus on APIs that aren't callback based that accomplish the following:

Out of scope:

adityamandaleeka commented 1 year ago

@davidfowl Should we open an issue to track this:

I still think we should focus on APIs that aren't callback based that accomplish the following:

  • Handling of the closing sequence
  • Handling of the individual message buffering

    • Translation of that message into a JSON object (of the user's choice)
davidfowl commented 1 year ago

Yes but I'm hoping we can make this an API proposal on the WebSocket API itself and avoid doing something on the server that doesnt' work on the client.

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/ncl See info in area-owners.md if you want to be subscribed.

Issue Details
### Is there an existing issue for this? - [X] I have searched the existing issues ### Is your feature request related to a problem? Please describe the problem. I recently began to learn WebSockets and the idea of bidirectional communication over HTTP, and I am liking it. I read the some examples online, such as [this](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-6.0) and managed to make a test server, using ASP.NET Core. I noticed that writing code for WebSockets, at least in ASP.NET Core, is very "close to the metal". The developer needs to write code for receiving and sending the messages, translating them from byte arrays, worry about timeouts, and other considerations. ASP.NET Core could have a friendlier programming approach to WebSockets, like Actions in Controllers, that exist for HTTP. ### Describe the solution you'd like I do not have a built idea, but I thought on an abstract class, like a "WebSocketController", with three abstract methods, for connecting, and for sending and receiving messages. The send message method could return objects, that are converted to JSON strings and then to byte arrays, sent through the WebSocket. This conversion from object to JSON byte array would be done by the ASP.NET, taking this responsibility away from the developer. The receive message method could have as parameters objects that are already pre-converted from the JSON received in the WebSocket message. ### Additional context _No response_
Author: alexandrehtrb
Assignees: -
Labels: `api-suggestion`, `area-System.Net`, `untriaged`
Milestone: -
CarnaViire commented 1 year ago

What exactly do you understand by "Handling of the closing sequence" @davidfowl?

davidfowl commented 1 year ago

@CarnaViire when close is received, sending the close frame back.

CarnaViire commented 1 year ago

Triage: easier APIs for WebSockets make sense, so we are open to adding them. For receiving, we should also add some setting like HttpClient.MaxResponseContentBufferSize to be able to limit the size of a reconstructed message.

Doesn't seem to be critical for 8.0, moving to Future.

If you need this feature, please upvote the top post, it will help us prioritize. Thanks!

davidfowl commented 1 year ago

cc @vicancy

vicancy commented 1 year ago

I would say this is an extremely important feature and could have huge benefits for customers using Azure Web PubSub service.

Azure Web PubSub service supports WebSocket clients connecting to the service directly.

However current native WebSocket APIs are a little bit complex for our customers, especially when compared with other programming languages, and we have to use some third-party WebSocket client packages in our C# samples to simplify the code.

It would be great if we have easier APIs for WebSocket in .NET world.

ygoe commented 1 year ago

A bit late to the (old) party ... but commenting on the second comment:

There's no dispatch because the JSON message itself isn't a specific protocol. When you do JSON over websockets, you need to build the dispatching mechanism yourself as there's no standard to do this.

I know one such standard and that is JSON-RPC. And WebSocket is actually an excellent transport for JSON-RPC as it has a message frame that TCP doesn't have. It saves you the JSON object splitting. I think this would be what the requester wanted. Other languages/frameworks like Flask/Python support this, albeit through extension packages (comparable to NuGet).

Personally I find SignalR too complex, for anything I can imagine. I feel like it was once built around a very specific need that nobody knows anymore. It has more features than a simple WebSocket dispatcher (hubs and groups), but at the same time makes simple WebSocket dispatching overly complex. And the library's code size is insane for what it does! Even a very basic solution like JSON-RPC would be easier here. I've also created my own solution that does PubSub+RPC over WebSocket for C# (router and client as NuGet package) and JavaScript (client only), in a simple and straightforward way with less code (currently not open-source yet).

davidfowl commented 1 year ago

@ygoe There are lots of JSON RPC packages available on NuGet https://www.nuget.org/packages?q=jsonrpc. I'm sure many of them work with ASP.NET Core. You can use one of those instead of SignalR if you don't need any of the features it offers.