Using Raw WebSockets in .NET

Somebody who likes to code
Some time ago, in Getting Started with SignalR in .NET 6, we discussed how easy it is to add real-time features to our applications. However, in some situations, we might need something simple, like using the basic functionality offered by .NET to implement WebSockets.
This time, we will implement a basic chat application. Run the following commands to set up the project and solution:
dotnet new webapi -n ChatServer
dotnet new sln -n Websockets
dotnet sln add ChatServer
Open the solution and replace the content Program.cs file with the following content:
using System.Net.WebSockets;
using System.Text;
using System.Collections.Concurrent;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWebSockets();
var clients = new ConcurrentDictionary<Guid, WebSocket>();
app.Map("/chat", async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Handle(webSocket, clients);
}
else
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
}
});
app.Run();
To enable Websockets, we add the corresponding middleware using the app.UseWebSockets() statement. Then, we define an endpoint at the path /chat that detects web socket requests. The HandleRequest method is as follows:
async Task HandleRequest(WebSocket webSocket, ConcurrentDictionary<Guid, WebSocket> clients)
{
var clientId = Guid.NewGuid();
clients.TryAdd(clientId, webSocket);
while (webSocket.State == WebSocketState.Open)
{
var (payload, messageType) = await Read(webSocket);
if (messageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(webSocket.CloseStatus!.Value, webSocket.CloseStatusDescription, CancellationToken.None);
break;
}
if (messageType == WebSocketMessageType.Text)
{
await Broadcast(payload, clientId, clients);
}
}
webSocket.Dispose();
clients.TryRemove(clientId, out _);
}
The connection between the server and the client will be managed through an infinite loop while the socket remains open. The basic algorithm will be as follows:
Check if the web socket is still open.
Read the data.
If the message type is
Close, we close the web socket.If the message type is
Text, we broadcast the message to all the clients.
The Read method is as follows:
async Task<(List<byte>, WebSocketMessageType)> Read(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
var payload = new List<byte>(1024 * 4);
WebSocketReceiveResult? result;
do
{
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
payload.AddRange(new ArraySegment<byte>(buffer, 0, result.Count));
}
while (result.EndOfMessage == false);
return (payload, result.MessageType);
}
Since the client can send the message in several chunks, we keep iterating over ReceiveAsync until we reach the end of the message. The Broadcast method is as follows:
async Task Broadcast(List<byte> payload, Guid clientId, ConcurrentDictionary<Guid, WebSocket> clients)
{
var receivedMessage = Encoding.UTF8.GetString(payload.ToArray());
foreach (var key in clients.Keys)
{
var message = Encoding.UTF8.GetBytes($"{clientId} says {receivedMessage}");
await clients[key].SendAsync(new ArraySegment<byte>(message), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
We decode the payload in UTF8, create a new message, and then encode the result into a new message. Every message is sent to all the connected clients. To test our chat server, create a client.html file at the solution level with the following content:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Chat</title>
</head>
<body>
<input id="message" placeholder="Message" />
<button id="send" type="button">Send</button>
<hr />
<ul id="messages"></ul>
<script>
document.addEventListener("DOMContentLoaded", () => {
const websocket = new WebSocket("http://localhost:5263/chat");
websocket.onmessage = (e) => {
const li = document.createElement("li");
li.textContent = `${e.data}`;
document.getElementById("messages").appendChild(li);
};
document.getElementById("send").addEventListener("click", async () => {
const message = document.getElementById("message").value;
try {
await websocket.send(message);
} catch (err) {
console.error(err);
}
});
});
</script>
</body>
</html>
The script above starts the WebSocket and sets up a handler to receive messages from the server. It also sets up a handler for the button to send messages to the server. You can find all the code here. Thanks, and happy coding.




