Tutorial: Build chat app with Azure Function in Serverless Mode (Preview)
This tutorial walks you through how to create a Web PubSub for Socket.IO service in Serverless Mode and build a chat app integrating with Azure Function.
Find full code samples that are used in this tutorial:
Important
Default Mode needs a persistent server, you cannot integration Web PubSub for Socket.IO in default mode with Azure Function.
Prerequisites
- An Azure account with an active subscription. If you don't have one, you can create a free account.
- Azure Function core tool
- Some familiarity with the Socket.IO library.
Create a Web PubSub for Socket.IO resource in Serverless Mode
To create a Web PubSub for Socket.IO, you can use the following Azure CLI command:
az webpubsub create -g <resource-group> -n <resource-name>--kind socketio --service-mode serverless --sku Premium_P1
Create an Azure Function project locally
You should follow the steps to initiate a local Azure Function project.
Follow to step to install the latest Azure Function core tool
In the terminal window or from a command prompt, run the following command to create a project in the
SocketIOProject
folder:func init SocketIOProject --worker-runtime javascript --model V4
This command creates a JavaScript project. And enter the folder
SocketIOProject
to run the following commands.Currently, the Function Bundle doesn't include Socket.IO Function Binding, so you need to manually add the package.
To eliminate the function bundle reference, edit the host.json file and remove the following lines.
"extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[4.*, 5.0.0)" }
Run the command:
func extensions install -p Microsoft.Azure.WebJobs.Extensions.WebPubSubForSocketIO -v 1.0.0-beta.4
Create a function for negotiation. The negotiation function used for generating endpoints and tokens for client to access the service.
func new --template "Http Trigger" --name negotiate
Open the file in
src/functions/negotiate.js
and replace with the following code:const { app, input } = require('@azure/functions'); const socketIONegotiate = input.generic({ type: 'socketionegotiation', direction: 'in', name: 'result', hub: 'hub' }); async function negotiate(request, context) { let result = context.extraInputs.get(socketIONegotiate); return { jsonBody: result }; }; // Negotiation app.http('negotiate', { methods: ['GET', 'POST'], authLevel: 'anonymous', extraInputs: [socketIONegotiate], handler: negotiate });
This step creates a function
negotiate
with HTTP Trigger andSocketIONegotiation
output binding, which means you can use an HTTP call to trigger the function and return a negotiation result that generated bySocketIONegotiation
binding.Create a function for handing messages.
func new --template "Http Trigger" --name message
Open the file
src/functions/message.js
and replace with the following code:const { app, output, trigger } = require('@azure/functions'); const socketio = output.generic({ type: 'socketio', hub: 'hub', }) async function chat(request, context) { context.extraOutputs.set(socketio, { actionName: 'sendToNamespace', namespace: '/', eventName: 'new message', parameters: [ context.triggerMetadata.socketId, context.triggerMetadata.message ], }); } // Trigger for new message app.generic('chat', { trigger: trigger.generic({ type: 'socketiotrigger', hub: 'hub', eventName: 'chat', parameterNames: ['message'], }), extraOutputs: [socketio], handler: chat });
This uses
SocketIOTrigger
to get triggered by a Socket.IO client message and useSocketIO
binding to broadcast messages to namespace.Create a function to return an index html for visiting.
Create a folder
public
undersrc/
.Create an html file
index.html
with the following content.<html> <body> <h1>Socket.IO Serverless Sample</h1> <div id="chatPage" class="chat-container"> <div class="chat-input"> <input type="text" id="chatInput" placeholder="Type your message here..."> <button onclick="sendMessage()">Send</button> </div> <div id="chatMessages" class="chat-messages"></div> </div> <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> <script> function appendMessage(message) { const chatMessages = document.getElementById('chatMessages'); const messageElement = document.createElement('div'); messageElement.innerText = message; chatMessages.appendChild(messageElement); hatMessages.scrollTop = chatMessages.scrollHeight; } function sendMessage() { const message = document.getElementById('chatInput').value; if (message) { document.getElementById('chatInput').value = ''; socket.emit('chat', message); } } async function initializeSocket() { const negotiateResponse = await fetch(`/api/negotiate`); if (!negotiateResponse.ok) { console.log("Failed to negotiate, status code =", negotiateResponse.status); return; } const negotiateJson = await negotiateResponse.json(); socket = io(negotiateJson.endpoint, { path: negotiateJson.path, query: { access_token: negotiateJson.token } }); socket.on('new message', (socketId, message) => { appendMessage(`${socketId.substring(0,5)}: ${message}`); }) } initializeSocket(); </script> </body> </html>
To return the HTML page, create a function and copy codes:
func new --template "Http Trigger" --name index
Open the file
src/functions/index.js
and replace with the following code:const { app } = require('@azure/functions'); const fs = require('fs').promises; const path = require('path') async function index(request, context) { try { context.log(`HTTP function processed request for url "${request.url}"`); const filePath = path.join(__dirname,'../public/index.html'); const html = await fs.readFile(filePath); return { body: html, headers: { 'Content-Type': 'text/html' } }; } catch (error) { context.log(error); return { status: 500, jsonBody: error } } }; app.http('index', { methods: ['GET', 'POST'], authLevel: 'anonymous', handler: index });
How to run the App locally
After code is prepared, following the instructions to run the sample.
Set up Azure Storage for Azure Function
Azure Functions requires a storage account to work even running in local. Choose either of the two following options:
- Run the free Azurite emulator.
- Use the Azure Storage service. This may incur costs if you continue to use it.
- Install the Azurite
npm install -g azurite
- Start the Azurite storage emulator:
azurite -l azurite -d azurite\debug.log
- Make sure the
AzureWebJobsStorage
in local.settings.json set toUseDevelopmentStorage=true
.
Set up configuration of Web PubSub for Socket.IO
- Add connection string to the Function APP:
func settings add WebPubSubForSocketIOConnectionString "<connection string>"
- Add hub settings to the Web PubSub for Socket.IO
az webpubsub hub create -n <resource name> -g <resource group> --hub-name hub --event-handler url-template="tunnel:///runtime/webhooks/socketio" user-event-pattern="*"
The connection string can be obtained by the Azure CLI command
az webpubsub key show -g <resource group> -n <resource name>
The output contains primaryConnectionString
and secondaryConnectionString
, and either is available.
Set up tunnel
In serverless mode, the service uses webhooks to trigger the function. When you try to run the app locally, a crucial problem is let the service be able to access your local function endpoint.
An easiest way to achieve it's to use Tunnel Tool
Install Tunnel Tool:
npm install -g @azure/web-pubsub-tunnel-tool
Run the tunnel
awps-tunnel run --hub hub --connection "<connection string>" --upstream http://127.0.0.1:7071
The
--upstream
is the url that local Azure Function exposes. The port may be different and you can check the output when starting the function in the next step.
Run Sample App
After tunnel tool is running, you can run the Function App locally:
func start
And visit the webpage at http://localhost:7071/api/index
.
Next steps
Next, you can try to use Bicep to deploy the app online with identity-based authentication: