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

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.

  1. Follow to step to install the latest Azure Function core tool

  2. 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.

  3. Currently, the Function Bundle doesn't include Socket.IO Function Binding, so you need to manually add the package.

    1. 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)"
      }
      
    2. Run the command:

      func extensions install -p Microsoft.Azure.WebJobs.Extensions.WebPubSubForSocketIO -v 1.0.0-beta.4
      
  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 and SocketIONegotiation output binding, which means you can use an HTTP call to trigger the function and return a negotiation result that generated by SocketIONegotiation binding.

  5. 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 use SocketIO binding to broadcast messages to namespace.

  6. Create a function to return an index html for visiting.

    1. Create a folder public under src/.

    2. 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>
      
    3. To return the HTML page, create a function and copy codes:

      func new --template "Http Trigger" --name index
      
    4. 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.
  1. Install the Azurite
npm install -g azurite
  1. Start the Azurite storage emulator:
azurite -l azurite -d azurite\debug.log
  1. Make sure the AzureWebJobsStorage in local.settings.json set to UseDevelopmentStorage=true.

Set up configuration of Web PubSub for Socket.IO

  1. Add connection string to the Function APP:
func settings add WebPubSubForSocketIOConnectionString "<connection string>"
  1. 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

  1. Install Tunnel Tool:

    npm install -g @azure/web-pubsub-tunnel-tool
    
  2. 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.

Screenshot of the serverless chat app.

Next steps

Next, you can try to use Bicep to deploy the app online with identity-based authentication: