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.
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 |
Pré-requisitos
Para seguir todas as etapas deste artigo, você precisa:
- Uma conta do Azure. Caso você não tenha uma assinatura do Azure, crie uma conta gratuita do Azure antes de começar.
- A CLI do Azure (versão 2.29.0 ou posterior) ou o Azure Cloud Shell para gerenciar recursos do Azure.
- Familiaridade básica com as APIs do Socket.IO.
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
Crie um projeto Node.js:
mkdir codestream cd codestream npm init
Instale o SDK do servidor e o Express:
npm install @azure/web-pubsub-socket.io npm install express
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')));
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 umroom
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
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] }
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.
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"); });
Cada cliente emite dois eventos aos quais o servidor pode responder:
joinRoom
esendToRoom
. Depois que o servidor obtém o valor que um cliente deseja ingressar, você usasocket.join
da API do Socket.IO para associar oroom_id
cliente de destino à sala especificada./*server.js*/ socket.on('joinRoom', async (message) => { const room_id = message["room_id"]; await socket.join(room_id); });
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 ummessage
tipo de , o cliente pode pedir ao servidor para enviar o estado mais recente doackJoinRoom
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'}); } // ... });
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.
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');
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 chamadaflush
organiza a lógica de envio./*client.js*/ socket.on("login", () => { updateStatus('Connected'); joinRoom(socket, `${room_id}`); setInterval(() => flush(), 200); // Update editor content // ... });
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(); }
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
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 umlogin
evento do servidor, ele solicita que o servidor se associe à sala especificada. A consultaroom_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}`); });
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 //... });
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; } } });
Implemente
joinRoom()
esendToRoom()
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.