ZetCode

C# WebSocket

last modified October 18, 2026

In this article we show how to work with WebSockets in C#.

WebSocket

WebSocket is a communications protocol that provides full-duplex communication over a single TCP connection (RFC 6455). The connection starts as an ordinary HTTP request with an Upgrade: websocket header; if the server agrees, both sides can send messages to each other at any time, without the request and response cycle of HTTP. WebSockets are used in highly interactive applications such as games, chats, or live dashboards.

WebSocket messages are either text (UTF-8) or binary. A message can be split into several frames, and the connection is closed with a closing handshake, in which each side sends a close frame with a status code and an optional reason.

In this article we create an ASP.NET Core application with WebSocket support, and three clients: two C# console programs and an HTML page with JavaScript. The server is a single-file app, which requires the .NET 10 SDK; for older SDKs, put the same code in a project created with dotnet new web.

C# ASP.NET WebSocket server

The following is a simple WebSocket server application with two endpoints. The /time endpoint pushes the current time to the client every second. The /echo endpoint sends back every message it receives.

server.cs
#:sdk Microsoft.NET.Sdk.Web

using System.Net;
using System.Net.WebSockets;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseWebSockets();

app.Map("/time", async context =>
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return;
    }

    using var ws = await context.WebSockets.AcceptWebSocketAsync();

    for (int i = 0; i < 5 && ws.State == WebSocketState.Open; i++)
    {
        byte[] data = Encoding.UTF8.GetBytes(DateTime.Now.ToString("HH:mm:ss"));
        await ws.SendAsync(data, WebSocketMessageType.Text, endOfMessage: true,
            context.RequestAborted);
        await Task.Delay(1000, context.RequestAborted);
    }

    if (ws.State == WebSocketState.Open)
    {
        await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "that is all",
            context.RequestAborted);
    }
});

app.Map("/echo", async context =>
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return;
    }

    using var ws = await context.WebSockets.AcceptWebSocketAsync();
    var buffer = new byte[1024];

    while (ws.State == WebSocketState.Open)
    {
        // a message may arrive in several frames: collect them until EndOfMessage
        using var message = new MemoryStream();
        WebSocketReceiveResult result;

        do
        {
            result = await ws.ReceiveAsync(buffer, context.RequestAborted);
            message.Write(buffer, 0, result.Count);
        } while (!result.EndOfMessage);

        if (result.MessageType == WebSocketMessageType.Close)
        {
            await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye",
                context.RequestAborted);
            break;
        }

        string text = Encoding.UTF8.GetString(message.ToArray());
        byte[] reply = Encoding.UTF8.GetBytes($"echo: {text}");
        await ws.SendAsync(reply, WebSocketMessageType.Text, true, context.RequestAborted);
    }
});

app.Run("http://localhost:5050");

The server sends five time messages and then closes the connection. The echo endpoint runs until the client closes the connection.

#:sdk Microsoft.NET.Sdk.Web

This directive of a file-based app selects the web SDK, which gives us ASP.NET Core. Run the server with dotnet run server.cs.

using System.Net.WebSockets;

WebSocket support is in the System.Net.WebSockets namespace.

app.UseWebSockets();

We enable the WebSocket middleware with UseWebSockets.

app.Map("/time", async context =>
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return;
    }

We map the communication to the /time endpoint. A request that is not a WebSocket handshake gets the 400 Bad Request status.

using var ws = await context.WebSockets.AcceptWebSocketAsync();

We accept the handshake and turn the HTTP request into a WebSocket connection with AcceptWebSocketAsync. The request handler must not return while the connection is in use, because returning ends the connection; that is why the loop runs inside the handler.

byte[] data = Encoding.UTF8.GetBytes(DateTime.Now.ToString("HH:mm:ss"));
await ws.SendAsync(data, WebSocketMessageType.Text, endOfMessage: true,
    context.RequestAborted);

We send a text message with SendAsync. The RequestAborted token is cancelled when the client disconnects, so the loop does not keep working for a client that is gone.

await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "that is all",
    context.RequestAborted);

We close the connection with CloseAsync, giving a status code and a reason that the client can read.

do
{
    result = await ws.ReceiveAsync(buffer, context.RequestAborted);
    message.Write(buffer, 0, result.Count);
} while (!result.EndOfMessage);

In the echo endpoint, we receive with ReceiveAsync. The buffer holds only 1024 bytes, and a longer message arrives in several pieces, so we append the pieces to a memory stream until EndOfMessage is true. Reading a single buffer and treating it as a whole message is a common bug.

if (result.MessageType == WebSocketMessageType.Close)
{
    await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", context.RequestAborted);
    break;
}

When the client sends a close frame, we answer it with our own to complete the closing handshake.

C# WebSocket client

In the next example, we create a C# console program that establishes a WebSocket connection to the /time endpoint.

Program.cs
using System.Net.WebSockets;
using System.Text;

using var ws = new ClientWebSocket();

await ws.ConnectAsync(new Uri("ws://localhost:5050/time"), CancellationToken.None);
byte[] buf = new byte[1024];

while (ws.State == WebSocketState.Open)
{
    var result = await ws.ReceiveAsync(buf, CancellationToken.None);

    if (result.MessageType == WebSocketMessageType.Close)
    {
        Console.WriteLine($"server closed the connection: {result.CloseStatusDescription}");
        await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
    }
    else
    {
        Console.WriteLine(Encoding.UTF8.GetString(buf, 0, result.Count));
    }
}

Console.WriteLine($"state: {ws.State}");

We connect to the server from a console application and print everything we receive.

using var ws = new ClientWebSocket();

The ClientWebSocket class creates WebSocket clients in a C# application.

await ws.ConnectAsync(new Uri("ws://localhost:5050/time"), CancellationToken.None);
byte[] buf = new byte[1024];

We connect asynchronously with ConnectAsync. The URI scheme is ws for plain connections and wss for TLS-protected ones. The buffer receives the incoming data.

var result = await ws.ReceiveAsync(buf, CancellationToken.None);

ReceiveAsync fills the buffer and returns a result with the message type, the number of bytes, and whether the message is complete. The time messages are tiny and always fit into one frame, so we do not loop here.

if (result.MessageType == WebSocketMessageType.Close)
{
    Console.WriteLine($"server closed the connection: {result.CloseStatusDescription}");
    await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}

If the message type is Close, the server wants to end the connection. We print the reason and complete the handshake with CloseAsync, after which the state becomes Closed and the loop ends.

$ dotnet run client1.cs
19:58:24
19:58:25
19:58:26
19:58:27
19:58:28
server closed the connection: that is all
state: Closed

C# WebSocket echo client

A WebSocket client can also send messages. In the next example, we send three messages to the echo endpoint and print the replies.

Program.cs
using System.Net.WebSockets;
using System.Text;

using var ws = new ClientWebSocket();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

await ws.ConnectAsync(new Uri("ws://localhost:5050/echo"), cts.Token);

var buf = new byte[1024];

foreach (var line in new[] { "hello", "falcon", "žltý kôň" })
{
    await ws.SendAsync(Encoding.UTF8.GetBytes(line), WebSocketMessageType.Text, true, cts.Token);

    var result = await ws.ReceiveAsync(buf, cts.Token);
    Console.WriteLine(Encoding.UTF8.GetString(buf, 0, result.Count));
}

await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "done", cts.Token);
Console.WriteLine($"state: {ws.State}");

The program sends a message and waits for the reply before sending the next one.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

A cancellation token with a timeout is passed to all operations, so the program cannot hang forever if the server stops responding.

await ws.SendAsync(Encoding.UTF8.GetBytes(line), WebSocketMessageType.Text, true, cts.Token);

We encode the string as UTF-8 and send it as one complete text message. The protocol requires text messages to be UTF-8; the non-ASCII third message shows that this works.

await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "done", cts.Token);

When we are done, we close the connection properly. This sends a close frame and waits for the server's answer.

$ dotnet run client2.cs
echo: hello
echo: falcon
echo: žltý kôň
state: Closed

JS WebSocket client

In the next example, we create a JavaScript client that opens a WebSocket connection from a browser. Save the page as index.html and open it in a browser while the server is running.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket client</title>
</head>
<body>

    <div id="output"></div>

    <p>
        <button id="start">Start</button>
    </p>

    <script>
        const output = document.getElementById("output");

        document.getElementById("start").addEventListener("click", () => {
            const ws = new WebSocket("ws://localhost:5050/time");

            ws.onopen = () => output.textContent = "connected";
            ws.onmessage = e => output.textContent = e.data;
            ws.onclose = e => output.textContent = `closed: ${e.reason}`;
            ws.onerror = () => output.textContent = "connection error";
        });
    </script>

</body>
</html>

In JavaScript, we use the built-in WebSocket class and its event handlers: onopen, onmessage, onclose, and onerror. Clicking the button connects to the server, shows each received time, and after five seconds displays the closing reason, "that is all". Browsers do not apply the same-origin policy to WebSocket connections, so any page can try to connect to the server; a server that needs to restrict its clients should check the Origin header.

The same client logic also works in Node.js 22 and later, which has a global WebSocket class.

$ node js.mjs
connected
19:58:29
19:58:30
19:58:31
19:58:32
19:58:33
closed: that is all

Source

WebSocket class

In this article we have worked with WebSocket in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.