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
- 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.
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.
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.
And $sendMessage will be configured like this:
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.
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.
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!
References
- AWS Compute Blog, Announcing WebSocket APIs in Amazon API Gateway,www.aws.amazon.com/blogs, 2018.
- Amazon Web Services, Working with WebSocket APIs, www.docs.aws.amazon.com.
- Amazon Web Services, Set up HTTP integrations in API Gateway, www.docs.aws.amazon.com.
- Amazon Web Services, API Gateway WebSocket API mapping template reference, www.docs.aws.amazon.com.