dotnet / runtime

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

No explanation or working code example for WebSocketClient? How are we supposed to use this? Is this a bug? #108695

Closed jonmdev closed 1 month ago

jonmdev commented 1 month ago

Description

The only documentation I see on WebSocketClient is here: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets

No working code is provided for setting up even the simplest WebSocket client configuration.

The best example I can find is here: https://stackoverflow.com/a/68284475/10305478

Which is also gibberish since it appears to continuously send random data to the server. I have tried my own configuration based on that as follows, but it does not work. I can send data but nothing is ever received back:

string url = "ws://localhost:4001/websocket";

Debug.WriteLine("START WEBSOCKET");
WebSocketClient.connectedEvent += async delegate {
    Debug.WriteLine("CONNECTED STATIC WEBSOCKET");
    await Task.Delay(1000);
Debug.WriteLine("DELAYED 1 second | " + WebSocketClient.webSocket.State.ToString() + " Is receive running? " + WebSocketClient.receiveRunning);
    //above line confirms receive loop is running and websocket is open

    await WebSocketClient.Send("ping");
    Debug.WriteLine("SENT PING TO STATIC WEBSOCKET"); //reaches here fine
};
WebSocketClient.Connect(url); 

Class:

public static class WebSocketClient {

    private static object consoleLock = new object();
    private const int sendChunkSize = 256;
    private const int receiveChunkSize = 1024 * 4;
    private const bool verbose = true;
    private static readonly TimeSpan delay = TimeSpan.FromMilliseconds(1000);
    public static event Action connectedEvent = null;

    public static ClientWebSocket webSocket;
    public static bool receiveRunning = false;

    public static async Task Connect(string uri) {
        webSocket = null;

        try {
            webSocket = new ClientWebSocket();
            await webSocket.ConnectAsync(new Uri(uri), CancellationToken.None);
            connectedEvent?.Invoke();
            Debug.WriteLine("CONNECTED EVENT DONE"); //reaches here okay
            await Receive();
            Debug.WriteLine("WEB SOCKET CLOSED"); //never reaches here
        }
        catch (Exception ex) {
            Debug.WriteLine("Exception: {0}", ex);
        }
        finally {
            if (webSocket != null) {
                webSocket.Dispose();

            }

            lock (consoleLock) {
                Debug.WriteLine("WebSocket closed.");
            }
        }
    }

    public static async Task Send(string message) {

        if (webSocket.State == WebSocketState.Open) {

            Debug.WriteLine("ABOUT TO SEND WEBSOCKET MESSAGE");
            await webSocket.SendAsync(Encoding.UTF8.GetBytes(message), System.Net.WebSockets.WebSocketMessageType.Text, false, CancellationToken.None);
            Debug.WriteLine("SENT MESSAGE"); //succeeds no problem
       }
    }

    private static async Task Receive() {
        Debug.WriteLine("START RECEIVE"); //reaches here
        while (webSocket.State == WebSocketState.Open) {
            receiveRunning = true;
            Debug.WriteLine("START WHILE LOOP");  //reaches here
            ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[receiveChunkSize]);
            var result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
            if (result.MessageType == WebSocketMessageType.Close) {
                await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
            }
            else {
                Debug.WriteLine("RECEIVED SOMETHING"); //NEVER GETS HERE, NOTHING IS EVER RECEIVED
                LogStatus(true, buffer.Array, result.Count);
            }
        }
        Debug.WriteLine("FINISHED RECEIVE"); //NEVER GETS HERE, STAYS IN LOOP ABOVE WAITING
    }

    private static void LogStatus(bool receiving, byte[] buffer, int length) {
        lock (consoleLock) {
            Debug.WriteLine("{0} {1} bytes... ", receiving ? "Received" : "Sent", length);

            if (verbose) {
                Debug.WriteLine(BitConverter.ToString(buffer, 0, length));

            }
        }
    }
}

In this case, I am able to successfully connect, and send. But nothing is ever received. I get to starting the while loop, but nothing is received by it and it never leaves this loop.

I can easily receive using WebSocketSharp (but this is abandoned) and Websocket.Client (which is a wrapper apparently of WebSocketClient), and I can also receive from Firefox console, so there is nothing wrong with the localhost server, but WebSocketClient just won't receive anything.

Is there any working documentation anywhere on the Internet for how we are supposed to implement WebSocketClient for a basic web socket client (ie. send text messages, receive messages back)?

Or what is wrong here? Is this a bug?

Reproduction Steps

Copy and paste the above code into a project with a localhost server configured to reply to "ping".

Expected behavior

Should get something back.

Actual behavior

No message is returned.

Regression?

No response

Known Workarounds

No response

Configuration

.NET 8 Windows

Other information

No response

dotnet-policy-service[bot] commented 1 month ago

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

jonmdev commented 1 month ago

I was able to get a basic case working with:

            Uri uri = new("ws://localhost:4001/websocket");

            Debug.WriteLine("START WEBSOCKET");
            ws = new();
            try {

                await ws.ConnectAsync(uri, default).ConfigureAwait(false);
                //Debug.WriteLine("STATE OF SOCKET| " + ws.State.ToString());
            }
            catch (Exception error) {
                //Debug.WriteLine("FAILED WEBSOCKET | " + error.ToString());
            }
            var receiveTask = Task.Run(async() => {
                var buffer = new byte[1024 * 4]; //whatever max expected size might be
                while (true) {
                    var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

                    if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close){
                        Debug.WriteLine("CLOSE WEBSOCKET MESSAGE");
                        break;
                    }
                    var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                    Debug.WriteLine("RECEIVED: " + message);
                }
            });
            //Debug.WriteLine("ABOUT TO SEND MESSAGE");
            await ws.SendAsync(Encoding.UTF8.GetBytes("ping"), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None);
            //Debug.WriteLine("SENT MESSAGE");

Looks like need to set the send third argument to "true" or it is never deemed finished on the server side. Will have to look at other issues beyond that.

CarnaViire commented 1 month ago

I agree that the WebSocket conceptual docs seem incomplete in this regard. Can you please open an issue in the docs repo https://github.com/dotnet/docs/ to add the full working example for sending and receiving messages?

Looks like need to set the send third argument to "true" or it is never deemed finished on the server side. Will have to look at other issues beyond that.

The "third argument" is the endOfMessage flag (see the API docs-system-net-websockets-websocketmessagetype-system-boolean-system-threading-cancellationtoken))) which corresponds to the FIN bit in the WebSocket protocol; until that bit set to 1, the fragmented message is incomplete by the protocol.

   FIN:  1 bit

      Indicates that this is the final fragment in a message.  The first
      fragment MAY also be the final fragment.

The way your localhost server reacts to the incomplete messages is up to the server implementation. I assume the server only replied after the message was received in its full. That's why you never got a response (I see that you were able to troubleshoot that yourself).

jonmdev commented 1 month ago

I agree that the WebSocket conceptual docs seem incomplete in this regard. Can you please open an issue in the docs repo https://github.com/dotnet/docs/ to add the full working example for sending and receiving messages?

Looks like need to set the send third argument to "true" or it is never deemed finished on the server side. Will have to look at other issues beyond that.

The "third argument" is the endOfMessage flag (see the API docs-system-net-websockets-websocketmessagetype-system-boolean-system-threading-cancellationtoken))) which corresponds to the FIN bit in the WebSocket protocol; until that bit set to 1, the fragmented message is incomplete by the protocol.

   FIN:  1 bit

      Indicates that this is the final fragment in a message.  The first
      fragment MAY also be the final fragment.

The way your localhost server reacts to the incomplete messages is up to the server implementation. I assume the server only replied after the message was received in its full. That's why you never got a response (I see that you were able to troubleshoot that yourself).

Thanks. I shared my working basic code example (so far) and posted the request here:

https://github.com/dotnet/docs/issues/43001

I would like to see it "fixed" to add other types of events like disconnection, message received, message sent, etc. and some way for handling re-connection attempts. I explained all that over there.