Creare un'app di streaming di codice in tempo reale usando Socket.IO e ospitarla in Azure

La creazione di un'esperienza in tempo reale come la funzionalità di creazione condivisa in Microsoft Word può risultare complessa.

Grazie alle API facili da usare, Socket.IO si è dimostrata come una libreria per la comunicazione in tempo reale tra client e server. Tuttavia, gli utenti di Socket.IO segnalano spesso difficoltà nel gestire la scalabilità delle connessioni di Socket.IO. Con Web PubSub per Socket.IO, gli sviluppatori non devono più preoccuparsi della gestione delle connessioni persistenti.

Importante

Le stringa di connessione non elaborate vengono visualizzate in questo articolo solo a scopo dimostrativo.

Una stringa di connessione include le informazioni di autorizzazione di cui la tua applicazione ha bisogno per accedere al servizio Azure Web PubSub. La chiave di accesso nella stringa di connessione agisce come una password root per il tuo servizio. Negli ambienti di produzione proteggere sempre le chiavi di accesso. Usare Azure Key Vault per gestire e ruotare le chiavi in modo sicuro e proteggere la connessione con WebPubSubServiceClient.

Evitare di distribuire le chiavi di accesso ad altri utenti, impostarle come hardcoded o salvarle in un file di testo normale accessibile ad altri. Ruotare le chiavi se si ritiene che siano state compromesse.

Panoramica

Questo articolo illustra come creare un'app che consente a un coder di trasmettere attività di codifica a un gruppo di destinatari. Per compilare questa applicazione, usare:

  • Editor Monaco, l'editor di codice che supporta Visual Studio Code.
  • Express, un framework Web Node.js.
  • API fornite dalla libreria di Socket.IO per la comunicazione in tempo reale.
  • Ospita connessioni Socket.IO che usano Web PubSub per Socket.IO.

L'app completata

L'app completata consente all'utente di un editor di codice di condividere un collegamento Web tramite il quale gli utenti possono osservare la digitazione.

Screenshot dell'app di streaming di codice completata.

Per mantenere incentrate e digeribili le procedure in circa 15 minuti, questo articolo definisce due ruoli utente e le operazioni che possono eseguire nell'editor:

  • Writer, che può digitare nell'editor online e il contenuto viene trasmesso in streaming
  • Spettatori, che ricevono in tempo reale i contenuti digitati dal redattore e non possono modificarli

Architettura

Articolo Scopo Vantaggi
libreria Socket.IO Fornisce un meccanismo di scambio di dati bidirezionali a bassa latenza tra l'applicazione back-end e i client API facili da usare che coprono la maggior parte degli scenari di comunicazione in tempo reale
Web PubSub per Socket.IO Ospita connessioni persistenti basate su WebSocket o su polling con client Socket.IO Supporto per 100.000 connessioni simultanee; architettura dell'applicazione semplificata

Diagramma che mostra come Web PubSub per Socket.IO servizio connette i client a un server.

Prerequisiti

Per seguire tutti i passaggi descritti in questo articolo, è necessario:

  • Un account Azure. Se non si ha una sottoscrizione di Azure, creare un account Azure gratuito prima di iniziare.
  • Interfaccia della riga di comando di Azure (versione 2.29.0 o successiva) o Azure Cloud Shell per gestire le risorse di Azure.
  • Conoscenza di base delle API di Socket.IO.

Creare una risorsa Web PubSub per Socket.IO

Usare l'interfaccia della riga di comando di Azure per creare la risorsa:

az webpubsub create -n <resource-name> \
                    -l <resource-location> \
                    -g <resource-group> \
                    --kind SocketIO \
                    --sku Free_F1

Ottenere una stringa di connessione

Un stringa di connessione consente di connettersi con Web PubSub per Socket.IO.

Eseguire i comandi seguenti. Conserva da qualche parte la stringa di connessione restituita, perché ti servirà quando eseguirai l'applicazione più avanti in questo articolo.

az webpubsub key show -n <resource-name> \ 
                      -g <resource-group> \ 
                      --query primaryKey \
                      -o tsv

Scrivere il codice lato server dell'applicazione

Iniziare a scrivere il codice dell'applicazione lavorando sul lato server.

Creare un server HTTP

  1. Creare un progetto Node.js:

    mkdir codestream
    cd codestream
    npm init
    
  2. Installare l'SDK del server ed Express:

    npm install @azure/web-pubsub-socket.io
    npm install express
    
  3. Importare i pacchetti necessari e creare un server HTTP per gestire i file statici:

    /*server.js*/
    
    // Import required packages
    const express = require('express');
    const path = require('path');
    
    // Create an HTTP server based on Express
    const app = express();
    const server = require('http').createServer(app);
    
    app.use(express.static(path.join(__dirname, 'public')));
    
  4. Definire un endpoint denominato /negotiate. Un client writer raggiunge prima questo endpoint. Questo endpoint restituisce una risposta HTTP. La risposta contiene un endpoint che il client deve usare per stabilire una connessione permanente. Restituisce anche un room valore a cui è assegnato il client.

    /*server.js*/
    app.get('/negotiate', async (req, res) => {
        res.json({
            url: endpoint
            room_id: Math.random().toString(36).slice(2, 7),
        });
    });
    
    // Make the Socket.IO server listen on port 3000
    io.httpServer.listen(3000, () => {
        console.log('Visit http://localhost:%d', 3000);
    });
    

Crea il server Web PubSub for Socket.IO

  1. Importare Web PubSub per Socket.IO SDK e definire le opzioni:

    Le stringa di connessione non elaborate vengono visualizzate in questo articolo solo a scopo dimostrativo. Negli ambienti di produzione proteggere sempre le chiavi di accesso. Usare Azure Key Vault per gestire e ruotare le chiavi in modo sicuro e proteggere la connessione con WebPubSubServiceClient.

    /*server.js*/
    const { useAzureSocketIO } = require("@azure/web-pubsub-socket.io");
    
    const wpsOptions = {
        hub: "codestream",
        connectionString: process.argv[2]
    }
    
  2. Creare un Web PubSub per Socket.IO server:

    /*server.js*/
    
    const io = require("socket.io")();
    useAzureSocketIO(io, wpsOptions);
    

I due passaggi sono leggermente diversi da come si crea normalmente un server Socket.IO, come descritto in questa documentazione Socket.IO. Con questi due passaggi, il codice lato server può eseguire l'offload della gestione delle connessioni permanenti a un servizio di Azure. Con l'aiuto di un servizio di Azure, il server applicazioni funge solo da server HTTP leggero.

Implementare la logica di business

Ora che è stato creato un server Socket.IO ospitato da Web PubSub, è possibile definire il modo in cui i client e il server comunicano usando le API di Socket.IO. Questo processo viene chiamato implementazione della logica di business.

  1. Dopo la connessione di un client, il server applicazioni indica al client che è connesso inviando un evento personalizzato denominato login.

    /*server.js*/
    io.on('connection', socket => {
        socket.emit("login");
    });
    
  2. Ogni client genera due eventi a cui il server può rispondere: joinRoom e sendToRoom. Dopo che il server riceve il valore room_id della stanza a cui un client vuole unirsi, si usa socket.join dell'API di Socket.IO per far entrare il client di destinazione nella stanza specificata.

    /*server.js*/
    socket.on('joinRoom', async (message) => {
        const room_id = message["room_id"];
        await socket.join(room_id);
    });
    
  3. Dopo che un client è stato aggiunto, il server informa il client del risultato riuscito inviando un message evento. Quando il client riceve un message evento con un tipo di ackJoinRoom, il client può chiedere al server di inviare lo stato dell'editor più recente.

    /*server.js*/
    socket.on('joinRoom', async (message) => {
        // ...
        socket.emit("message", {
            type: "ackJoinRoom", 
            success: true 
        })
    });
    
    /*client.js*/
    socket.on("message", (message) => {
        let data = message;
        if (data.type === 'ackJoinRoom' && data.success) {
            sendToRoom(socket, `${room_id}-control`, { data: 'sync'});
        }
        // ... 
    });
    
  4. Quando un client invia un sendToRoom evento al server, il server trasmette le modifiche allo stato dell'editor di codice nella sala specificata. Tutti i client nella sala possono ora ricevere l'aggiornamento più recente.

    socket.on('sendToRoom', (message) => {
        const room_id = message["room_id"]
        const data = message["data"]
    
        socket.broadcast.to(room_id).emit("message", {
            type: "editorMessage",
            data: data
        });
    });
    

Scrivere il codice lato client dell'applicazione

Ora che le procedure sul lato server sono state completate, è possibile lavorare sul lato client.

Configurazione iniziale

È necessario creare un client Socket.IO per comunicare con il server. La domanda è il server con cui il client deve stabilire una connessione permanente. Poiché si usa Web PubSub per Socket.IO, il server è un servizio di Azure. Ricorda che hai definito una route /negotiate per fornire ai client un endpoint per Web PubSub per Socket.IO.

/*client.js*/

async function initialize(url) {
    let data = await fetch(url).json()

    updateStreamId(data.room_id);

    let editor = createEditor(...); // Create an editor component

    var socket = io(data.url, {
        path: "/clients/socketio/hubs/codestream",
    });

    return [socket, editor, data.room_id];
}

La initialize(url) funzione organizza alcune operazioni di configurazione insieme:

  • Recupera l'endpoint di un servizio Azure dal tuo server HTTP
  • Crea un'istanza di Monaco Editor
  • Stabilisce una connessione permanente con Web PubSub per Socket.IO

Client di scrittura

Come accennato in precedenza, si hanno due ruoli utente sul lato client: writer e visualizzatore. Tutto ciò che l'utente digita viene trasmesso sullo schermo del visualizzatore.

  1. Recuperare l'endpoint di Web PubSub per Socket.IO e il valore room_id:

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Quando il client writer è connesso al server, il server invia un login evento al writer. Il writer può rispondere chiedendo al server di aggiungersi a una sala specificata. Ogni 200 millisecondi, il client di scrittura invia alla stanza lo stato più recente dell'editor. Una funzione denominata flush organizza la logica di invio.

    /*client.js*/
    
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
        setInterval(() => flush(), 200);
        // Update editor content
        // ...
    });
    
  3. Se un writer non apporta modifiche, flush() non esegue alcuna operazione e restituisce semplicemente. In caso contrario, le modifiche apportate allo stato dell'editor vengono inviate alla sala.

    /*client.js*/
    
    function flush() {
        // No changes from editor need to be flushed
        if (changes.length === 0) return;
    
        // Broadcast the changes made to editor content
        sendToRoom(socket, room_id, {
            type: 'delta',
            changes: changes
            version: version++,
        });
    
        changes = [];
        content = editor.getValue();
    }
    
  4. Quando un nuovo client visualizzatore è connesso, il visualizzatore deve ottenere lo stato completo più recente dell'editor. A tale scopo, viene inviato un messaggio che contiene sync dati al client writer. Il messaggio chiede al client writer di inviare lo stato completo dell'editor.

    /*client.js*/
    
    socket.on("message", (message) => {
        let data = message.data;
        if (data.data === 'sync') {
            // Broadcast the full content of the editor to the room
            sendToRoom(socket, room_id, {
                type: 'full',
                content: content
                version: version,
            });
        }
    });
    

Client visualizzatore

  1. Analogamente al client writer, il client del visualizzatore crea il client Socket.IO tramite initialize(). Quando il client del visualizzatore è connesso e riceve un login evento dal server, chiede al server di aggiungersi alla sala specificata. La query room_id specifica la sala.

    /*client.js*/
    
    let [socket, editor] = await initialize(`/register?room_id=${room_id}`)
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
    });
    
  2. Quando un client di visualizzazione riceve un evento message dal server e il tipo di dati è ackJoinRoom, il client di visualizzazione chiede al client di scrittura nella stanza di inviare lo stato completo dell'editor.

    /*client.js*/
    
    socket.on("message", (message) => {
        let data = message;
        // Ensures the viewer client is connected
        if (data.type === 'ackJoinRoom' && data.success) { 
            sendToRoom(socket, `${id}`, { data: 'sync'});
        } 
        else //...
    });
    
  3. Se il tipo di dati è editorMessage, il client del visualizzatore aggiorna l'editor in base al contenuto effettivo.

    /*client.js*/
    
    socket.on("message", (message) => {
        ...
        else 
            if (data.type === 'editorMessage') {
            switch (data.data.type) {
                case 'delta':
                    // ... Let editor component update its status
                    break;
                case 'full':
                    // ... Let editor component update its status
                    break;
            }
        }
    });
    
  4. Implementare joinRoom() e sendToRoom() usando le API di Socket.IO:

    /*client.js*/
    
    function joinRoom(socket, room_id) {
        socket.emit("joinRoom", {
            room_id: room_id,
        });
    }
    
    function sendToRoom(socket, room_id, data) {
        socket.emit("sendToRoom", {
            room_id: room_id,
            data: data
        });
    }
    

Eseguire l'applicazione

Trova il repository

Le sezioni precedenti hanno illustrato la logica di base correlata alla sincronizzazione dello stato dell'editor tra visualizzatori e writer. È possibile trovare il codice completo nel repository di esempi.

Clona il repository

È possibile clonare il repository ed eseguire npm install per installare le dipendenze del progetto.

Avviare il server

node server.js <web-pubsub-connection-string>

Questa è la stringa di connessione ricevuta in un passaggio precedente.

Prova l'editor di codice in tempo reale

Apri http://localhost:3000 in una scheda del browser. Apri un'altra scheda con l'URL visualizzato nella prima pagina web.

Se si scrive codice nella prima scheda, la digitazione dovrebbe essere riflessa in tempo reale nell'altra scheda. Web PubSub per Socket.IO facilita il passaggio del messaggio nel cloud. Il express server serve solo il file statico index.html e l'endpoint /negotiate .