Skip to Content
Course content

73: Working with WebSockets in .NET

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time building dashboards, and there's a specific kind of frustration that comes with "polling." You know the drill: the client asks the server every two seconds, "Do you have new data yet?" Most of the time, the server says "No," and you've just wasted a request and a bit of battery life. For a live system health monitor—where we want to see CPU spikes the moment they happen—polling is just clunky.

The frustration with Request-Response

I started by trying to build a simple resource monitor using a standard API endpoint. I had the client hitting /api/health every second. It worked, but the network tab in my browser looked like a strobe light. I wanted a persistent pipe where the server could just push a message whenever the CPU hit 90%. That's where WebSockets come in. Unlike HTTP, which is a one-way street (client asks, server answers), a WebSocket is a two-way tunnel that stays open.

First, I had to tell the .NET middleware to actually allow these kinds of connections. I added this to my Program.cs:

app.UseWebSockets();

Simple enough. But then I hit my first wall. I tried to treat the WebSocket connection like a regular controller action, expecting it to just "return" a value. I quickly realized that a WebSocket isn't a request; it's a handover. You aren't returning a result; you're hijacking the connection to keep it open.

Opening the Pipe

I decided to write a small piece of middleware to handle the "handshake." I wanted to check if the incoming request was actually asking for a WebSocket. If it was, I'd accept it and enter a loop. Here is how I tackled the initial connection logic:

app.Use(async (context, next) => 
{
    if (context.WebSockets.IsWebSocketRequest)
    {
        using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
        // I'm now "inside" the tunnel. 
        // If I exit this method, the connection closes.
        await HandleMonitorConnection(webSocket);
    }
    else
    {
        await next();
    }
});

The AcceptWebSocketAsync call is the magic moment. It upgrades the HTTP connection to a WebSocket connection. But here's the catch: if HandleMonitorConnection returns, the connection dies. To keep the monitor alive, I have to keep the code execution trapped in a loop as long as the socket is open.

Wrestling with Byte Buffers

This is where things got messy. WebSockets don't speak "Strings" or "JSON" natively; they speak bytes. I tried to just send a string, but the SendAsync method demanded an ArraySegment<byte>. It felt like I was stepping back into 1995.

I wrote a helper to push my CPU metrics. I noticed that if I didn't manage the buffer correctly, I was allocating memory like crazy every time I sent a packet. I shifted to using Encoding.UTF8.GetBytes and wrapping it in an ArraySegment. Here's the logic I settled on for pushing data:

async Task HandleMonitorConnection(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    
    while (webSocket.State == WebSocketState.Open)
    {
        // Simulating a CPU metric read
        string metric = $"CPU: {Random.Shared.Next(10, 90)}% | Mem: {Random.Shared.Next(2000, 8000)}MB";
        var bytes = Encoding.UTF8.GetBytes(metric);
        
        await webSocket.SendAsync(
            new ArraySegment<byte>(bytes), 
            WebSocketMessageType.Text, 
            true, 
            CancellationToken.None);

        await Task.Delay(1000); // Push every second
    }
}

One thing I noticed during testing: if I closed the browser tab, the server would throw an exception on the next SendAsync call. The server doesn't magically know the client is gone until it tries to send something and fails. I had to wrap the loop in a try-catch block specifically for WebSocketException to ensure the server cleaned up the resources gracefully without crashing the whole process.

Handling the Disconnect

I realized that just catching exceptions isn't "clean." A proper WebSocket implementation should handle the close handshake. The client sends a "Close" frame, and the server should acknowledge it. I adjusted my loop to check for ReceiveAsync. Even if the server is the one pushing data, it still needs to "listen" for the client's request to close the connection.

By adding a ReceiveAsync call in a separate Task or as part of the loop, I could detect when result.MessageType == WebSocketMessageType.Close. Only then did I call webSocket.CloseAsync. This prevents those annoying "Connection reset by peer" errors in the logs and makes the teardown smooth.




📋 Practical Task

Build a Real-Time System Resource Tracker

Your goal is to implement a WebSocket handler that mimics a system monitor. Instead of just random numbers, you will create a service that pushes the actual current memory usage of the application to a connected client.

  • The Setup: Create a .NET Minimal API project and enable WebSockets.
  • The Logic: Create a middleware or a specific route that accepts a WebSocket connection.
  • The Data: Use GC.GetTotalMemory(false) to get the current memory usage of the app.
  • The Loop: Every 500ms, send the memory usage (formatted as a string) to the client.
  • The Safety: Implement a try-catch block to handle WebSocketException so that if the client disconnects abruptly, the server logs "Client disconnected" instead of crashing.
  • The Handshake: Ensure you use webSocket.CloseAsync when the client sends a close frame.

Test your implementation using a browser-based WebSocket client (like a Chrome extension or a simple JS snippet in the console) to verify that the memory numbers are updating in real-time.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.