My previous post on websockets on how to stream messages to a client has become quite popular. I felt that I left this halfway when I only sent messages on way (to the client), but I did not send messages to the server. Therefore I have made this follow up post on how to do that. For this I have created a small Chat application on my websocket playground on github.
In this sample application I have created a very simple webpage (no css just pure html) with some simple plain vanilla javascript to make the websocket connection. The application is a simple chat where you can open multiple tabs in your browser and see the messages being sent to everyone in real time.
If you wish to know more about the differences between simple HTTP calls and websockets, check out my post here.
On the server side
As backend, I use asp.net core version 3.1. My startup.cs file looks like the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddSingleton<IWebsocketHandler, WebsocketHandler>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseWebSockets();
app.UseEndpoints(routes =>
{
routes.MapControllerRoute(
name: "default",
pattern: "{controller=Page}/{action=Index}/{id?}");
});
}
There is not much out of the ordinary here. I setup the application to use Razor pages and static files in order to be able to serve a webpage with some javascript - which I will refer to as frontend or the client. I call app.UseWebSockets();
in order to set up my application to use websockets. A singleton for the class WebsocketHandler
is added, this will handle the websocket logic, but we will get back to that shortly.
Below is the code for the StreamController which handles the websocket handshake:
[Route("api/[controller]")]
public class StreamController : Controller
{
public IWebsocketHandler WebsocketHandler { get; }
public StreamController(IWebsocketHandler websocketHandler)
{
WebsocketHandler = websocketHandler;
}
[HttpGet]
public async Task Get()
{
var context = ControllerContext.HttpContext;
var isSocketRequest = context.WebSockets.IsWebSocketRequest;
if (isSocketRequest)
{
WebSocket websocket = await context.WebSockets.AcceptWebSocketAsync();
await WebsocketHandler.Handle(Guid.NewGuid(), websocket);
}
else
{
context.Response.StatusCode = 400;
}
}
}
Basically the only thing this does is upgrade the communication to use websockets and call our WebsocketHandler
with the new socket. The WebsocketHandler
is injected using dependency injection as a singleton as it will contain all our sockets and handle the communication to and from them. This is the heart of the backend, which can be seen below:
public List<SocketConnection> websocketConnections = new List<SocketConnection>();
public async Task Handle(Guid id,WebSocket webSocket)
{
lock (websocketConnections) {
websocketConnections.Add(new SocketConnection {
Id = id,
WebSocket = webSocket
});
}
await SendMessageToSockets($"User with id <b>{id}</b> has joined the chat");
while (webSocket.State == WebSocketState.Open)
{
var message = await ReceiveMessage(id, webSocket);
if (message != null)
await SendMessageToSockets(message);
}
}
private async Task<string> ReceiveMessage(Guid id, WebSocket webSocket)
{
var arraySegment = new ArraySegment<byte>(new byte[4096]);
var receivedMessage = await webSocket.ReceiveAsync(arraySegment, CancellationToken.None);
if (receivedMessage.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.Default.GetString(arraySegment).TrimEnd('\0');
if (!string.IsNullOrWhiteSpace(message))
return $"<b>{id}</b>: {message}";
}
return null;
}
private async Task SendMessageToSockets(string message)
{
IEnumerable<SocketConnection> toSentTo;
lock (websocketConnections)
{
toSentTo = websocketConnections.ToList();
}
var tasks = toSentTo.Select(async websocketConnection =>
{
var bytes = Encoding.Default.GetBytes(message);
var arraySegment = new ArraySegment<byte>(bytes);
await websocketConnection.WebSocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
});
await Task.WhenAll(tasks);
}
The first thing that happens when the Handle()
method is called is that we add the new socket to a collection. This collection (websocketConnections
) contains all websockets for clients that have connected to our backend. When we add a socket to the collection we also add an Id so that it is easier to keep track of all the websockets and who is sending messages. We then send a message to all current websockets that we have a new client who joined the chat.
After this we start receiving messages, this means that we now await the client to send a message to the backend. If we receive a message we send it to all the websockets in our collection of sockets. You may think this blocks this client from receiving messages, however every time a client sends a message we forward this message in the same scope to all the clients, then wait for a new message to arrive. So the call sending the message makes sure it is forwarded as well. The messages are sent in parallel, so there is no guarantee which socket receives it first.
In the above I have added some locks. At first I made the above with a ConcurrentBag, however I later added logic to remove closed sockets and removing these from the bag was troublesome for me (I have added the clean up logic at the bottom of this page). If you have a simpler solution please let me know.
That is it for the backend, let us move on to the frontend (client)
On the frontend
The frontend is quite simple, the HTML consists of a button (input button), an input field (input text) and an unordered list (ul):
<body>
<div>
<h1>Stream chat</h1>
<input id="sendmessage" type="button" value="Send!" />
<input id="messageTextInput" type="text" />
<ul id="chatMessages"></ul>
<script src="~/js/chatstream.js"></script>
</div>
</body>
As you can see at the bottom of the HTML is some javascript included:
(function() {
let webSocket
var getWebSocketMessages = function (onMessageReceived)
{
let url = `ws://${location.host}/api/stream`;
webSocket = new WebSocket(url);
webSocket.onmessage = onMessageReceived;
};
let ulElement = document.getElementById('chatMessages');
getWebSocketMessages(function (message) {
ulElement.innerHTML = ulElement.innerHTML += `<li>${message.data}</li>`
});
document.getElementById("sendmessage").addEventListener("click", function () {
let textElement = document.getElementById("messageTextInput");
let text = textElement.value;
webSocket.send(text);
textElement.value = '';
});
}());
In the above we first set up the URL for our controller endpoint and create a new websocket. We then make it so the ul element on our page is populated with messages coming from our backend using the webSocket.onmessage
event. Then we attach an eventListener to the button on the page which takes the input of the textfield and sends it using webSocket.send
.
That's it, that is all that is needed to handle the back and forth communication on the client side.
The result
Using the above you can open several tabs in your browser and send messages back and forth as seen below:
In the above I first connect with the top tab and then the second one, sending a hello world to one another. The top tab is the first to join and see itself and the other join, the second one joins later and only sees itself joining.
That is basically it, I am now sending websocket requests back and forth between the clients and server, where the server emits all messages to all clients. Looking at the network tab we see just the following:
In the above we can see the 101 HTTP status code (switching protocols). We do not see the individual requests as they are now handled through the socket. But we can actually see them in the message tab:
This is another example than the previous one, so you will see different Id's, but the same flow except one leaves the room at the end. The green message is the "hello" send to the backend, whereas the others are received from the backend.
Clean up of closed / aborted sockets
I made the below code to clean up closed or aborted sockets, it is placed within the WebsocketHandler
:
public WebsocketHandler()
{
SetupCleanUpTask();
}
private void SetupCleanUpTask()
{
Task.Run(async () =>
{
while (true)
{
IEnumerable<SocketConnection> openSockets;
IEnumerable<SocketConnection> closedSockets;
lock (websocketConnections)
{
openSockets = websocketConnections.Where(x => x.WebSocket.State == WebSocketState.Open || x.WebSocket.State == WebSocketState.Connecting);
closedSockets = websocketConnections.Where(x => x.WebSocket.State != WebSocketState.Open && x.WebSocket.State != WebSocketState.Connecting);
websocketConnections = openSockets.ToList();
}
foreach (var closedWebsocketConnection in closedSockets)
{
await SendMessageToSockets($"User with id <b>{closedWebsocketConnection.Id}</b> has left the chat");
}
await Task.Delay(5000);
}
});
}
The above contains a while loop that runs every 5 seconds. It removes all disconnected websockets and sends a message to the currently connected websockets that someone was disconnected. I decided to go with this solution since clients may not always send a close message.
Wrapping it up
A small disclaimer: the above has not been tested in production, it is just me fiddling around with websockets in asp.net core. If you have tried out the above and have some feedback please post it down below in the comments, it is much appreciated.
I know the UI is boring and that the enter key does not even work. I do not wish to add anything to this solution that is not relevant. Often I find solutions with a ton of CSS and javascript that is not needed to understand the essence of the example, this I have tried to avoid.
If you found the above helpful, please let me know down in the comments below!