Tutorial: Create a chat app with Azure Web PubSub service
Article
In Publish and subscribe message tutorial, you've learned the basics of publishing and subscribing messages with Azure Web PubSub. In this tutorial, you'll learn the event system of Azure Web PubSub so use it to build a complete web application with real-time communication functionality.
In this tutorial, you learn how to:
Create a Web PubSub service instance
Configure event handler settings for Azure Web PubSub
If you prefer to run CLI reference commands locally, install the Azure CLI. If you're running on Windows or macOS, consider running Azure CLI in a Docker container. For more information, see How to run the Azure CLI in a Docker container.
If you're using a local installation, sign in to the Azure CLI by using the az login command. To finish the authentication process, follow the steps displayed in your terminal. For other sign-in options, see Sign in with the Azure CLI.
When you're prompted, install the Azure CLI extension on first use. For more information about extensions, see Use extensions with the Azure CLI.
Run az version to find the version and dependent libraries that are installed. To upgrade to the latest version, run az upgrade.
This setup requires version 2.22.0 or higher of the Azure CLI. If using Azure Cloud Shell, the latest version is already installed.
Create an Azure Web PubSub instance
Create a resource group
A resource group is a logical container into which Azure resources are deployed and managed. Use the az group create command to create a resource group named myResourceGroup in the eastus location.
az group create --name myResourceGroup --location EastUS
Create a Web PubSub instance
Run az extension add to install or upgrade the webpubsub extension to the current version.
az extension add --upgrade --name webpubsub
Use the Azure CLI az webpubsub create command to create a Web PubSub in the resource group you've created. The following command creates a Free Web PubSub resource under resource group myResourceGroup in EastUS:
Important
Each Web PubSub resource must have a unique name. Replace <your-unique-resource-name> with the name of your Web PubSub in the following examples.
The output of this command shows properties of the newly created resource. Take note of the two properties listed below:
Resource Name: The name you provided to the --name parameter above.
hostName: In the example, the host name is <your-unique-resource-name>.webpubsub.azure.com/.
At this point, your Azure account is the only one authorized to perform any operations on this new resource.
Get the ConnectionString for future use
Important
A connection string includes the authorization information required for your application to access Azure Web PubSub service. The access key inside the connection string is similar to a root password for your service. In production environments, always be careful to protect your access keys. Use Azure Key Vault to manage and rotate your keys securely. Avoid distributing access keys to other users, hard-coding them, or saving them anywhere in plain text that is accessible to others. Rotate your keys if you believe they may have been compromised.
Use the Azure CLI az webpubsub key command to get the ConnectionString of the service. Replace the <your-unique-resource-name> placeholder with the name of your Azure Web PubSub instance.
az webpubsub key show --resource-group myResourceGroup --name <your-unique-resource-name> --query primaryConnectionString --output tsv
Copy the connection string to use later.
Copy the fetched ConnectionString and it will be used later in this tutorial as the value of <connection_string>.
In Azure Web PubSub, there are two roles, server and client. This concept is similar to the server and client roles in a web application. Server is responsible for managing the clients, listen, and respond to client messages, while client's role is to send user's messages to server, and receive messages from server and visualize them to end user.
In this tutorial, we'll build a real-time chat web application. In a real web application, server's responsibility also includes authenticating clients and serving static web pages for the application UI.
Also create an HTML file and save it as wwwroot/index.html, we'll use it for the UI of the chat app later.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
You can test the server by running dotnet run --urls http://localhost:8080 and access http://localhost:8080/index.html in browser.
You may remember in the publish and subscribe message tutorial the subscriber uses an API in Web PubSub SDK to generate an access token from connection string and use it to connect to the service. This is usually not safe in a real world application as connection string has high privilege to do any operation to the service so you don't want to share it with any client. Let's change this access token generation process to a REST API at server side, so client can call this API to request an access token every time it needs to connect, without need to hold the connection string.
Install dependencies.
dotnet add package Microsoft.Extensions.Azure
Add a Sample_ChatApp class to handle hub events. Add DI for the service middleware and service client. Don't forget to replace <connection_string> with the one of your services.
using Microsoft.Azure.WebPubSub.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddWebPubSub(
o => o.ServiceEndpoint = new ServiceEndpoint("<connection_string>"))
.AddWebPubSubServiceClient<Sample_ChatApp>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
});
app.Run();
sealed class Sample_ChatApp : WebPubSubHub
{
}
AddWebPubSubServiceClient<THub>() is used to inject the service client WebPubSubServiceClient<THub>, with which we can use in negotiation step to generate client connection token and in hub methods to invoke service REST APIs when hub events are triggered.
Add a /negotiate API to the server inside app.UseEndpoints to generate the token.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/negotiate", async (WebPubSubServiceClient<Sample_ChatApp> serviceClient, HttpContext context) =>
{
var id = context.Request.Query["id"];
if (id.Count != 1)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("missing user id");
return;
}
await context.Response.WriteAsync(serviceClient.GetClientAccessUri(userId: id).AbsoluteUri);
});
});
This token generation code is similar to the one we used in the publish and subscribe message tutorial, except we pass one more argument (userId) when generating the token. User ID can be used to identify the identity of client so when you receive a message you know where the message is coming from.
You can test this API by running dotnet run --urls http://localhost:8080 and accessing http://localhost:8080/negotiate?id=<user-id> and it will give you the full url of the Azure Web PubSub with an access token.
Then update index.html to include the following script to get the token from server and connect to service.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let url = await res.text();
let ws = new WebSocket(url);
ws.onopen = () => console.log('connected');
})();
</script>
</html>
If you are using Chrome, you can test it by opening the home page, input your user name. Press F12 to open the Developer Tools window, switch to Console table and you'll see connected being printed in browser console.
We'll use express.js, a popular web framework for Node.js to achieve this job.
First create an empty express app.
Install express.js
npm init -y
npm install --save express
Then create an express server and save it as server.js
Also create an HTML file and save it as public/index.html, we'll use it for the UI of the chat app later.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
You can test the server by running node server and access http://localhost:8080 in browser.
You may remember in the publish and subscribe message tutorial the subscriber uses an API in Web PubSub SDK to generate an access token from connection string and use it to connect to the service. This is usually not safe in a real world application as connection string has high privilege to do any operation to the service so you don't want to share it with any client. Let's change this access token generation process to a REST API at server side, so client can call this API to request an access token every time it needs to connect, without need to hold the connection string.
Install Azure Web PubSub SDK
npm install --save @azure/web-pubsub
Add a /negotiate API to the server to generate the token
const express = require('express');
const { WebPubSubServiceClient } = require('@azure/web-pubsub');
const app = express();
const hubName = 'Sample_ChatApp';
const port = 8080;
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
app.get('/negotiate', async (req, res) => {
let id = req.query.id;
if (!id) {
res.status(400).send('missing user id');
return;
}
let token = await serviceClient.getClientAccessToken({ userId: id });
res.json({
url: token.url
});
});
app.use(express.static('public'));
app.listen(8080, () => console.log('server started'));
This token generation code is similar to the one we used in the publish and subscribe message tutorial, except we pass one more argument (userId) when generating the token. User ID can be used to identify the identity of client so when you receive a message you know where the message is coming from.
Run the below command to test this API:
export WebPubSubConnectionString="<connection-string>"
node server
Access http://localhost:8080/negotiate?id=<user-id> and it will give you the full url of the Azure Web PubSub with an access token.
Then update index.html with the following script to get the token from server and connect to service
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let data = await res.json();
let ws = new WebSocket(data.url);
ws.onopen = () => console.log('connected');
})();
</script>
</html>
If you are using Chrome, you can test it by opening the home page, input your user name. Press F12 to open the Developer Tools window, switch to Console table and you'll see connected being printed in browser console.
We will use the Javalin web framework to host the web pages and handle incoming requests.
First let's use Maven to create a new app webpubsub-tutorial-chat and switch into the webpubsub-tutorial-chat folder:
Let's navigate to the /src/main/java/com/webpubsub/tutorial directory, open the App.java file in your editor, use Javalin.create to serve static files:
package com.webpubsub.tutorial;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
// start a server
Javalin app = Javalin.create(config -> {
config.addStaticFiles("public");
}).start(8080);
}
}
Depending on your setup, you might need to explicitly set the language level to Java 8. This can be done in the pom.xml. Add the following snippet:
You may remember in the publish and subscribe message tutorial the subscriber uses an API in Web PubSub SDK to generate an access token from connection string and use it to connect to the service. This is usually not safe in a real world application as connection string has high privilege to do any operation to the service so you don't want to share it with any client. Let's change this access token generation process to a REST API at server side, so client can call this API to request an access token every time it needs to connect, without need to hold the connection string.
Add Azure Web PubSub SDK dependency into the dependencies node of pom.xml:
Add a /negotiate API to the App.java file to generate the token:
package com.webpubsub.tutorial;
import com.azure.messaging.webpubsub.WebPubSubServiceClient;
import com.azure.messaging.webpubsub.WebPubSubServiceClientBuilder;
import com.azure.messaging.webpubsub.models.GetClientAccessTokenOptions;
import com.azure.messaging.webpubsub.models.WebPubSubClientAccessToken;
import com.azure.messaging.webpubsub.models.WebPubSubContentType;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Expecting 1 arguments: <connection-string>");
return;
}
// create the service client
WebPubSubServiceClient service = new WebPubSubServiceClientBuilder()
.connectionString(args[0])
.hub("Sample_ChatApp")
.buildClient();
// start a server
Javalin app = Javalin.create(config -> {
config.addStaticFiles("public");
}).start(8080);
// Handle the negotiate request and return the token to the client
app.get("/negotiate", ctx -> {
String id = ctx.queryParam("id");
if (id == null) {
ctx.status(400);
ctx.result("missing user id");
return;
}
GetClientAccessTokenOptions option = new GetClientAccessTokenOptions();
option.setUserId(id);
WebPubSubClientAccessToken token = service.getClientAccessToken(option);
ctx.result(token.getUrl());
return;
});
}
}
This token generation code is similar to the one we used in the publish and subscribe message tutorial, except we call setUserId method to set the user ID when generating the token. User ID can be used to identify the identity of client so when you receive a message you know where the message is coming from.
You can test this API by running the following command, replacing <connection_string> with the ConnectionString fetched in previous step, and accessing http://localhost:8080/negotiate?id=<user-id> and it will give you the full url of the Azure Web PubSub with an access token.
Then update index.html with the following script to get the token from the server and connect to the service.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let url = await res.text();
let ws = new WebSocket(url);
ws.onopen = () => console.log('connected');
})();
</script>
</html>
If you are using Chrome, you can test it by opening the home page, input your user name. Press F12 to open the Developer Tools window, switch to Console table and you'll see connected being printed in browser console.
Handle events
In Azure Web PubSub, when there are certain activities happening at client side (for example a client is connected or disconnected), service will send notifications to server so it can react to these events.
Events are delivered to server in the form of Webhook. Webhook is served and exposed by the application server and registered at the Azure Web PubSub service side. The service invokes the webhooks whenever an event happens.
Azure Web PubSub follows CloudEvents to describe the event data.
Here we're using Web PubSub middleware SDK, there is already an implementation to parse and process CloudEvents schema, so we don't need to deal with these details. Instead, we can focus on the inner business logic in the hub methods.
Add event handlers inside UseEndpoints. Specify the endpoint path for the events, let's say /eventhandler. The UseEndpoints should look like follows:
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/negotiate", async (WebPubSubServiceClient<Sample_ChatApp> serviceClient, HttpContext context) =>
{
var id = context.Request.Query["id"];
if (id.Count != 1)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("missing user id");
return;
}
await context.Response.WriteAsync(serviceClient.GetClientAccessUri(userId: id).AbsoluteUri);
});
endpoints.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");
});
Go the Sample_ChatApp we created in previous step. Add a constructor to work with WebPubSubServiceClient<Sample_ChatApp> so we can use to invoke service. And override OnConnectedAsync() method to respond when connected event is triggered.
In the above code, we use the service client to broadcast a notification message to all of whom is joined.
If you use Web PubSub SDK, there is already an implementation to parse and process CloudEvents schema so you don't need to deal with these details.
Add the following code to expose a REST API at /eventhandler (which is done by the express middleware provided by Web PubSub SDK) to handle the client connected event:
In the above code, we simply print a message to console when a client is connected. You can see we use req.context.userId so we can see the identity of the connected client.
For now, you need to implement the event handler by your own in Java, the steps are straight forward following the protocol spec and illustrated below.
Add HTTP handler for the event handler path, let's say /eventhandler.
First we'd like to handle the abuse protection OPTIONS requests, we check if the header contains WebHook-Request-Origin header, and we return the header WebHook-Allowed-Origin. For simplicity for demo purpose, we return * to allow all the origins.
Then we'd like to check if the incoming requests are the events we expect. Let's say we now care about the system connected event, which should contain the header ce-type as azure.webpubsub.sys.connected. We add the logic after abuse protection:
In the above code, we simply print a message to console when a client is connected. You can see we use ctx.header("ce-userId") so we can see the identity of the connected client.
Set up the event handler
Expose localhost
Then we need to set the Webhook URL in the service so it can know where to call when there is a new event. But there is a problem that our server is running on localhost so does not have an internet accessible endpoint. There are several tools available on the internet to expose localhost to the internet, for example, ngrok, loophole, or TunnelRelay. Here we use ngrok.
First download ngrok from https://ngrok.com/download, extract the executable to your local folder or your system bin folder.
Start ngrok
ngrok http 8080
ngrok will print a URL (https://<domain-name>.ngrok.io) that can be accessed from internet. In above step we listens the /eventhandler path, so next we'd like the service to send events to https://<domain-name>.ngrok.io/eventhandler.
Set event handler
Then we update the service event handler and set the Webhook URL to https://<domain-name>.ngrok.io/eventhandler. Event handlers can be set from either the portal or the CLI as described in this article, here we set it through CLI.
Use the Azure CLI az webpubsub hub create command to create the event handler settings for the chat hub
Important
Replace <your-unique-resource-name> with the name of your Web PubSub resource created from the previous steps.
Replace <domain-name> with the name ngrok printed.
After the update is completed, open the home page http://localhost:8080/index.html, input your user name, you’ll see the connected message printed in the server console.
Handle Message events
Besides system events like connected or disconnected, client can also send messages through the WebSocket connection and these messages will be delivered to server as a special type of event called message event. We can use this event to receive messages from one client and broadcast them to all clients so they can talk to each other.
This event handler uses WebPubSubServiceClient.SendToAllAsync() to broadcast the received message to all clients. You can see in the end we returned UserEventResponse, which contains a message directly to the caller and make the WebHook request success. If you have extra logic to validate and would like to break this call, you can throw an exception here. The middleware will deliver the exception message to service and service will drop current client connection. Do not forget to include the using Microsoft.Azure.WebPubSub.Common; statement at the begining of the Program.cs file.
Update index.html to add the logic to send message from user to server and display received messages in the page.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
<input id="message" placeholder="Type to chat...">
<div id="messages"></div>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let url = await res.text();
let ws = new WebSocket(url);
ws.onopen = () => console.log('connected');
let messages = document.querySelector('#messages');
ws.onmessage = event => {
let m = document.createElement('p');
m.innerText = event.data;
messages.appendChild(m);
};
let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
if (e.charCode !== 13) return;
ws.send(message.value);
message.value = '';
});
})();
</script>
</body>
</html>
You can see in the above code we use WebSocket.send() to send message and WebSocket.onmessage to listen to message from service.
Now run the server using dotnet run --urls http://localhost:8080 and open multiple browser instances to access http://localhost:8080/index.html, then you can chat with each other.
The complete code sample of this tutorial can be found here, the ASP.NET Core 3.1 version here.
This event handler uses WebPubSubServiceClient.sendToAll() to broadcast the received message to all clients.
You can see handleUserEvent also has a res object where you can send message back to the event sender. Here we simply call res.success() to make the WebHook return 200 (note this call is required even you don't want to return anything back to client, otherwise the WebHook never returns and client connection will be closed).
Update index.html to add the logic to send message from user to server and display received messages in the page.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
<input id="message" placeholder="Type to chat...">
<div id="messages"></div>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let data = await res.json();
let ws = new WebSocket(data.url);
ws.onopen = () => console.log('connected');
let messages = document.querySelector('#messages');
ws.onmessage = event => {
let m = document.createElement('p');
m.innerText = event.data;
messages.appendChild(m);
};
let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
if (e.charCode !== 13) return;
ws.send(message.value);
message.value = '';
});
})();
</script>
</body>
</html>
You can see in the above code we use WebSocket.send() to send message and WebSocket.onmessage to listen to message from service.
sendToAll accepts object as an input and send JSON text to the clients. In real scenarios, we probably need complex object to carry more information about the message. Finally update the handlers to broadcast JSON objects to all clients:
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
<input id="message" placeholder="Type to chat...">
<div id="messages"></div>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let data = await res.json();
let ws = new WebSocket(data.url);
ws.onopen = () => console.log('connected');
let messages = document.querySelector('#messages');
ws.onmessage = event => {
let m = document.createElement('p');
let data = JSON.parse(event.data);
m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
messages.appendChild(m);
};
let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
if (e.charCode !== 13) return;
ws.send(message.value);
message.value = '';
});
})();
</script>
</body>
</html>
Now run the server and open multiple browser instances, then you can chat with each other.
The complete code sample of this tutorial can be found here.
The ce-type of message event is always azure.webpubsub.user.message, details see Event message.
This event handler uses client.sendToAll() to broadcast the received message to all clients.
Update index.html to add the logic to send message from user to server and display received messages in the page.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
<input id="message" placeholder="Type to chat...">
<div id="messages"></div>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let url = await res.text();
let ws = new WebSocket(url);
ws.onopen = () => console.log('connected');
let messages = document.querySelector('#messages');
ws.onmessage = event => {
let m = document.createElement('p');
m.innerText = event.data;
messages.appendChild(m);
};
let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
if (e.charCode !== 13) return;
ws.send(message.value);
message.value = '';
});
})();
</script>
</body>
</html>
You can see in the above code we use WebSocket.send() to send message and WebSocket.onmessage to listen to message from service.
Finally update the connected event handler to broadcast the connected event to all clients so they can see who joined the chat room.