Compartir a través de


Creación de una aplicación de streaming de código en tiempo real mediante Socket.IO y hospedaje en Azure

Crear una experiencia en tiempo real, como la característica de creación conjunta en Microsoft Word puede ser difícil.

Por medio de sus API fáciles de usar, Socket.IO se ha consolidado como una biblioteca para la comunicación en tiempo real entre clientes y un servidor. Pero los usuarios de Socket.IO suelen tener dificultades relacionadas con el escalado de las conexiones de Socket.IO. Con Web PubSub para Socket.IO, los desarrolladores ya no necesitan preocuparse por administrar conexiones persistentes.

Importante

Las cadenas de conexión sin procesar solo aparecen en este artículo con fines de demostración.

Una cadena de conexión incluye la información de autorización necesaria para que la aplicación acceda al servicio Azure Web PubSub. La clave de acceso dentro de la cadena de conexión es similar a una contraseña raíz para el servicio. En entornos de producción, proteja siempre las claves de acceso. Use Azure Key Vault para administrar y rotar las claves de forma segura y proteja la conexión con WebPubSubServiceClient.

Evite distribuirlas a otros usuarios, codificarlas de forma rígida o guardarlas en un archivo de texto sin formato al que puedan acceder otros usuarios. Rote sus claves si cree que se han puesto en peligro.

Información general

En este artículo se muestra cómo crear una aplicación que permita a un codificador transmitir actividades de codificación a un público. Compilará esta aplicación mediante lo siguiente:

  • Monaco Editor, el editor de código que impulsa Visual Studio Code.
  • Express, un marco web de Node.js.
  • API que proporciona la biblioteca de Socket.IO para la comunicación en tiempo real.
  • Hospede conexiones Socket.IO que usan Web PubSub para Socket.IO.

La aplicación finalizada

La aplicación finalizada permite al usuario de un editor de código compartir un vínculo web con el que los usuarios pueden ver la escritura.

Recorte de pantalla de la aplicación de streaming de código finalizada.

Para mantener los procedimientos centrados y comprensibles en unos 15 minutos, en este artículo se definen dos roles de usuario y lo que pueden hacer en el editor:

  • Un escritor, que puede escribir en el editor en línea y el contenido se transmite
  • Visores, que reciben el contenido en tiempo real escrito por el escritor y no pueden editarlo

Arquitectura

Elemento Objetivo Ventajas
Biblioteca Socket.IO Proporciona un mecanismo de intercambio de datos bidireccional y de baja latencia entre la aplicación de back-end y los clientes API fáciles de usar que cubren la mayoría de los escenarios de comunicación en tiempo real
Web PubSub para Socket.IO Hospeda conexiones persistentes basadas en sondeos o WebSocket con clientes de Socket.IO Compatibilidad con 100 000 conexiones simultáneas; arquitectura de aplicación simplificada

Diagrama en el que se muestra cómo el servicio Web PubSub para Socket.IO conecta los clientes con un servidor.

Requisitos previos

Para seguir todos los pasos de este artículo, necesita lo siguiente:

Creación de un recurso de Web PubSub para Socket.IO

Use la CLI de Azure para crear el recurso:

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

Conseguir una cadena de conexión

Una cadena de conexión permite conectarse con Web PubSub para Socket.IO.

Ejecute los comandos siguientes: Mantenga la cadena de conexión devuelta en algún lugar, ya que la necesitará cuando ejecute la aplicación más adelante en este artículo.

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

Escritura del código del lado servidor de la aplicación

Empiece a escribir el código de la aplicación en el lado servidor.

Creación de un servidor HTTP

  1. Cree un proyecto de Node.js:

    mkdir codestream
    cd codestream
    npm init
    
  2. Instale el SDK del servidor y Express:

    npm install @azure/web-pubsub-socket.io
    npm install express
    
  3. Importe los paquetes necesarios y cree un servidor HTTP para proporcionar archivos estáticos:

    /*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. Defina un punto de conexión denominado /negotiate. Un cliente de escritor llega primero a este punto de conexión. Este punto de conexión devuelve una respuesta HTTP. La respuesta contiene un punto de conexión que el cliente debe usar para establecer una conexión persistente. También devuelve un valor room que se asigna al cliente.

    /*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);
    });
    

Creación del servidor de Web PubSub para Socket.IO

  1. Importe el SDK de Web PubSub para Socket.IO y defina las opciones:

    Las cadenas de conexión sin procesar solo aparecen en este artículo con fines de demostración. En entornos de producción, proteja siempre las claves de acceso. Use Azure Key Vault para administrar y rotar las claves de forma segura y proteja la conexión con WebPubSubServiceClient.

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

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

Los dos pasos son ligeramente diferentes de cómo crearía normalmente un servidor de Socket.IO, como se describe en esta documentación de Socket.IO. Con estos dos pasos, el código del lado servidor puede descargar la administración de conexiones persistentes a un servicio de Azure. Con la ayuda de un servicio de Azure, el servidor de aplicaciones actúa solo como un servidor HTTP ligero.

Implementar la lógica de negocios

Ahora que ha creado un servidor Socket.IO hospedado por Web PubSub, puede definir cómo se comunican los clientes y el servidor mediante las API de Socket.IO. Este proceso se denomina implementación de la lógica de negocios.

  1. Una vez que un cliente se conecta, el servidor de aplicaciones indica al cliente que ha iniciado sesión enviando un evento personalizado denominado login.

    /*server.js*/
    io.on('connection', socket => {
        socket.emit("login");
    });
    
  2. Cada cliente emite dos eventos a los que el servidor puede responder: joinRoom y sendToRoom. Una vez que el servidor obtiene el valor room_id que un cliente quiere unir, se usa socket.join de la API de Socket.IO para unir el cliente de destino a la sala especificada.

    /*server.js*/
    socket.on('joinRoom', async (message) => {
        const room_id = message["room_id"];
        await socket.join(room_id);
    });
    
  3. Una vez que se une un cliente, el servidor informa al cliente del resultado correcto mediante el envío de un evento message. Cuando el cliente recibe un evento message con un tipo de ackJoinRoom, el cliente puede pedir al servidor que envíe el estado del editor más reciente.

    /*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. Cuando un cliente envía un evento sendToRoom al servidor, el servidor difunde loscambios del estado del editor de código a la sala especificada. Todos los clientes de la sala ahora pueden recibir la actualización más reciente.

    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
        });
    });
    

Escritura del código del lado cliente de la aplicación

Ahora que han finalizados los procedimientos del lado servidor, puede trabajar en el lado cliente.

Configuración inicial

Debe crear un cliente de Socket.IO para comunicarse con el servidor. La pregunta es con qué servidor debe establecer el cliente una conexión persistente. Como usa Web PubSub para Socket.IO, el servidor es un servicio de Azure. Recuerde que ha definido una ruta /negotiate para proporcionar a los clientes un punto de conexión a Web PubSub para 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 función initialize(url) organiza algunas operaciones de configuración:

  • Captura el punto de conexión a un servicio de Azure desde el servidor HTTP
  • Crea una instancia del Editor Mónaco
  • Establece una conexión persistente con Web PubSub para Socket.IO

Cliente de escritor

Como se ha mencionado antes, tiene dos roles de usuario en el lado cliente: escritor y visor. Cualquier cosa que escriba el escritor se transmite a la pantalla del visor.

  1. Obtenga el punto de conexión a Web PubSub para Socket.IO y el valor room_id:

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Cuando el cliente escritor está conectado con el servidor, el servidor envía un evento login al escritor. El escritor puede responder pidiendo al servidor que se una a una sala especificada. Cada 200 milisegundos, el cliente del escritor envía el estado del editor más reciente a la sala. Una función denominada flush organiza la lógica de envío.

    /*client.js*/
    
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
        setInterval(() => flush(), 200);
        // Update editor content
        // ...
    });
    
  3. Si un escritor no realiza ninguna edición, flush() no hace nada y simplemente devuelve. De lo contrario, los cambios de estado del editor se envían a la 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. Cuando se conecta un nuevo cliente de visor, el visor debe obtener el estado completo más reciente del editor. Para lograrlo, se envía un mensaje que contiene datos sync al cliente escritor. El mensaje pide al cliente escritor que envíe el estado completo del 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,
            });
        }
    });
    

Cliente de visor

  1. Al igual que el cliente de escritor, el cliente del visor crea su cliente de Socket.IO mediante initialize(). Cuando el cliente visor está conectado y recibe un evento login del servidor, pide al servidor que se una a sí mismo a la sala especificada. La consulta room_id especifica la sala.

    /*client.js*/
    
    let [socket, editor] = await initialize(`/register?room_id=${room_id}`)
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
    });
    
  2. Cuando un cliente de visor recibe un evento message del servidor y el tipo de datos es ackJoinRoom, el cliente de visor pide al cliente de escritor en la sala que envíe el estado completo del 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. Si el tipo de datos es editorMessage, el cliente de visor actualiza el editor según su contenido real.

    /*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. Implemente joinRoom() y sendToRoom() mediante las API de 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
        });
    }
    

Ejecución de la aplicación

Búsqueda del repositorio

En las secciones anteriores se ha descrito la lógica principal relacionada con la sincronización del estado del editor entre los visores y el escritor. Puede encontrar el código completo en el repositorio de ejemplos.

Clonación del repositorio

Puede clonar el repositorio y ejecutar npm install para instalar dependencias del proyecto.

Inicio del servidor

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

Esta es la cadena de conexión que ha recibido en un paso anterior.

Experimentación con el editor de código en tiempo real

Abra http://localhost:3000 en una pestaña del explorador. Abra otra pestaña con la dirección URL que se muestra en la primera página web.

Si escribe código en la primera pestaña, debería ver que la escritura se refleja en tiempo real en la otra pestaña. Web PubSub para Socket.IO facilita la transmisión de mensajes en la nube. El servidor express solo sirve el archivo index.html estático y el punto de conexión /negotiate.