C# tutorials > Input/Output (I/O) and Networking > .NET Networking > Working with WebSockets (`ClientWebSocket`, `WebSocket`)

Working with WebSockets (`ClientWebSocket`, `WebSocket`)

This tutorial explores how to use WebSockets in C# with `ClientWebSocket` for clients and `WebSocket` (primarily within ASP.NET Core) for servers. WebSockets provide full-duplex communication channels over a single TCP connection, enabling real-time data exchange between client and server.

Introduction to WebSockets

What are WebSockets?
WebSockets are a communication protocol that provides full-duplex communication channels over a single TCP connection. Unlike HTTP, which is request-response based, WebSockets allow for persistent connections where both client and server can send data at any time.

Why use WebSockets?
WebSockets are ideal for real-time applications such as chat applications, online games, financial tickers, and live dashboards where low latency and continuous data flow are crucial.

Client-Side WebSocket with `ClientWebSocket`

This code snippet demonstrates how to create a WebSocket client using `ClientWebSocket`.

Explanation:

  1. The `ConnectAndReceive` method establishes a connection to the WebSocket server at the provided URI.
  2. The `Receive` method continuously listens for incoming messages from the server. It decodes the received bytes into a string and prints it to the console. It also handles the closing of the connection.
  3. The `Send` method sends a string message to the WebSocket server. It converts the string into a byte array before sending.
  4. The `Main` method creates a URI object for the WebSocket server and calls the `ConnectAndReceive` method to start the connection and message processing. Important: You will need to replace `ws://localhost:5000/ws` with the actual URL of your WebSocket server.

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public class WebSocketClient
{
    public static async Task ConnectAndReceive(Uri wsUri)
    {
        using (ClientWebSocket ws = new ClientWebSocket())
        {
            try
            {
                await ws.ConnectAsync(wsUri, CancellationToken.None);
                Console.WriteLine("Connected to: " + wsUri);

                await Receive(ws);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception: {0}", ex);
            }
            finally
            {
                Console.WriteLine("Closing...");
                if (ws.State == WebSocketState.Open)
                {
                    await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                }
            }
        }
    }

    private static async Task Receive(ClientWebSocket ws)
    {
        byte[] buffer = new byte[1024 * 4];

        while (ws.State == WebSocketState.Open)
        {
            WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

            if (result.MessageType == WebSocketMessageType.Close)
            {
                await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by server", CancellationToken.None);
                break;
            }
            else
            {
                Console.WriteLine("Received: " + Encoding.UTF8.GetString(buffer, 0, result.Count));
            }
        }
    }

    public static async Task Send(ClientWebSocket ws, string message)
    {
        byte[] buffer = Encoding.UTF8.GetBytes(message);
        await ws.SendAsync(new ArraySegment<byte>(buffer, 0, buffer.Length), WebSocketMessageType.Text, true, CancellationToken.None);
        Console.WriteLine("Sent: " + message);
    }

    public static async Task Main(string[] args)
    {
        Uri wsUri = new Uri("ws://localhost:5000/ws"); // Replace with your WebSocket server URL
        await ConnectAndReceive(wsUri);
    }
}

Server-Side WebSocket with `WebSocket` (ASP.NET Core)

This code snippet demonstrates how to handle WebSocket connections in an ASP.NET Core application.

Explanation:

  1. In the `Configure` method of `Startup.cs`, `app.UseWebSockets()` enables WebSocket support.
  2. Middleware is added to handle requests to `/ws`. It checks if the request is a WebSocket request.
  3. If it's a WebSocket request, it accepts the connection using `context.WebSockets.AcceptWebSocketAsync()`, which returns a `WebSocket` instance.
  4. The `Echo` method receives messages from the client and sends them back (echoes them). It continues until the client closes the connection.
  5. The `await webSocket.CloseAsync(...)` closes the WebSocket connection gracefully.

// Startup.cs (Configure method)
app.UseWebSockets();

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
    else
    {
        await next();
    }
});

// Echo method
private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

Concepts Behind the Snippet

WebSocket Protocol: WebSockets use the `ws://` or `wss://` URI scheme. `wss://` indicates a secure WebSocket connection over TLS/SSL.

Full-Duplex Communication: Unlike HTTP, WebSockets allow data to flow in both directions simultaneously.

Persistent Connection: The connection remains open until explicitly closed by either the client or the server.

Real-Life Use Case

A real-time chat application is a classic example of WebSocket usage. When a user sends a message, the server immediately pushes the message to all connected clients in the chat room without requiring each client to repeatedly poll the server for updates. This drastically reduces latency and provides a seamless user experience.

Best Practices

  • Handle Errors: Always include proper error handling to catch exceptions during connection, sending, and receiving.
  • Secure Connections: Use `wss://` for secure WebSocket connections, especially when transmitting sensitive data.
  • Keep-Alive Pings: Implement keep-alive pings to detect and handle broken connections.
  • Message Serialization: Use a consistent serialization format (e.g., JSON) for sending and receiving messages.
  • Rate Limiting: Implement rate limiting to prevent abuse and ensure server stability.
  • Connection Management: Manage WebSocket connections efficiently to avoid resource exhaustion.

Interview Tip

When discussing WebSockets in an interview, highlight your understanding of the benefits of full-duplex communication, the importance of persistent connections, and the use cases where WebSockets are superior to traditional HTTP polling. Be prepared to explain the difference between `ws://` and `wss://` and the security implications.

When to Use Them

Use WebSockets when:

  • You need real-time, bidirectional communication.
  • Low latency is crucial.
  • You want to avoid the overhead of repeated HTTP requests.
  • You're building applications like chat, online games, or live dashboards.

Memory Footprint

WebSockets can be more memory-efficient than long polling or server-sent events because they maintain a single persistent connection. However, you need to manage WebSocket connections carefully to avoid memory leaks or resource exhaustion, especially with a large number of concurrent connections. Pooling buffers used for receiving and sending data can help reduce memory allocation overhead.

Alternatives

Alternatives to WebSockets include:

  • Server-Sent Events (SSE): Unidirectional communication from server to client.
  • Long Polling: The client makes a request and the server holds the connection open until data is available.
  • gRPC: A modern high-performance RPC framework that can be used for real-time communication.

Pros

  • Real-time communication: Enables instant data transfer between client and server.
  • Full-duplex: Allows simultaneous bidirectional data flow.
  • Persistent connection: Reduces overhead compared to HTTP polling.
  • Low latency: Ideal for applications requiring quick responses.

Cons

  • Complexity: Requires more complex server-side implementation compared to simple HTTP requests.
  • Stateful: Maintaining WebSocket connections requires managing state on the server.
  • Scalability: Scaling WebSocket servers can be challenging due to the persistent connections.
  • Firewall Issues: Some firewalls might interfere with WebSocket connections.

FAQ

  • What's the difference between `ws://` and `wss://`?

    `ws://` is the URI scheme for unencrypted WebSockets, while `wss://` is the URI scheme for secure WebSockets using TLS/SSL. Always use `wss://` when transmitting sensitive data.
  • How do I handle errors in WebSocket connections?

    Wrap your WebSocket code in `try-catch` blocks to catch exceptions during connection, sending, and receiving. Log the errors for debugging and handle them gracefully to prevent application crashes. Implement retry logic for transient errors.
  • How do I close a WebSocket connection?

    Use the `CloseAsync` method of the `ClientWebSocket` or `WebSocket` object. Specify a `WebSocketCloseStatus` and a description for the closure. Ensure that both the client and server close the connection gracefully.