How To Use WebSockets In ASP.NET Core - Day Nine

In the previous article of this series, we discussed the URL rewriting middleware concept within .NET Core applications. Now in this article, we will discuss about web sockets and how to use web sockets in ASP.NET Core applications.

If you want to look at the previous articles, please visit the links given below.
 
Actually, Web Socket is one type of communication protocol which provides full duplex communication channel over a TCP connection. Web Socket is mainly designed for implementing within web servers and web browsers. However, we can implement Web Socket at any client or server application since it is totally independent of TCP based protocols. It is used for applications such as chat, stock tickers, games, anywhere you want real-time functionality in a web application.

How Web Socket works

Web Socket actually provides a persistent or steady connection between a server and a client so that both the sides can send data to the other side any time. The client establishes a WebSocket connection through a process known as the WebSocket Handshake. This process starts with the client sending a regular HTTP request to the server.
A brief history of Real-Time Web Applications

In the early ages of web development, web sites were basically developed to mainly fulfill only idea i.e. client can send request of fetching any type of data to the Server and on the other hand, Sever must fulfill the request to send back those data to the client. But in the year of 2005, with the introduction of AJAX, this basic concept of web application changed.

At that time, people wanted to develop bidirectional web applications or sites for establishing connection between Client and Server with the help of AJAX. Web applications have grown up a lot and are now consuming more data than ever before. The biggest thing holding them back was the traditional HTTP model of client initiated transactions. To overcome this, a number of different strategies were devised to allow Servers to push data to the Client. One of the most popular of these strategies was long-polling.
 
This involves keeping an HTTP connection open until the Server has some data to push down to the client. But there was a problem in case of HTTP connection. Actually, against each and every HTML request, a bunch of headers and cookies data are transferred from the client machine to the Server, which kills the network bandwidth. But if we want to develop a web based application like game or chat application where data transmission volume in one of the key elements, the worst part of this is that a lot of these headers and cookies aren’t actually needed to fulfill the client’s request.
 
What we really need is a way of creating a persistent, low latency connection that can support transactions initiated by either the client or server. This is exactly what Web Sockets provide and in this post you are going to learn all about how to use them in your own applications.

When to use it

Web Socket is an advanced technology that makes it possible to establish an interactive connection between server and client browser’s. With this technology or API, we can send message to a server from client and also can receive an event driven response without having any TCP or HTTP protocol use. ASP.NET SignalR provides a richer application model for real-time functionality, but it runs only on ASP.NET, not ASP.NET Core. A Core version of SignalR is under development. But you might have to develop features that SignalR would provide, such as:

  • Support for a broader range of browser versions by using automatic fallback to alternative transport methods.
  • Automatic reconnection when a connection drops.
  • Support for clients calling methods on the server or vice versa.
  • Support for scaling to multiple servers. 

How to use it

  1. Install the Microsoft.AspNetCore.WebSockets package from Nuget Package Manager.
  2. Configure the middleware.
  3. Accept WebSocket requests.
  4. Send and receive messages

Configure the middleware

Add the WebSockets middleware in the Configure method of the Startup.cs class.

  1. app.UseWebSockets();  

The following settings can be configured,

KeepAliveInterval

How frequently to send "ping" frames to the client, to ensure proxies keep the connection open.

ReceiveBufferSize

The size of the buffer used to receive data. Only advanced users would need to change this, for performance tuning based on the size of their data.

  1. var wsOptions = new WebSocketOptions()  
  2. {  
  3.    KeepAliveInterval = TimeSpan.FromSeconds(120),  
  4.    ReceiveBufferSize = 4 * 1024  
  5. };  
  6. app.UseWebSockets(wsOptions);  

Accept WebSocket requests

Somewhere later in the request life cycle check if it's a Web Socket request and accept the Web Socket request. This example is from later in the Configure method,

  1. app.Use(async (context, next) =>  
  2. {  
  3.    if (context.Request.Path == "/ws")  
  4.    {  
  5.       if (context.WebSockets.IsWebSocketRequest)  
  6.       {  
  7.          WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();  
  8.          await Echo(context, webSocket);  
  9.       }  
  10.       else  
  11.       {  
  12.          context.Response.StatusCode = 400;  
  13.       }  
  14.    }  
  15. else  
  16. {  
  17.    await next();  
  18. }  
  19. });  

Send and receive messages

When we want to transfer message from server to client or vice versa, then we need to call AcceptWebSocketAsync method to upgrades the TCP connection to a WebSocket connection and it gives us a WebSocket object. Use the WebSocket object to send and receive messages. The code receives a message and immediately sends back the same message. It stays in a loop doing that until the client closes the connection.

  1. private async Task Echo(HttpContext context, WebSocket webSocket)  
  2. {  
  3.    var buffer = new byte[1024 * 4];  
  4.    WebSocketReceiveResult wsresult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer),  
  5.    CancellationToken.None);  
  6.    while (!result.CloseStatus.HasValue)  
  7.    {  
  8.       await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), wsresult.MessageType,  
  9.       wsresult.EndOfMessage, CancellationToken.None);  
  10.       wsresult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);  
  11.    }  
  12.    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription,  
  13.    CancellationToken.None);  
  14. }  

When you accept the Web Socket before beginning this loop, the middleware pipeline ends. Upon closing the socket, the pipeline unwinds, that is, the request stops moving forward in the pipeline when you accept a Web Socket, just as it would when you hit an MVC action. But when you finish this loop and close the socket, the request proceeds back up the pipeline.

Program.cs
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.IO;  
  4. using System.Linq;  
  5. using System.Threading.Tasks;  
  6. using Microsoft.AspNetCore.Hosting;  
  7.   
  8. namespace Prog8_WebSocket  
  9. {  
  10.     public class Program  
  11.     {  
  12.         public static void Main(string[] args)  
  13.         {  
  14.             var host = new WebHostBuilder()  
  15.                 .UseKestrel()  
  16.                 .UseContentRoot(Directory.GetCurrentDirectory())  
  17.                 .UseIISIntegration()  
  18.                 .UseStartup<Startup>()  
  19.                 .Build();  
  20.   
  21.             host.Run();  
  22.         }  
  23.     }  
  24. }  
Startup.cs
  1. using Microsoft.AspNetCore.Builder;  
  2. using Microsoft.AspNetCore.Hosting;  
  3. using Microsoft.AspNetCore.Http;  
  4. using Microsoft.Extensions.DependencyInjection;  
  5. using Microsoft.Extensions.Logging;  
  6. using System;  
  7. using System.Net.WebSockets;  
  8. using System.Threading;  
  9. using System.Threading.Tasks;  
  10.   
  11. namespace Prog8_WebSocket  
  12. {  
  13.     public class Startup  
  14.     {  
  15.         public void ConfigureServices(IServiceCollection services)  
  16.         {  
  17.         }  
  18.   
  19.          public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)  
  20.         {  
  21.             loggerFactory.AddConsole(LogLevel.Debug);  
  22.             if (env.IsDevelopment())  
  23.             {  
  24.                 app.UseDeveloperExceptionPage();  
  25.             }  
  26.   
  27.             app.UseWebSockets();  
  28.   
  29.             var webSocketOptions = new WebSocketOptions()  
  30.             {  
  31.                 KeepAliveInterval = TimeSpan.FromSeconds(120),  
  32.                 ReceiveBufferSize = 4 * 1024  
  33.             };  
  34.             app.UseWebSockets(webSocketOptions);  
  35.   
  36.             app.Use(async (context, next) =>  
  37.             {  
  38.                 if (context.Request.Path == "/ws")  
  39.                 {  
  40.                     if (context.WebSockets.IsWebSocketRequest)  
  41.                     {  
  42.                         WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();  
  43.                         await Echo(context, webSocket);  
  44.                     }  
  45.                     else  
  46.                     {  
  47.                         context.Response.StatusCode = 400;  
  48.                     }  
  49.                 }  
  50.                 else  
  51.                 {  
  52.                     await next();  
  53.                 }  
  54.             });  
  55.             app.UseFileServer();  
  56.         }  
  57.   
  58.         private async Task Echo(HttpContext context, WebSocket webSocket)  
  59.         {  
  60.             var buffer = new byte[1024 * 4];  
  61.             WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);  
  62.             while (!result.CloseStatus.HasValue)  
  63.             {  
  64.                 await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);  
  65.                 result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);  
  66.             }  
  67.             await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);  
  68.         }  
  69.   
  70.     }  
  71. }  
 Index.html
  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <meta charset="utf-8" />  
  5.     <title></title>  
  6.     <style>  
  7.         table {  
  8.             border: 0  
  9.         }  
  10.   
  11.         .commslog-data {  
  12.             font-family: Consolas, Courier New, Courier, monospace;  
  13.         }  
  14.   
  15.         .commslog-server {  
  16.             background-color: red;  
  17.             color: white  
  18.         }  
  19.   
  20.         .commslog-client {  
  21.             background-color: green;  
  22.             color: white  
  23.         }  
  24.     </style>  
  25. </head>  
  26. <body>  
  27.     <h1>WebSocket Sample Application</h1>  
  28.     <p id="stateLabel">Ready to connect...</p>  
  29.     <div>  
  30.         <label for="connectionUrl">WebSocket Server URL:</label>  
  31.         <input id="connectionUrl" />  
  32.         <button id="connectButton" type="submit">Connect</button>  
  33.     </div>  
  34.     <p></p>  
  35.     <div>  
  36.         <label for="sendMessage">Message to send:</label>  
  37.         <input id="sendMessage" disabled />  
  38.         <button id="sendButton" type="submit" disabled>Send</button>  
  39.         <button id="closeButton" disabled>Close Socket</button>  
  40.     </div>  
  41.     <h2>Communication Log</h2>  
  42.     <table style="width: 800px">  
  43.         <thead>  
  44.             <tr>  
  45.                 <td style="width: 100px">From</td>  
  46.                 <td style="width: 100px">To</td>  
  47.                 <td>Data</td>  
  48.             </tr>  
  49.         </thead>  
  50.         <tbody id="commsLog"></tbody>  
  51.     </table>  
  52.     <script>  
  53.         var connectionForm = document.getElementById("connectionForm");  
  54.         var connectionUrl = document.getElementById("connectionUrl");  
  55.         var connectButton = document.getElementById("connectButton");  
  56.         var stateLabel = document.getElementById("stateLabel");  
  57.         var sendMessage = document.getElementById("sendMessage");  
  58.         var sendButton = document.getElementById("sendButton");  
  59.         var sendForm = document.getElementById("sendForm");  
  60.         var commsLog = document.getElementById("commsLog");  
  61.         var socket;  
  62.         var scheme = document.location.protocol == "https:" ? "wss" : "ws";  
  63.         var port = document.location.port ? (":" + document.location.port) : "";  
  64.         connectionUrl.value = scheme + "://" + document.location.hostname + port + "/ws";  
  65.   
  66.         function updateState() {  
  67.             function disable() {  
  68.                 sendMessage.disabled = true;  
  69.                 sendButton.disabled = true;  
  70.                 closeButton.disabled = true;  
  71.             }  
  72.   
  73.             function enable() {  
  74.                 sendMessage.disabled = false;  
  75.                 sendButton.disabled = false;  
  76.                 closeButton.disabled = false;  
  77.             }  
  78.   
  79.             connectionUrl.disabled = true;  
  80.             connectButton.disabled = true;  
  81.   
  82.             if (!socket) {  
  83.                 disable();  
  84.             } else {  
  85.                 switch (socket.readyState) {  
  86.                     case WebSocket.CLOSED:  
  87.                         stateLabel.innerHTML = "Closed";  
  88.                         disable();  
  89.                         connectionUrl.disabled = false;  
  90.                         connectButton.disabled = false;  
  91.                         break;  
  92.                     case WebSocket.CLOSING:  
  93.                         stateLabel.innerHTML = "Closing...";  
  94.                         disable();  
  95.                         break;  
  96.                     case WebSocket.CONNECTING:  
  97.                         stateLabel.innerHTML = "Connecting...";  
  98.                         disable();  
  99.                         break;  
  100.                     case WebSocket.OPEN:  
  101.                         stateLabel.innerHTML = "Open";  
  102.                         enable();  
  103.                         break;  
  104.                     default:  
  105.                         stateLabel.innerHTML = "Unknown WebSocket State: " + socket.readyState;  
  106.                         disable();  
  107.                         break;  
  108.                 }  
  109.             }  
  110.         }  
  111.   
  112.         closeButton.onclick = function () {  
  113.             if (!socket || socket.readyState != WebSocket.OPEN) {  
  114.                 alert("socket not connected");  
  115.             }  
  116.             socket.close(1000, "Closing from client");  
  117.         }  
  118.   
  119.         sendButton.onclick = function () {  
  120.             if (!socket || socket.readyState != WebSocket.OPEN) {  
  121.                 alert("socket not connected");  
  122.             }  
  123.             var data = sendMessage.value;  
  124.             socket.send(data);  
  125.             commsLog.innerHTML += '<tr>' +  
  126.                 '<td class="commslog-client">Client</td>' +  
  127.                 '<td class="commslog-server">Server</td>' +  
  128.                 '<td class="commslog-data">' + data + '</td>'  
  129.             '</tr>';  
  130.         }  
  131.   
  132.         connectButton.onclick = function () {  
  133.             stateLabel.innerHTML = "Connecting";  
  134.             socket = new WebSocket(connectionUrl.value);  
  135.             socket.onopen = function (event) {  
  136.                 updateState();  
  137.                 commsLog.innerHTML += '<tr>' +  
  138.                     '<td colspan="3" class="commslog-data">Connection opened</td>' +  
  139.                     '</tr>';  
  140.             };  
  141.   
  142.             socket.onclose = function (event) {  
  143.                 updateState();  
  144.                 commsLog.innerHTML += '<tr>' +  
  145.                     '<td colspan="3" class="commslog-data">Connection closed. Code: ' + event.code + '. Reason: ' + event.reason + '</td>' +  
  146.                     '</tr>';  
  147.             };  
  148.   
  149.             socket.onerror = updateState;  
  150.             socket.onmessage = function (event) {  
  151.                 commsLog.innerHTML += '<tr>' +  
  152.                     '<td class="commslog-server">Server</td>' +  
  153.                     '<td class="commslog-client">Client</td>' +  
  154.                     '<td class="commslog-data">' + event.data + '</td>'  
  155.                 '</tr>';  
  156.             };  
  157.         };  
  158.     </script>  
  159. </body>  
  160. </html>  
Now, run the code from command prompt. The output is shown below.

You can read the next part here,