Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!

How to create real-time chat applications using WebSocket APIs in API Gateway

 

My experience developing real-time messaging applications leveraging AWS services inspires me to share with the community to hope that it helps something learning how to build scalable and reliable real-time messaging applications. The demo project in this article can be deployed using the following AWS services:

1. API Gateway is adaptable to manage the APIs resource.
2. It is the most appropriate procedure when running with multiple EC2 instances behind a load balancer.
3. WebSocket APIs in Amazon API Gateway come in handy to build secure, real-time communication applications without providing or managing servers to handle connections or large-scale data exchanges.

But before diving deeper into the how-to, it’s essential to go over some AWS WebSocket API concepts to facilitate us on the same page.

 

WebSocket API Concepts

What is WebSocket API?

While WebSocket is a computer communication protocol, WebSocket API is defined as advanced technology mainly used to create real-time applications, such as chat apps, collaboration platforms, streaming dashboards. These applications take advantage of two-way/bidirectional communication between a server and users’ browsers.

 

What is WebSocket API in AWS API Gateway?

It is a collection of WebSocket routes and route keys integrated with backend HTTP endpoints, Lambda functions, or other AWS services. Using API Gateway features facilitates all aspects of the API lifecycle, from creation to monitoring your production APIs.

 

What are an AWS WebSocket route and a route key?

A WebSocket route in API Gateway is employed to direct incoming messages to a specific integration. When you elaborate your WebSocket API, a route key and an integration backend are stipulated. The route key is a value of a JSON property in the message body. The integration backend is invoked when the route key is matched in an incoming message.

For example, if your JSON messages embody an action property, and you want to implement various actions based on this property, your route selection expression might be ${request.body.action}. Let’s look at an example of the incoming message from a client:
{"action":"sendMessage","message":"Hello world"}

 

Three predefined routes can be used: $connect, $disconnect, and $default. Also, you are capable of creating custom routes.

  • $default route is a particular routing value that can be used in the fallback route.
  • $connect route is implemented when a persistent connection between a WebSocket API and the client is being initiated.
  • $disconnect route is completed after clients or the server disconnects from API.
  • $custom route is created if you want to invoke a specific integration based on message content.

 

How to create and test a real-time chat application in the development environment

To make it easy to understand, let’s explore a simple chat room application that does the following:

  • Clients take part in the chat room as they connect to the WebSocket API.
  • Users can send messages to the room, and other users can receive a new message in real-time.
  • Disconnected clients are removed from the chat room.

The following diagram illustrated how it works:

Real-time chat architecture

Real-Time Chat Architecture

  • Connect to the chat room:
    (1) Open a WebSocket correlation between clients and servers.
    (2) API Gateway calls the $connect route when having new connections.
    (3) Backend received a callback request from AWS WebSocket via Ngrok that exposes local servers to the public internet in this demo.
    (4) The Connect method of the server API is involved.
    (5) Store a WebSocket connection in the memory of the webserver.
  • Send a new message to the chat room:
    (6) The sendMessage function is involved when users enter a new message into the chat room.
  • Transfer message:
    (7) WebSocket server receives a new message after the app server transmits data to the active connections stored in server memory. It transfers the messages to all connected clients using the new API Gateway Management API.
    (8) Clients are listening for incoming data via the WebSocket.onmessage property.
  • Leave chat room:
    (9) The WebSocket method close() is called from the clients. So API Gateway calls $onDisconnect, then the Disconnect method of the server API is involved and removes that connection out of the server memory.

 

Step 1: Create a WebSocket API

Let’s follow AWS guidelines to create a WebSocket API in API Gateway. Below is the preview of the setting for the demo.

 

Create a WebSocket API

 

Then, set up a WebSocket API integration request:

(1): Selecting a route key to integrate to the backend.

(2): Specifying HTTP endpoint to invoke. For more information about Integration Type, follow this tutorial to Set up a WebSocket API integration request in API Gateway.

(3): Configuring how to transform the incoming message’s payload includes the necessary data before sending them to the back-end integration.

For step (2), we are still not implementing the backend, so there is no HTTP endpoint to invoke. To have another endpoint instead of testing purposes, we will use https://webhook.site to generate a unique URL that quickly inspects any incoming HTTP request.

Next, we follow the steps for the WebSocket API integration request above; the details of the $connect route’s configuration are shown below.

 

WebSocket API integration

 

For the request template, the ID is stored in the server’s memory when a new connection integrates with the server. For more information about those parameters, you can read API Gateway WebSocket API mapping template references.

Similar to the $connect route configuration, we have the following one for the $disconnect route.

 

Configuration of $disconnect API

 

And $sendMessage will be configured like this:

 

Configuration for Sendmessage

The connection and message property are required before sending it through to the server.

 

Step 2: Set up a WebSocket API integration responses

We must set up at least one integration response because the non-proxy integration is being used at the integration request. You can read more about Setting up WebSocket API integration responses in API Gateway.

Here is the configuration for the $connect route key. Every value is initialized by default.

 

Set up a WebSocket API integration responses

 

Step 3: Deploy the WebSocket API

For the first-time deployment, we need to create a stage, such as “dev,” and give a sample description.

To test a WebSocket API, use wscat to connect to a WebSocket API and send messages to it. You can refer to the article for how to use wscat to connect to a WebSocket API and send messages to it.

 

Implement backend code

After creating a web API with ASP.NET Core, add a new controller WebSocketDemoController that has implemented the three methods Connect, Disconnect, SendMessage that map with 3 WebSocket API route keys $connect, $disconnect, $sendMessage.

[Route("websocket")]
[ApiController]
public class WebSocketDemoController : ControllerBase
	{
		/// <summary>
		/// The memory storage for the list of connectionIds
		/// </summary>
		private static readonly List<string> ConnectionIds = new List<string>();
		
		private readonly IWebSocketService _webSocketService;

		public WebSocketDemoController(IWebSocketService webSocketService)
		{
			_webSocketService = webSocketService;
		}

		/// <summary>
		/// Having a new connection from the client.
		/// </summary>
		[HttpPost("connect")]
		[AllowAnonymous]
		public void Connect([FromBody] ConnectionModel connection)
		{
			// Store connectionId in memory data
			ConnectionIds.Add(connection.Id);

			// The connection.UserId can be used to identify which user using that connectionId
		}

		/// <summary>
		/// There is a disconnected connection from the client.
		/// </summary>
		[HttpPost("disconnect")]
		[AllowAnonymous]
		public void Disconnect([FromBody] ConnectionModel connection)
		{
			// Remove connectionId out of memory data
			ConnectionIds.Remove(connection.Id);
		}

		/// <summary>
		/// The clients send messages to the server
		/// </summary>
		[HttpPost("message")]
		[AllowAnonymous]
		public async Task SendMessage([FromBody] TextMessageModel input)
		{
			// Get the connectionIds of other users in the chat room
			var connectionIds = ConnectionIds.Where(id => id != input.ConnectionId).ToList();

			// Sends the message to all connected clients using the new API Gateway Management API 
		        // that excepts the current connectionId
			await _webSocketService.SendMessage(connectionIds, "newMessage", input.Message);
		}
	}

For the Connect method, the ConnectionModel class will be mapped with the Request Template of $connect from the WebSocket API integration request.

public class ConnectionModel
{
    public string Id { get; set; }
    public string Domain { get; set; }
    public string Stage { get; set; }
}

Similarly, the TextMessageModel class will be mapped with the Request Template of the $sendMessage route.

public class TextMessageModel.
{
    public string ConnectionId { get; set; }
    public string Message { get; set; }
}

Once the backend API works, we will use backend APIs defined on WebSocketDemoController for the HTTP endpoint instead of the URL of WebSite.Hook.

 

Implement frontend code

Create an index.html file with the source code below.

<h1>Chat Room</h1>
<pre id="messages" style="height: 400px; overflow-y: scroll"></pre>
<input type="text" id="messageBox" placeholder="Type your message here" style="display: block; width: 100%; margin-bottom: 10px; padding: 10px;" />
<button id="send" title="Send Message!" style="width: 100%; height: 30px;">Send Message</button>

<script>
  (function() {
    const sendBtn = document.querySelector('#send');
    const messages = document.querySelector('#messages');
    const messageBox = document.querySelector('#messageBox');

    let ws;

    function showMessage(message, isOutgoing) {
      messages.textContent += isOutgoing ? `\n\n You: ${message}` : `\n\n Another User: ${message}`;
      messages.scrollTop = messages.scrollHeight;
      messageBox.value = '';
    }

    function init() {
      if (ws) {
        ws.onerror = ws.onopen = ws.onclose = null;
        ws.close();
      }

      // connect to Websocket server
      ws = new WebSocket("your_websocket_endpoint");
      ws.onopen = (e) => {
        console.log('Connection opened!');
      }
	  
      // An event listener to be called when a message is received from the server
      ws.onmessage = function (e) {
        var data = JSON.parse(e.data);
        showMessage(data.payload, false);
      }
	 
      // An event listener to be called when the connection is closed.
      ws.onclose = function() {
        ws = null;
        console.log('Connection closed!');
        }
      }

      sendBtn.onclick = function() {
        if (!ws) {
          showMessage("No WebSocket connection :(");
          return ;
        }


        var data = JSON.stringify({
        "action": "sendMessage",
        "message": messageBox.value
          });

        // Send a new message to the sendMessage route key
        ws.send(data);

        // Show the outgoing message
        showMessage(messageBox.value, true);
      }

    init();
  })();
</script>

 

Step 4: Demo

To demo the app, we run the index.html files in two browser tabs: a normal tab and an incognito tab as two different users.

 

Demo real-time application using WebSocket API

For a better understanding, you can discover my demo source code here.

 

Essential Notes for WebSocket APIs

API Gateway can handle messages up to 128 KB with a maximum size of 32 KB per frame. If a message is larger than 32 KB, you must divide it into multiple frames, each 32 KB or smaller. If a larger message is attempted to send, the connection will be closed with code 1009.

 

Final thoughts

I recommend using AWS Websocket APIs for real-time applications rather than building your own WebSocket APIs. Because it is stable on production, and you no longer worry about hosting, load balancing, and scalability in the future. You, therefore, can focus on implementing other essential features to serve your users. Another alternative is to use Azure SignalR Service from Microsoft Azure for the same purpose.

I hope this article is helpful to you. Happy coding!

 

CTA Enlab Software

 

References

About the author

Trong Pham

I’m a Software Engineer at Enlab Software. I am interested in .NET programming, SQL Server, and system architectures. What motivates me to code and create software products is helping people improve their business efficiency. When not at work, I'm into playing music or football.

Up Next

Big Data Technologies Transforming Software Development
July 05, 2024 by Dat Le
In the rapidly evolving world of software development, Big Data stands out as a transformative force....
June 27, 2024 by Dat Le
In today's rapidly evolving digital landscape, secure coding practices are paramount to safeguarding applications from a...
June 20, 2024 by Dat Le
In the rapidly evolving digital landscape, the role of User Interface (UI) and User Experience (UX)...
Leveraging UX Design Principles in Software Development
June 17, 2024 by Dat Le
In the dynamic world of software development, one element has emerged as crucial to success: User...
Roll to Top

Can we send you our next blog posts? Only the best stuffs.

Subscribe