Building a Real-Time Chat Application with ASP.NET Core and WebSockets

Hello Everyone, 

Hope you are doing well. Today we will delve into one of the communication protocols.

WebSocket provides a way to create a persistent, full-duplex communication channel over a single TCP connection, allowing data to flow in both directions simultaneously. This makes it especially useful for real-time, interactive applications such as live chat systems, gaming, and real-time financial data services.

In this article, we'll explore how to create a real-time chat application using ASP.NET Core for the server-side logic and static HTML for the client-side interface. This combination provides a powerful and efficient method for implementing bi-directional communication between a client and a server.

WebSockets provide a way to open interactive communication sessions between the user's browser and a server. With WebSockets, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.

Overview

Here’s what we’ll cover,

  1. Setting up the Server: Using ASP.NET Core to handle WebSocket requests and manage connections.
  2. Client-Side Implementation: Creating a simple HTML interface to send and receive messages.
  3. Server-Client Interaction: How messages are exchanged and displayed in real-time.
  4. Testing an application: Demonstrate the working application.
  5. Advantage Vs Disadvantage: WebSockets provide a unique capability for real-time, full-duplex communication on the web, which can be essential for certain types of applications. However, like any technology, they come with both advantages and disadvantages.

Server-Side Setup with ASP.NET Core

Implementation Details

  • ASP.NET Core Setup: Begin by setting up an ASP.NET Core project with the necessary services and middleware for hosting a WebSocket server.
  • WebSocket Management: Handle WebSocket requests, establish connections, and manage ongoing communications.

Code Explanation

  1. Initialize Core Services
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllersWithViews();
    
    var app = builder.Build();
  2. Middleware Configuration
    if (app.Environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseWebSockets();
    
    // Dictionary to store WebSocket connections
    ConcurrentDictionary<string, WebSocket> webSockets = new ConcurrentDictionary<string, WebSocket>();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
  3. Request Handling
    app.Use(async (context, next) =>
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            var connectionId = Guid.NewGuid().ToString();
            webSockets.TryAdd(connectionId, webSocket);
    
            await HandleWebSocketCommunication(context, webSocket, connectionId);
            webSockets.TryRemove(connectionId, out var _);
        }
        else
        {
            await next.Invoke();
        }
    });
    
    app.Run();
  4. WebSocket Communication Handling
    async Task HandleWebSocketCommunication(HttpContext context, WebSocket webSocket, string connectionId)
    {
        var buffer = new byte[1024 * 4];
        WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    
        while (!result.CloseStatus.HasValue)
        {
            var receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
            await Task.Delay(3000); // Simulate processing delay
    
            var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
            var responseMessage = $"[{timestamp}] {connectionId}: {receivedMessage}";
            var responseBuffer = Encoding.UTF8.GetBytes(responseMessage);
    
            await webSocket.SendAsync(
                new ArraySegment<byte>(responseBuffer, 0, responseBuffer.Length),
                WebSocketMessageType.Text,
                true,
                CancellationToken.None
            );
    
            result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        }
    
        await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
    }

This function handles the lifecycle of a WebSocket connection including receiving data from the client, processing this data (potentially adding a timestamp), and sending a response back.

Client-Side Implementation

The client-side of the application uses basic HTML and JavaScript to establish WebSocket connections and interact with the server.

HTML Setup

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Chat Example</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 10px;
        }

        #messageArea {
            height: 300px;
            width: 300px;
            overflow-y: scroll;
            border: 1px solid #ccc;
            padding: 5px;
        }

        #inputArea {
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <h2>WebSocket Chat Example</h2>

    <div id="messageArea"></div>
    <div id="inputArea">
        <input type="text" id="messageInput" placeholder="Type a message...">
        <button onclick="sendMessage();">Send</button>
    </div>

    <script>
        let websocket;

        function connectWebSocket() {
            const protocolPrefix = (window.location.protocol === 'https:') ? 'wss:' : 'ws:';
            const port = window.location.port ? ':' + window.location.port : '';
            const wsUrl = `${protocolPrefix}//${window.location.hostname}${port}/ws`;

            websocket = new WebSocket(wsUrl);

            websocket.onopen = function () {
                displayMessage('[System] Connected to WebSocket server.');
            };

            websocket.onmessage = function (evt) {
                const timestamp = formatTimestamp(new Date());
                displayMessage(`[${timestamp}] ${evt.data}`);
            };

            websocket.onerror = function (evt) {
                displayMessage('[System] WebSocket error: ' + evt.data);
            };

            websocket.onclose = function () {
                displayMessage('[System] WebSocket connection closed.');
            };
        }

        function sendMessage() {
            const messageInput = document.getElementById('messageInput');
            const message = messageInput.value;
            const timestamp = formatTimestamp(new Date());
            displayMessage(`[${timestamp}] You: ${message}`);
            websocket.send(message);
            messageInput.value = '';
        }

        function displayMessage(message) {
            const messageArea = document.getElementById('messageArea');
            messageArea.innerHTML += message + '<br>';
            messageArea.scrollTop = messageArea.scrollHeight;
        }

        function formatTimestamp(date) {
            return date;
        }

        window.onload = connectWebSocket;
    </script>
</body>
</html>

Establishing a WebSocket Connection

  1. URL Construction
    • WebSockets use ws:// for unencrypted connections or wss:// for encrypted connections (similar to http:// and https://).
    • The URL can also include a specific port and endpoint, typically decided by the server's configuration.
  2. Initialization
    • In the client's code (JavaScript), a WebSocket object is instantiated using the constructed URL. This is where the attempt to connect to the WebSocket server begins.
  3. Initial Handshake
    • The client sends a WebSocket handshake request using standard HTTP upgrade headers that include details necessary to initiate the WebSocket protocol.
    • The server, upon recognizing the upgrade request to WebSocket, responds accordingly if it supports WebSockets, hence upgrading the HTTP connection to a WebSocket connection.
  4. Connection Establishment
    • Once the handshake is successful, an open connection is established, triggering the open event in the client’s WebSocket instance.
    • The server and client can now exchange data freely in a bidirectional manner, firing message events for incoming data.
  5. Handlers
    • Custom event handlers (onopen, onmessage, onerror, onclose) are implemented to appropriately manage various phases of the connection lifecycle such as opening, message receipt, errors, and closing of the connection.
  6. Communication
    • The client and server can now send data back and forth as often as needed with minimal overhead.
  7. Closure
    • Either party can close the WebSocket connection by sending a close message, which is automatically negotiated between the client and the server.

Testing an application

Run the Project in Visual Studio and open the index.html.It will give you an interactive chat box. when user enter the message, the server will respond after 3 second with Guid. For each session, it has a different guide as we can see in the below example. This example illustrates the real-time duplex communication between client and server.

Test application

Advantage Vs Disadvantage
 

Advantages Disadvantages
Real-Time Communication Complexity in Scalability
Allows for live, instant data exchange Managing large-scale deployments can be complex due to persistent connections
Reduced Overhead Limited Browser Support
No need for an HTTP header on each message Older browsers may not support WebSockets
Full Duplex Communication Security Concerns
Simultaneous bi-directional messaging Needs careful configuration to secure WebSocket connections, especially over wss://
Efficient Use of Resources Compatibility with Proxies and Firewalls
Persistent connection reduces the need for repeated handshakes and connection teardowns Some proxies and firewalls may not properly support WebSockets
Fallback Mechanisms Not RESTful
Can degrade to HTTP polling methods if necessary Does not conform to REST architecture, which can complicate API design and integration
Persistent Connection Risk of Resource Exhaustion
Keeps the connection open, avoiding the continuous open/close in HTTP Maintaining a large number of open connections can exhaust server resources


Conclusion

WebSockets offer a highly efficient and real-time communication method for modern web applications, eliminating the lag and overhead associated with traditional HTTP requests. By establishing a persistent, two-way interactive channel between the client and the server, WebSockets enable immediate data exchanges ideal for applications requiring rapid updates such as live chat systems, gaming, or financial trading platforms. Implementing WebSockets enhances user experience due to smoother interactions and quicker responsiveness, emphasizing the importance of understanding and utilizing this technology to its full potential in creating responsive and dynamic web applications.


Similar Articles