ASP.NET Core: Building chat room using WebSocket

WebSocket is real-time communication protocol we can use to make UI in browser to send and receive messages from server over real-time communication channel. WebSocket is also supported by ASP.NET Core. This blog post demonstrates how to build simple browser based chat room on ASP.NET Core and WebSocket.

Our chat room has two views: one for inserting nick name and the other one for chat room. We will use two controller actions and one middleware class to get the fully functional chat room. Of course, we will use WebSocket package too. The web site Can I use reports that majority of modern browsers support WebSocket protocol.

NB! You can browse source code or download Visual Studio 2017 solution from AspNetCoreChatRoom Git repository.

Creating “Login form”

We use here simple form where user can insert his or her preferred nick name for chat. To keep us focused on WebSocket stuff we don’t add any logic or checks in this point. Let’s add view called InsertUserName.cshtml under Home folder.

<form action="@Url.Action("Index")" method="post">
    <input type="text" placeholder="Insert user name" name="userName" />
    <input type="submit" value="Eńter" 
/>
</
form
>

There will be another view for chat room and we will come back to it later when web sockets related code is done. Index() methods of home controller look like this.

[HttpGet]
public IActionResult
Index()
{
   
return View("InsertUserName"
);
}

[
HttpPost]
public IActionResult Index(string
username)
{
   
return View("Index", username);
}

If request method is GET then we show nick name form and if request method is POST we will show chat room view.

WebSockets middleware

Now let’s write ASP.Core middleware for WebSocket. To keep things simple I mixed together custom WebSocket middleware and custom WebSocket connection manager from Radu Matei’s post Creating a WebSockets middleware for ASP .NET Core. I like the work Radu has done but here we will keep things as small as possible. To get better understanding of WebSockets I suggest you to go through Radu’s post.

NB! To use WebSockets in ASP.NET Core project add reference to Microsoft.AspNetCore.WebSockets NuGet package!

In breaf, this is what our WebSocket middleware class does:

  1. Keep concurrent dictionary with connected WebSockets (this is needed for message broadcast)
  2. Read messages from WebSocket and broadcast there to all known WebSockets
  3. Try to keep WebSockets dictionary as clean as possible

Here is the WebSocket middleware class.

public class ChatWebSocketMiddleware
{
   
private static ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket
>();

   
private readonly RequestDelegate
_next;

   
public ChatWebSocketMiddleware(RequestDelegate
next)
    {
        _next = next;
    }

   
public async Task Invoke(HttpContext
context)
    {
       
if
(!context.WebSockets.IsWebSocketRequest)
        {
           
await
_next.Invoke(context);
           
return
;
        }

       
CancellationToken
ct = context.RequestAborted;
       
WebSocket currentSocket = await
context.WebSockets.AcceptWebSocketAsync();
       
var socketId = Guid
.NewGuid().ToString();

        _sockets.TryAdd(socketId, currentSocket);

       
while (true
)
        {
           
if
(ct.IsCancellationRequested)
            {
               
break
;
            }

           
var response = await
ReceiveStringAsync(currentSocket, ct);
           
if(string
.IsNullOrEmpty(response))
            {
               
if(currentSocket.State != WebSocketState
.Open)
                {
                   
break
;
                }

               
continue
;
            }

           
foreach (var socket in
_sockets)
            {
               
if(socket.Value.State != WebSocketState
.Open)
                {
                   
continue
;
                }

               
await
SendStringAsync(socket.Value, response, ct);
            }
        }

       
WebSocket
dummy;
        _sockets.TryRemove(socketId,
out
dummy);

       
await currentSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing"
, ct);
        currentSocket.Dispose();
    }

   
private static Task SendStringAsync(WebSocket socket, string data, CancellationToken ct = default(CancellationToken
))
    {
       
var buffer = Encoding
.UTF8.GetBytes(data);
       
var segment = new ArraySegment<byte
>(buffer);
       
return socket.SendAsync(segment, WebSocketMessageType.Text, true
, ct);
    }

   
private static async Task<string> ReceiveStringAsync(WebSocket socket, CancellationToken ct = default(CancellationToken
))
    {
       
var buffer = new ArraySegment<byte>(new byte
[8192]);
       
using (var ms = new MemoryStream
())
        {
           
WebSocketReceiveResult
result;
           
do
            {
                ct.ThrowIfCancellationRequested();

                result =
await
socket.ReceiveAsync(buffer, ct);
                ms.Write(buffer.Array, buffer.Offset, result.Count);
            }
           
while
(!result.EndOfMessage);

            ms.Seek(0,
SeekOrigin
.Begin);
           
if (result.MessageType != WebSocketMessageType
.Text)
            {
               
return null
;
            }

           
// Encoding UTF8: https://tools.ietf.org/html/rfc6455#section-5.6
            using (var reader = new StreamReader(ms, Encoding
.UTF8))
            {
               
return await reader.ReadToEndAsync();
            }
        }
    }
}

Before using middleware we have to introduce it to request pipeline in Startup class of web application. We do it in configure method before initializing MVC.

app.UseWebSockets();
app.UseMiddleware<
ChatWebSocketMiddleware
>();

app.UseMvc(routes =>
{
    routes.MapRoute(
        name:
"default"
,
        template:
"{controller=Home}/{action=Index}/{id?}");
});

NB! It’s very important to add WebSockets before MVC. WebSockets middleware checks in the beginning if request is for WebSocket. If it is WebSocket request then middleware starts handling it. If MVC is added to pipeline before then MVC will handle all the requests and our WebSocket middleware is never used.

Chat room view

Now let’s add chat room view. We will use Index view of home controller for chat room. Chat room view initializes WebSocket connection and starts listening to it. If user writes something to chat box and presses Enter then message is sent to server over WebSocket and WebSocket middleware will broadcast it to all WebSocket clients it knows.

@model string
@{
    ViewData[
"Title"] = "Home Page"
;
}

<style>
    body {margin:0px; padding:0px
;}
   
.msg
{
       
position: absolute
;
       
top: 0
;
       
bottom: 30px
;
       
border: 1px solid green
;
       
margin-bottom: auto
;
       
display:block
;
       
overflow: scroll
;
       
width:100%
;
       
white-space:nowrap
;
    }

</style
>
<
div class="msg">
    <div style="position:absolute; bottom:0;" id="msgs"></div
>
</
div>

<div style="position:absolute;height:20px;bottom:10px;left:0; display:block;width:100%">
    <input type="text" style="max-width:unset;width:100%;max-width:100%" id="MessageField" placeholder="type message and press enter" 
/>
</
div>

@section Scripts {
   
<script>
    $(function
() {
       
var userName = '@Model'
;

       
var protocol = location.protocol === "https:" ? "wss:" : "ws:"
;
       
var wsUri = protocol + "//"
+ window.location.host;
       
var socket = new
WebSocket(wsUri);
        socket.onopen = e => {
            console.log(
"socket opened"
, e);
        };

        socket.onclose =
function
(e) {
            console.log(
"socket closed"
, e);
        };

        socket.onmessage =
function
(e) {
            console.log(e);
            $(
'#msgs').append(e.data + '<br />'
);
        };

        socket.onerror =
function
(e) {
            console.error(e.data);
        };

        $(
'#MessageField').keypress(function
(e) {
           
if
(e.which != 13) {
               
return
;
            }

            e.preventDefault();

           
var message = userName + ": " + $('#MessageField'
).val();
            socket.send(message);
            $(
'#MessageField').val(''
);
        });
    });
   
</script>
}

Now let’s build application and run it.

WebSocket chat room in action

The screenshot below shows how our chat room looks like. It’s extremely primitive and simple but it works. There’s room enough for improvements but this is the fun I leave to all my dear readers.

ASP.NET Core WebSocket chat room in action

Wrapping up

Using WebSockets in ASP.NET Core is simple. Without any additional libraries we can use the one by Microsoft. We had to write custom middleware class for WebSocket communication and in our case the class came pretty small. We used concurrent dictionary as a WebSockets cache and this enabled us to broadcast messages over sockets. Our solution is very primitive and simple, there is a lot of room for improvements but as a proof of concept it works well.

References

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    24 thoughts on “ASP.NET Core: Building chat room using WebSocket

    • March 21, 2017 at 11:11 am
      Permalink

      Cheers, Radu!
      I will check out your package and if it is ready for action I will make my code samples use it.

    • March 26, 2017 at 6:49 pm
      Permalink

      Hii.
      I like this.
      This cdoe is simple and i liked.

    • June 13, 2017 at 6:11 pm
      Permalink

      Im getting this error when running the sample…

      WebSocket connection to ‘ws://localhost:36453/’ failed: Error during WebSocket handshake: Unexpected response code: 200

      maybe the middleware is not capturing the ws:/… request?

    • July 11, 2017 at 11:54 am
      Permalink

      Great article Gunnar.
      Thank you,
      but what about private chat?
      lets consider we wanna make a private chat between two asp.net identityCore user.

      what should I do?

    • July 17, 2017 at 8:51 pm
      Permalink

      To implement private chat you have to implement mapping between users and sockets. When new socket is created you keep with it also user name. This is the first idea that comes to my mind.

    • October 4, 2017 at 10:52 am
      Permalink

      Looks like messy crap, still writing buffers in 2017.. wow. Think I’ll stick to SocketIO.

    • October 4, 2017 at 1:01 pm
      Permalink

      You can use ready-made NuGet package too for web sockets :)

    • October 9, 2017 at 10:19 am
      Permalink

      Thanks for this great simple sample. Shows how easy it is :)
      To broadcast to the ohter clients in a foreach is ok for a sample but I prefer to do that with a parallel task (await Task.all(…);)

    • January 7, 2018 at 10:06 pm
      Permalink

      This is a very informative article. Thank you for sharing your time and effort. The Chat Application use case is very recognizable, but could you offer any pointers on something more like a relay where a client opens up a web socket to the server, then a second client performs a Get on a separate API on the Server and the server can initiate a message to the first client. For example, a smarthome device at my house connects to my service on Azure, an AWS lambda function calls my service on Azure. For example, I’m working on porting the NODE.js Azure service on this to a .Net client: https://manuel-rauber.com/2017/05/30/alexa-start-lego-robot-connecting-amazon-alexa-to-lego-mindstorms-ev3/#apibridge

    • January 8, 2018 at 2:44 am
      Permalink

      I understand that you are building something more complex. If you are in hurry then go with WebSocket directly. If you have time to wait then I suggest you to wait until SignalR for ASP.NET Core gets to first stable release.

      I don’t have such example right now but I can give some advice. Based on my code you can use multiple WebSocket end-points and modify WebSocket class the way that it can accept messages from controller actions. This way you can build API controller action that sends message to all clients of some specific end-point.

    • January 14, 2018 at 1:19 pm
      Permalink

      Thanks you Gunnar for great Article and simple project. I could learn a lot from it

    • February 19, 2018 at 1:37 pm
      Permalink

      Is there a sample which handles websocket (client) not javascript?

    • February 19, 2018 at 3:23 pm
      Permalink

      Hello Gunnar, actually I would like to check if there is ASP.NET Core 2.0. I see ASP.NET Core samples on the internet but they (client and server) both on same machine or they use javascript. But I need .net code for it so I can use it for my other projects. (I will add signalr to it)

    • February 19, 2018 at 4:01 pm
      Permalink

      There is SignalR support coming to ASP.NET Core. The release is planned with version 2.1 during the first half of this year. If you want to play with pre-release bits then you can start with this page: https://github.com/aspnet/SignalR

    • February 19, 2018 at 8:57 pm
      Permalink

      Actually, I’m trying to make a web api (.net core) project which connects to websockets (some logic) then publish those data with signalr. I know that signalr core is not ready but I want to complete websocket part at least.

    • Pingback:Reading List Mar 16 – 23, 2017 | Jega's Blog

    • July 3, 2018 at 12:17 pm
      Permalink

      Hi, Gunnar, how I can handle closed connection from client if client forced close or client doesn’t have network? because when when it happen, the client not completing close handshake,can you give me more sample. thanks

    • July 4, 2018 at 12:07 am
      Permalink

      Hi, Where do i put this part??

      app.UseWebSockets();
      app.UseMiddleware();

      app.UseMvc(routes =>
      {
      routes.MapRoute(
      name: “default”,
      template: “{controller=Home}/{action=Index}/{id?}”);
      });

      Thanks

    • September 29, 2020 at 5:05 pm
      Permalink

      How can I implement chat history view for newly connected users?

    • November 23, 2022 at 9:29 pm
      Permalink

      You just laied down a bunch of code without explaining anything of substance. In fact, I don’t think you explained anything.

    Leave a Reply

    Your email address will not be published. Required fields are marked *