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.
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 |
Requisitos previos
Para seguir todos los pasos de este artículo, necesita lo siguiente:
- Tiene una cuenta de Azure. Si no tiene una suscripción a Azure, cree una cuenta gratuita de Azure antes de empezar.
- La CLI de Azure (versión 2.29.0 o posterior) o Azure Cloud Shell para administrar recursos de Azure.
- Conocimientos básicos de las API de Socket.IO.
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
Cree un proyecto de Node.js:
mkdir codestream cd codestream npm init
Instale el SDK del servidor y Express:
npm install @azure/web-pubsub-socket.io npm install express
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')));
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 valorroom
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
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] }
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.
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"); });
Cada cliente emite dos eventos a los que el servidor puede responder:
joinRoom
ysendToRoom
. Una vez que el servidor obtiene el valorroom_id
que un cliente quiere unir, se usasocket.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); });
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 eventomessage
con un tipo deackJoinRoom
, 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'}); } // ... });
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.
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');
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 denominadaflush
organiza la lógica de envío./*client.js*/ socket.on("login", () => { updateStatus('Connected'); joinRoom(socket, `${room_id}`); setInterval(() => flush(), 200); // Update editor content // ... });
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(); }
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
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 eventologin
del servidor, pide al servidor que se una a sí mismo a la sala especificada. La consultaroom_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}`); });
Cuando un cliente de visor recibe un evento
message
del servidor y el tipo de datos esackJoinRoom
, 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 //... });
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; } } });
Implemente
joinRoom()
ysendToRoom()
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
.