Tutorial: Publicar e assinar mensagens entre clientes WebSocket usando subprotocolo
Artigo
No tutorial Criar um aplicativo de chat, você aprendeu como usar APIs WebSocket para enviar e receber dados com o Azure Web PubSub. Você pode ver que não há nenhum protocolo necessário quando o cliente está se comunicando com o serviço. Por exemplo, você pode enviar qualquer tipo de dados usando WebSocket.send()o , e o servidor os recebe exatamente como estão. O processo de APIs WebSocket é fácil de usar, mas a funcionalidade é limitada. Por exemplo, não é possível especificar o nome do evento ao enviar o evento para o servidor ou publicar uma mensagem para outros clientes em vez de enviá-la para o servidor. Neste tutorial, você aprenderá a usar o subprotocolo para estender a funcionalidade do cliente.
Neste tutorial, irá aprender a:
Criar uma instância de serviço Web PubSub
Gere a URL completa para estabelecer a conexão WebSocket
Publicar mensagens entre clientes WebSocket usando subprotocolo
Se preferir executar comandos de referência da CLI localmente, instale a CLI do Azure. Se estiver a utilizar o Windows ou macOS, considere executar a CLI do Azure num contentor Docker. Para obter mais informações, consulte Como executar a CLI do Azure em um contêiner do Docker.
Se estiver a utilizar uma instalação local, inicie sessão no CLI do Azure ao utilizar o comando az login. Para concluir o processo de autenticação, siga os passos apresentados no seu terminal. Para outras opções de entrada, consulte Entrar com a CLI do Azure.
Quando solicitado, instale a extensão da CLI do Azure na primeira utilização. Para obter mais informações sobre as extensões, veja Utilizar extensões com o CLI do Azure.
Execute o comando az version para localizar a versão e as bibliotecas dependentes instaladas. Para atualizar para a versão mais recente, execute o comando az upgrade.
Esta configuração requer a versão 2.22.0 ou superior da CLI do Azure. Se estiver usando o Azure Cloud Shell, a versão mais recente já está instalada.
Criar uma instância do Azure Web PubSub
Criar um grupo de recursos
Um grupo de recursos é um contentor lógico no qual os recursos do Azure são implementados e geridos. Use o comando az group create para criar um grupo de recursos nomeado myResourceGroup no eastus local.
az group create --name myResourceGroup --location EastUS
Criar uma instância do Web PubSub
Execute az extension add para instalar ou atualizar a extensão webpubsub para a versão atual.
az extension add --upgrade --name webpubsub
Use o comando Azure CLI az webpubsub create para criar um Web PubSub no grupo de recursos que você criou. O comando a seguir cria um recurso Free Web PubSub no grupo de recursos myResourceGroup em EastUS:
Importante
Cada recurso Web PubSub deve ter um nome exclusivo. Substitua <your-unique-resource-name> pelo nome do seu Web PubSub nos exemplos a seguir.
A saída deste comando mostra as propriedades do recurso recém-criado. Tome nota das duas propriedades listadas abaixo:
Nome do recurso: O nome que você forneceu para o --name parâmetro acima.
hostName: No exemplo, o nome do host é <your-unique-resource-name>.webpubsub.azure.com/.
Neste ponto, sua conta do Azure é a única autorizada a executar quaisquer operações neste novo recurso.
Obtenha o ConnectionString para uso futuro
Importante
Uma cadeia de conexão inclui as informações de autorização necessárias para seu aplicativo acessar o serviço Azure Web PubSub. A chave de acesso dentro da cadeia de conexão é semelhante a uma senha de root para o seu serviço. Em ambientes de produção, tenha sempre o cuidado de proteger as suas chaves de acesso. Use o Azure Key Vault para gerenciar e girar suas chaves com segurança. Evite distribuir chaves de acesso para outros usuários, codificá-las ou salvá-las em qualquer lugar em texto simples acessível a outras pessoas. Rode as chaves se acreditar que podem ter sido comprometidas.
Use o comando Azure CLI az webpubsub key para obter o ConnectionString do serviço. Substitua o espaço reservado <your-unique-resource-name> pelo nome da sua instância do Azure Web PubSub.
az webpubsub key show --resource-group myResourceGroup --name <your-unique-resource-name> --query primaryConnectionString --output tsv
Copie a cadeia de conexão para usar mais tarde.
Copie o ConnectionString buscado e use posteriormente neste tutorial como o valor de <connection_string>.
Se você usar outros nomes de protocolo, eles serão ignorados pelo serviço e passarão para o servidor no manipulador de eventos connect, para que você possa criar seus próprios protocolos.
Agora vamos criar um aplicativo Web usando o json.webpubsub.azure.v1 subprotocolo.
Crie um server.py para hospedar a API e a /negotiate página da Web.
import json
import sys
from http.server import HTTPServer, SimpleHTTPRequestHandler
from azure.messaging.webpubsubservice import WebPubSubServiceClient
service = WebPubSubServiceClient.from_connection_string(sys.argv[1], hub='stream')
class Resquest(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.path = 'public/index.html'
return SimpleHTTPRequestHandler.do_GET(self)
elif self.path == '/negotiate':
roles = ['webpubsub.sendToGroup.stream',
'webpubsub.joinLeaveGroup.stream']
token = service.get_client_access_token(roles=roles)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({
'url': token['url']
}).encode())
if __name__ == '__main__':
if len(sys.argv) != 2:
print('Usage: python server.py <connection-string>')
exit(1)
server = HTTPServer(('localhost', 8080), Resquest)
print('server started')
server.serve_forever()
Vamos navegar até o diretório /src/main/java/com/webpubsub/tutorial, abrir o arquivo App.java em seu editor, usar Javalin.create para servir arquivos estáticos:
package com.webpubsub.tutorial;
import com.azure.messaging.webpubsub.WebPubSubServiceClient;
import com.azure.messaging.webpubsub.WebPubSubServiceClientBuilder;
import com.azure.messaging.webpubsub.models.GetClientAccessTokenOptions;
import com.azure.messaging.webpubsub.models.WebPubSubClientAccessToken;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Expecting 1 arguments: <connection-string>");
return;
}
// create the service client
WebPubSubServiceClient service = new WebPubSubServiceClientBuilder()
.connectionString(args[0])
.hub("chat")
.buildClient();
// start a server
Javalin app = Javalin.create(config -> {
config.addStaticFiles("public");
}).start(8080);
// Handle the negotiate request and return the token to the client
app.get("/negotiate", ctx -> {
GetClientAccessTokenOptions option = new GetClientAccessTokenOptions();
option.addRole("webpubsub.sendToGroup.stream");
option.addRole("webpubsub.joinLeaveGroup.stream");
WebPubSubClientAccessToken token = service.getClientAccessToken(option);
// return JSON string
ctx.result("{\"url\":\"" + token.getUrl() + "\"}");
return;
});
}
}
Dependendo da sua configuração, talvez seja necessário definir explicitamente o nível de linguagem para Java 8 no pom.xml. Adicione o seguinte trecho:
Crie uma página HTML com o conteúdo abaixo e salve-a como wwwroot/index.html:
Crie uma página HTML com o conteúdo abaixo e salve-a como public/index.html:
Crie uma página HTML com o conteúdo abaixo e salve-a como public/index.html:
Crie uma página HTML com o conteúdo abaixo e salve-a em /src/main/resources/public/index.html:
<html>
<body>
<div id="output"></div>
<script>
(async function () {
let res = await fetch('/negotiate')
let data = await res.json();
let ws = new WebSocket(data.url, 'json.webpubsub.azure.v1');
ws.onopen = () => {
console.log('connected');
};
let output = document.querySelector('#output');
ws.onmessage = event => {
let d = document.createElement('p');
d.innerText = event.data;
output.appendChild(d);
};
})();
</script>
</body>
</html>
O código acima se conecta ao serviço e imprime qualquer mensagem recebida na página. A principal mudança é que especificamos o subprotocolo ao criar a conexão WebSocket.
Usamos a ferramenta Secret Manager para .NET Core para definir a cadeia de conexão. Execute o comando abaixo, substituindo <connection_string> pelo obtido na etapa anterior, e abra http://localhost:5000/index.html no navegador:
dotnet user-secrets init
dotnet user-secrets set Azure:WebPubSub:ConnectionString "<connection-string>"
dotnet run
Agora execute o comando abaixo, substituindo <connection-string> pelo ConnectionString buscado na etapa anterior, e abra http://localhost:8080 no navegador:
export WebPubSubConnectionString="<connection-string>"
node server
Agora execute o comando abaixo, substituindo <connection-string> pelo ConnectionString buscado na etapa anterior, e abra http://localhost:8080 no navegador:
python server.py "<connection-string>"
Agora execute o comando abaixo, substituindo <connection-string> pelo ConnectionString buscado na etapa anterior, e abra http://localhost:8080 no navegador:
Se estiver a utilizar o Chrome, pode premir F12 ou clicar com o botão direito do rato em -Inspect ->>Developer Tools e selecionar o separador Rede. Carregue a página da Web e você pode ver que a conexão WebSocket está estabelecida. Selecione para inspecionar a conexão WebSocket, você pode ver abaixo connected a mensagem de evento é recebida no cliente. Você pode ver que você pode obter o connectionId gerado para este cliente.
Você pode ver que, com a ajuda do subprotocolo, você pode obter alguns metadados da conexão quando a conexão é connected.
O cliente agora recebe uma mensagem JSON em vez de um texto sem formatação. A mensagem JSON contém mais informações, como tipo e origem da mensagem. Assim, você pode usar essas informações para fazer mais processamento para a mensagem (por exemplo, exibir a mensagem em um estilo diferente se for de uma fonte diferente), que você pode encontrar em seções posteriores.
Isso é útil se você quiser transmitir uma grande quantidade de dados para outros clientes em tempo real. Vamos usar esse recurso para criar um aplicativo de streaming de logs, que pode transmitir logs do console para o navegador em tempo real.
using System;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace stream
{
class Program
{
private static readonly HttpClient http = new HttpClient();
static async Task Main(string[] args)
{
// Get client url from remote
var stream = await http.GetStreamAsync("http://localhost:5000/negotiate");
var url = (await JsonSerializer.DeserializeAsync<ClientToken>(stream)).url;
var client = new ClientWebSocket();
client.Options.AddSubProtocol("json.webpubsub.azure.v1");
await client.ConnectAsync(new Uri(url), default);
Console.WriteLine("Connected.");
var streaming = Console.ReadLine();
while (streaming != null)
{
if (!string.IsNullOrEmpty(streaming))
{
var message = JsonSerializer.Serialize(new
{
type = "sendToGroup",
group = "stream",
data = streaming + Environment.NewLine,
});
Console.WriteLine("Sending " + message);
await client.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, default);
}
streaming = Console.ReadLine();
}
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, null, default);
}
private sealed class ClientToken
{
public string url { get; set; }
}
}
}
Crie um stream.js com o seguinte conteúdo.
const WebSocket = require('ws');
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
async function main() {
let res = await fetch(`http://localhost:8080/negotiate`);
let data = await res.json();
let ws = new WebSocket(data.url, 'json.webpubsub.azure.v1');
let ackId = 0;
ws.on('open', () => {
process.stdin.on('data', data => {
ws.send(JSON.stringify({
type: 'sendToGroup',
group: 'stream',
ackId: ++ackId,
dataType: 'text',
data: data.toString()
}));
});
});
ws.on('message', data => console.log("Received: %s", data));
process.stdin.on('close', () => ws.close());
}
main();
O código acima cria uma conexão WebSocket com o serviço e, em seguida, sempre que recebe alguns dados, ele usa ws.send() para publicar os dados. Para publicar para outras pessoas, você só precisa definir typesendToGroup e especificar um nome de grupo na mensagem.
Abra outra janela bash para o stream programa e instale a websockets dependência:
mkdir stream
cd stream
# Create venv
python -m venv env
# Active venv
source ./env/bin/activate
pip install websockets
Crie um stream.py com o seguinte conteúdo.
import asyncio
import sys
import threading
import time
import websockets
import requests
import json
async def connect(url):
async with websockets.connect(url, subprotocols=['json.webpubsub.azure.v1']) as ws:
print('connected')
id = 1
while True:
data = input()
payload = {
'type': 'sendToGroup',
'group': 'stream',
'dataType': 'text',
'data': str(data + '\n'),
'ackId': id
}
id = id + 1
await ws.send(json.dumps(payload))
await ws.recv()
if __name__ == '__main__':
res = requests.get('http://localhost:8080/negotiate').json()
try:
asyncio.get_event_loop().run_until_complete(connect(res['url']))
except KeyboardInterrupt:
pass
O código acima cria uma conexão WebSocket com o serviço e, em seguida, sempre que recebe alguns dados, ele usa ws.send() para publicar os dados. Para publicar para outras pessoas, você só precisa definir typesendToGroup e especificar um nome de grupo na mensagem.
Vamos usar outro terminal e voltar para a pasta raiz para criar um aplicativo logstream-streaming de console de streaming e alternar para a pasta de streaming de fluxo de log:
Agora vamos usar o WebSocket para se conectar ao serviço. Vamos navegar até o diretório /src/main/java/com/webpubsub/quickstart, abrir o arquivo App.java em seu editor e substituir o código pelo abaixo:
package com.webpubsub.quickstart;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.util.concurrent.CompletionStage;
import com.google.gson.Gson;
public class App
{
public static void main( String[] args ) throws IOException, InterruptedException
{
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/negotiate"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
Gson gson = new Gson();
String url = gson.fromJson(response.body(), Entity.class).url;
WebSocket ws = HttpClient.newHttpClient().newWebSocketBuilder().subprotocols("json.webpubsub.azure.v1")
.buildAsync(URI.create(url), new WebSocketClient()).join();
int id = 0;
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String streaming = reader.readLine();
App app = new App();
while (streaming != null && !streaming.isEmpty()){
String frame = gson.toJson(app.new GroupMessage(streaming + "\n", ++id));
System.out.println("Sending: " + frame);
ws.sendText(frame, true);
streaming = reader.readLine();
}
}
private class GroupMessage{
public String data;
public int ackId;
public final String type = "sendToGroup";
public final String group = "stream";
GroupMessage(String data, int ackId){
this.data = data;
this.ackId = ackId;
}
}
private static final class WebSocketClient implements WebSocket.Listener {
private WebSocketClient() {
}
@Override
public void onOpen(WebSocket webSocket) {
System.out.println("onOpen using subprotocol " + webSocket.getSubprotocol());
WebSocket.Listener.super.onOpen(webSocket);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
System.out.println("onText received " + data);
return WebSocket.Listener.super.onText(webSocket, data, last);
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
System.out.println("Bad day! " + webSocket.toString());
WebSocket.Listener.super.onError(webSocket, error);
}
}
private static final class Entity {
public String url;
}
}
Navegue até o diretório que contém o arquivo pom.xml e execute o projeto usando o comando abaixo
Você pode ver que há um novo conceito "grupo" aqui. Grupo é um conceito lógico em um hub onde você pode publicar mensagens em um grupo de conexões. Em um hub, você pode ter vários grupos e um cliente pode se inscrever em vários grupos ao mesmo tempo. Ao usar o subprotocolo, você só pode publicar em um grupo em vez de transmitir para todo o hub. Para obter detalhes sobre os termos, verifique os conceitos básicos.
Como usamos o grupo aqui, também precisamos atualizar a página index.html da Web para ingressar no grupo quando a conexão WebSocket é estabelecida dentro ws.onopen do retorno de chamada.
Você pode ver o cliente se junta ao grupo enviando uma mensagem em joinGroup tipo.
Além disso, atualize ligeiramente a lógica de retorno de chamada para analisar a ws.onmessage resposta JSON e imprimir as mensagens somente do grupo para que ele atue como impressora de stream transmissão ao vivo.
ws.onmessage = event => {
let message = JSON.parse(event.data);
if (message.type === 'message' && message.group === 'stream') {
let d = document.createElement('span');
d.innerText = message.data;
output.appendChild(d);
window.scrollTo(0, document.body.scrollHeight);
}
};
Por motivos de segurança, por padrão, um cliente não pode publicar ou assinar um grupo sozinho. Então você notou que definimos roles para o cliente ao gerar o token:
O exemplo de código completo deste tutorial pode ser encontrado aqui.
Próximos passos
Este tutorial fornece uma ideia básica de como se conectar ao serviço Web PubSub e como publicar mensagens para os clientes conectados usando o subprotocolo.
Confira outros tutoriais para se aprofundar em como usar o serviço.