Compartilhar via


Criar um aplicativo de streaming de código em tempo real utilizando o Socket.IO e hospedando-o no Azure

Criar uma experiência em tempo real, como o recurso de cocriação no Microsoft Word , pode ser um desafio.

Através de suas APIs fáceis de usar, Socket.IO provou ser uma biblioteca para comunicação em tempo real entre clientes e um servidor. No entanto, os usuários do Socket.IO geralmente relatam dificuldade em dimensionar as conexões do Socket.IO. Com o Web PubSub for Socket.IO, os desenvolvedores não precisam mais se preocupar com o gerenciamento de conexões persistentes.

Visão geral

Este artigo mostra como criar um aplicativo que permite que um codificador transmita atividades de codificação para um público. Você cria este aplicativo usando:

  • Monaco Editor, o editor de código que alimenta o Visual Studio Code.
  • Express, um Node.js framework web.
  • APIs que a biblioteca Socket.IO fornece para comunicação em tempo real.
  • Hospede conexões Socket.IO que usam Web PubSub para Socket.IO.

O aplicativo finalizado

O aplicativo concluído permite que o usuário de um editor de código compartilhe um link da Web através do qual as pessoas podem assistir à digitação.

Screenshot of the finished code-streaming app.

Para manter os procedimentos focados e digeríveis em cerca de 15 minutos, este artigo define duas funções de usuário e o que eles podem fazer no editor:

  • Um escritor, que pode digitar no editor on-line e o conteúdo é transmitido
  • Visualizadores, que recebem conteúdo digitado em tempo real pelo gravador e não podem editar o conteúdo

Arquitetura

Item Finalidade Benefícios
Socket.IO biblioteca Fornece um mecanismo de troca de dados bidirecional de baixa latência entre o aplicativo back-end e os clientes APIs fáceis de usar que cobrem a maioria dos cenários de comunicação em tempo real
Web PubSub para Socket.IO Hospeda WebSocket ou conexões persistentes baseadas em sondagem com clientes Socket.IO Suporte para 100.000 conexões simultâneas; arquitetura de aplicativos simplificada

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

Pré-requisitos

Para seguir todas as etapas deste artigo, você precisa:

Criar um Web PubSub para Socket.IO recurso

Use a CLI do Azure para criar o recurso:

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

Obter uma cadeia de conexão

Uma cadeia de conexão permite que você se conecte ao Web PubSub para Socket.IO.

Execute os seguintes comandos. Mantenha a cadeia de conexão retornada em algum lugar, porque você precisará dela quando executar o aplicativo posteriormente neste artigo.

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

Escrever o código do lado do servidor do aplicativo

Comece a escrever o código do aplicativo trabalhando no lado do servidor.

Criar um servidor HTTP

  1. Crie um projeto Node.js:

    mkdir codestream
    cd codestream
    npm init
    
  2. Instale o SDK do servidor e o Express:

    npm install @azure/web-pubsub-socket.io
    npm install express
    
  3. Importe os pacotes necessários e crie um servidor HTTP para servir arquivos 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 um ponto de extremidade chamado /negotiate. Um cliente de gravador atinge esse ponto de extremidade primeiro. Esse ponto de extremidade retorna uma resposta HTTP. A resposta contém um ponto de extremidade que o cliente deve usar para estabelecer uma conexão persistente. Ele também retorna um room valor ao qual o cliente está atribuído.

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

Criar o Web PubSub para Socket.IO servidor

  1. Importe o Web PubSub para Socket.IO SDK e defina opções:

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

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

As duas etapas são ligeiramente diferentes de como você normalmente criaria um servidor Socket.IO, conforme descrito nesta documentação Socket.IO. Com essas duas etapas, seu código do lado do servidor pode descarregar o gerenciamento de conexões persistentes para um serviço do Azure. Com a ajuda de um serviço do Azure, seu servidor de aplicativos atua apenas como um servidor HTTP leve.

Implementar lógica de negócios

Agora que você criou um servidor Socket.IO hospedado pelo Web PubSub, pode definir como os clientes e o servidor se comunicam usando as APIs do Socket.IO. Esse processo é chamado de implementação da lógica de negócios.

  1. Depois que um cliente é conectado, o servidor de aplicativos informa ao cliente que ele está conectado enviando um evento personalizado chamado login.

    /*server.js*/
    io.on('connection', socket => {
        socket.emit("login");
    });
    
  2. Cada cliente emite dois eventos aos quais o servidor pode responder: joinRoom e sendToRoom. Depois que o servidor obtém o valor que um cliente deseja ingressar, você usa socket.join da API do Socket.IO para associar o room_id cliente de destino à sala especificada.

    /*server.js*/
    socket.on('joinRoom', async (message) => {
        const room_id = message["room_id"];
        await socket.join(room_id);
    });
    
  3. Depois que um cliente ingressa, o servidor informa o cliente do resultado bem-sucedido enviando um message evento. Quando o cliente recebe um evento com um message tipo de , o cliente pode pedir ao servidor para enviar o estado mais recente do ackJoinRoomeditor.

    /*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 um cliente envia um sendToRoom evento para o servidor, o servidor transmite as alterações no estado do editor de código para a sala especificada. Todos os clientes na sala agora podem receber a atualização mais 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
        });
    });
    

Escrever o código do lado do cliente do aplicativo

Agora que os procedimentos do lado do servidor foram concluídos, você pode trabalhar no lado do cliente.

Instalação inicial

Você precisa criar um cliente Socket.IO para se comunicar com o servidor. A questão é com qual servidor o cliente deve estabelecer uma conexão persistente. Como você está usando o Web PubSub para Socket.IO, o servidor é um serviço do Azure. Lembre-se de que você definiu uma rota /negotiate para servir aos clientes um ponto de extremidade para o 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];
}

A initialize(url) função organiza algumas operações de configuração em conjunto:

  • Busca o ponto de extremidade para um serviço do Azure do seu servidor HTTP
  • Cria uma instância do Monaco Editor
  • Estabelece uma conexão persistente com o Web PubSub para Socket.IO

Cliente do Writer

Como mencionado anteriormente, você tem duas funções de usuário no lado do cliente: escritor e visualizador. Tudo o que o escritor digita é transmitido para a tela do espectador.

  1. Obtenha o ponto de extremidade para Web PubSub para Socket.IO e o room_id valor:

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Quando o cliente gravador está conectado ao servidor, o servidor envia um login evento para o gravador. O gravador pode responder solicitando que o servidor se junte a uma sala especificada. A cada 200 milissegundos, o cliente do escritor envia o estado mais recente do editor para a sala. Uma função chamada flush organiza a lógica de envio.

    /*client.js*/
    
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
        setInterval(() => flush(), 200);
        // Update editor content
        // ...
    });
    
  3. Se um escritor não faz nenhuma edição, flush() não faz nada e simplesmente retorna. Caso contrário, as alterações no estado do editor serão enviadas para a 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 um novo cliente de visualizador é conectado, o visualizador precisa obter o estado completo mais recente do editor. Para conseguir isso, uma mensagem que contém sync dados é enviada para o cliente de gravador. A mensagem pede que o cliente do gravador envie o estado completo do 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 visualizador

  1. Assim como o cliente gravador, o cliente visualizador cria seu cliente Socket.IO por meio do initialize(). Quando o cliente visualizador está conectado e recebe um login evento do servidor, ele solicita que o servidor se associe à sala especificada. A consulta room_id especifica a sala.

    /*client.js*/
    
    let [socket, editor] = await initialize(`/register?room_id=${room_id}`)
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
    });
    
  2. Quando um cliente visualizador recebe um message evento do servidor e o tipo de dados é ackJoinRoom, o cliente visualizador pede ao cliente gravador na sala para enviar o estado completo do 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 o tipo de dados for editorMessage, o cliente visualizador atualizará o editor de acordo com seu conteúdo 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() e sendToRoom() usando as APIs do 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
        });
    }
    

Executar o aplicativo

Localizar o repositório

As seções anteriores abordaram a lógica central relacionada à sincronização do estado do editor entre os espectadores e o escritor. Você pode encontrar o código completo no repositório de exemplos.

Clonar o repositório

Você pode clonar o repositório e executar npm install para instalar dependências do projeto.

Iniciar o servidor

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

Esta é a cadeia de conexão que você recebeu em uma etapa anterior.

Jogue com o editor de código em tempo real

Abra http://localhost:3000 em uma guia do navegador. Abra outra guia com a URL exibida na primeira página da Web.

Se você escrever código na primeira guia, você deve ver sua digitação refletida em tempo real na outra guia Socket.IO. Seu express servidor serve apenas o arquivo estático index.html e o /negotiate ponto de extremidade.