Share via


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

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

A través de sus API fáciles de usar, Socket.IO se ha demostrado como una biblioteca para la comunicación en tiempo real entre clientes y un servidor. Sin embargo, Socket.IO los usuarios suelen notificar dificultades en torno al escalado de las conexiones de Socket.IO. Con Web PubSub para Socket.IO, los desarrolladores ya no necesitan preocuparse por administrar conexiones persistentes.

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 una audiencia. Compile esta aplicación mediante:

  • Monaco Editor, el editor de código que impulsa Visual Studio Code.
  • Express, un marco web Node.js.
  • API que proporciona la biblioteca de Socket.IO para la comunicación en tiempo real.
  • Hospede Socket.IO conexiones 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 a través del cual los usuarios pueden ver la escritura.

Screenshot of the finished code-streaming app.

Para mantener los procedimientos centrados y digeribles 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 contenido en tiempo real escrito por el escritor y no pueden editar el contenido

Architecture

Elemento Propósito Ventajas
biblioteca de Socket.IO Proporciona un mecanismo de intercambio de datos bidireccional y de baja latencia entre la aplicación 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

Diagram that shows how the Web PubSub for Socket.IO service connects clients with a server.

Requisitos previos

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

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

Un 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

Escribir el código del lado servidor de la aplicación

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

Compilación de un servidor HTTP

  1. Cree un proyecto 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 room valor al que se asigna el 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 de Web PubSub para Socket.IO servidor

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

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

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

Los dos pasos son ligeramente diferentes de cómo crearía normalmente un servidor 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 solo actúa como un servidor HTTP ligero.

Implementación de 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 lógica de negocios.

  1. Una vez conectado un cliente, el servidor de aplicaciones indica al cliente que ha iniciado sesión mediante el envío de 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 room_id valor que un cliente quiere unir, se usa socket.join desde 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 unido un cliente, el servidor informa al cliente del resultado correcto mediante el envío de un message evento. Cuando el cliente recibe un message evento 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 sendToRoom evento al servidor, el servidor transmite los cambios al 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 finalizan los procedimientos del lado servidor, puede trabajar en el lado cliente.

Configuración inicial

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

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

Cliente de escritor

Como se mencionó anteriormente, tiene dos roles de usuario en el lado cliente: escritor y visor. Cualquier cosa que los tipos de escritor se transmitan a la pantalla del visor.

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

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Cuando el cliente de escritura está conectado con el servidor, el servidor envía un login evento 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 en el 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 sync datos al cliente de escritor. El mensaje pide al cliente de 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 escritura, el cliente del visor crea su Socket.IO cliente a través de initialize(). Cuando el cliente del visor está conectado y recibe un login evento del servidor, pide al servidor que se una 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 del visor recibe un message evento del servidor y el tipo de datos es ackJoinRoom, el cliente del 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 del 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() use 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

Buscar el repositorio

En las secciones anteriores se trata 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>

Este es el cadena de conexión que recibió en un paso anterior.

Jugar 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 mostrada en la primera página web.

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