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 co-criação no Microsoft Word pode ser um desafio.

Por meio de suas APIs fáceis de usar, o 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 frequentemente relatam dificuldade em escalar as conexões do Socket.IO. Com o Web PubSub para Socket.IO, os desenvolvedores não precisam mais se preocupar com o gerenciamento de conexões persistentes.

Importante

As cadeias de conexão brutas aparecem neste artigo somente para fins de demonstração.

Uma cadeia de conexão inclui as informações de autorização necessárias para que o seu aplicativo acesse o serviço Azure Web PubSub. A chave de acesso dentro da cadeia de conexão é semelhante a uma senha raiz para o serviço. Em ambientes de produção, sempre proteja suas chaves de acesso. Use o Azure Key Vault para gerenciar e girar suas chaves com segurança e proteger sua conexão com WebPubSubServiceClient.

Evite distribuir chaves de acesso para outros usuários, fazer hard-coding com elas ou salvá-las em qualquer lugar em texto sem formatação que seja acessível a outras pessoas. Gire suas chaves se você acredita que elas podem ter sido comprometidas.

Visão geral

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

  • Monaco Editor, o editor de código que alimenta o Visual Studio Code.
  • Expresso, uma estrutura da Web do Node.js.
  • APIs que a biblioteca de Socket.IO fornece para comunicação em tempo real.
  • Hospedar conexões Socket.IO que usam o 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 por meio do qual as pessoas podem assistir à digitação.

Captura de tela do aplicativo de streaming de código concluído.

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 elas podem fazer no editor:

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

Arquitetura

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

Diagrama que mostra como o serviço Web PubSub para Socket.IO conecta clientes com um servidor.

Pré-requisitos

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

Criar um recurso Web PubSub para Socket.IO

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, pois você precisará dela quando executar o aplicativo mais adiante neste artigo.

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

Grave 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. Criar um projeto Node.js:

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

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

Crie o Web PubSub para o servidor Socket.IO

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

    Cadeias de conexão brutas aparecem neste artigo somente para fins de demonstração. Em ambientes de produção, sempre proteja suas chaves de acesso. Use o Azure Key Vault para gerenciar e girar suas chaves com segurança e proteger sua conexão com WebPubSubServiceClient.

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

    /*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 a lógica de negócios

Agora que você criou um servidor Socket.IO hospedado pelo Web PubSub, você 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 room_id que um cliente deseja ingressar, você usa socket.join da API do Socket.IO para ingressar o cliente de destino na 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 é ingressado, o servidor informa ao cliente sobre o resultado bem-sucedido enviando um evento message. Quando o cliente recebe um evento message com um tipo de ackJoinRoom, o cliente pode pedir ao servidor para enviar o estado mais recente do editor.

    /*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 evento sendToRoom para o servidor, o servidor transmite as alterações para o 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
        });
    });
    

Gravar 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 o 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 função initialize(url) organiza algumas operações de instalação em conjunto:

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

Cliente gravador

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

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

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Quando o cliente gravador está conectado ao servidor, o servidor envia um evento login para o gravador. O gravador pode responder solicitando que o servidor ingresse em uma sala especificada. A cada 200 milissegundos, o cliente gravador 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 gravador não fizer edições, flush() não fará nada e simplesmente retornará. 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 visualizador estiver conectado, o visualizador precisará obter o estado completo mais recente do editor. Para fazer isso, uma mensagem que contém dados sync é enviada ao cliente gravador. A mensagem pede ao cliente gravador para enviar 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 de initialize(). Quando o cliente visualizador está conectado e recebe um evento login do servidor, ele solicita que o servidor ingresse na 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 evento message 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 atualiza 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 visualizadores e o gravador. 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>

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

Reproduzir 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, deverá ver sua digitação refletida em tempo real na outra guia. O Web PubSub para Socket.IO facilita a passagem de mensagens na nuvem. O servidor express serve apenas o arquivo index.html estático e o ponto de extremidade /negotiate.