Share via


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, Socket.IO gli utenti spesso segnalano difficoltà nel ridimensionare le connessioni di Socket.IO. Con Web PubSub per Socket.IO, gli sviluppatori non devono più preoccuparsi della gestione delle connessioni persistenti.

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.
  • Ospitare Socket.IO connessioni 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 of the finished code-streaming app.

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
  • Visualizzatori, che ricevono contenuti in tempo reale digitati dal writer e non possono modificare il contenuto

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

Diagram that shows how the Web PubSub for Socket.IO service connects clients with a 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 un sito Web PubSub per Socket.IO risorsa

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 un stringa di connessione

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

Eseguire i comandi seguenti. Mantenere il stringa di connessione restituito da qualche parte, perché sarà necessario quando si esegue 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);
    });
    

Creare web PubSub per Socket.IO server

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

    /*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 ottiene il room_id valore che un client vuole aggiungere, si usa dall'API socket.join di Socket.IO per aggiungere il client di destinazione alla sala 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. Tenere presente che è stata definita una route /negotiate per servire i client a 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 in un servizio di Azure dal server HTTP
  • Crea un'istanza dell'editor monaco
  • Stabilisce una connessione permanente con Web PubSub per Socket.IO

Client writer

Come accennato in precedenza, si hanno due ruoli utente sul lato client: writer e visualizzatore. Tutto ciò che i tipi di writer vengono trasmessi alla schermata del visualizzatore.

  1. Ottenere l'endpoint su Web PubSub per Socket.IO e il room_id valore:

    /*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 writer invia lo stato dell'editor più recente alla sala. 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 stanza.

    /*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 visualizzatore riceve un message evento dal server e il tipo di dati è ackJoinRoom, il client del visualizzatore chiede al client writer nella sala di inviare lo stato dell'editor completo.

    /*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

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

Clonare 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>

Questo è il stringa di connessione ricevuto in un passaggio precedente.

Eseguire la riproduzione con l'editor di codice in tempo reale

Aprire in una scheda del browser. Aprire http://localhost:3000 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 .