WebSocketStream: como integrar streams à API WebSocket

Evite que seu app seja inundado por mensagens do WebSocket ou inunde um servidor do WebSocket com mensagens aplicando a contrapressão.

A API WebSocket oferece uma interface JavaScript para o protocolo WebSocket, o que possibilita abrir uma sessão de comunicação interativa bidirecional entre o navegador do usuário e um servidor. Com essa API, é possível enviar mensagens para um servidor e receber respostas orientadas a eventos sem consultar o servidor para uma resposta.

API Streams

A API Streams permite que o JavaScript acesse de forma programática fluxos de blocos de dados recebidos pela rede e os processe conforme necessário. Um conceito importante no contexto de transmissões é a contrapressão. Esse é o processo pelo qual um único stream ou uma cadeia de pipe regula a velocidade de leitura ou gravação. Quando o stream ou um stream posterior na cadeia de pipe ainda está ocupado e não está pronto para aceitar mais blocos, ele envia um sinal de volta pela cadeia para desacelerar a entrega, conforme apropriado.

O problema com a API WebSocket atual

É impossível aplicar a contrapressão às mensagens recebidas.

Com a API WebSocket atual, a reação a uma mensagem acontece em WebSocket.onmessage, um EventHandler chamado quando uma mensagem é recebida do servidor.

Vamos supor que você tenha um aplicativo que precisa realizar operações pesadas de processamento de dados sempre que uma nova mensagem é recebida. Você provavelmente configuraria o fluxo de forma semelhante ao código abaixo. Como você await o resultado da chamada process(), tudo deve estar certo, certo?

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

Errado! O problema com a API WebSocket atual é que não há como aplicar a contrapressão. Quando as mensagens chegam mais rápido do que o método process() pode processar, o processo de renderização preenche a memória armazenando essas mensagens em buffer, deixa de responder devido ao uso de 100% da CPU ou ambos.

A aplicação de contrapressão às mensagens enviadas não é ergonômica

É possível aplicar a contrapressão às mensagens enviadas, mas isso envolve consultar a propriedade WebSocket.bufferedAmount, que é ineficiente e não ergonômica. Essa propriedade somente leitura retorna o número de bytes de dados que foram enfileirados usando chamadas para WebSocket.send(), mas ainda não transmitidos para a rede. Esse valor é redefinido para zero depois que todos os dados da fila são enviados, mas se você continuar chamando WebSocket.send(), ele vai continuar aumentando.

O que é a API WebSocketStream?

A API WebSocketStream lida com o problema de backpressure inexistente ou não ergonômico integrando streams com a API WebSocket. Isso significa que a contrapressão pode ser aplicada "sem custo financeiro", sem custo extra.

Casos de uso sugeridos para a API WebSocketStream

Exemplos de sites que podem usar essa API:

  • Aplicativos WebSocket de alta largura de banda que precisam manter a interatividade, principalmente em vídeos e compartilhamento de tela.
  • Da mesma forma, a captura de vídeo e outros aplicativos geram muitos dados no navegador que precisam ser enviados para o servidor. Com a contrapressão, o cliente pode parar de produzir dados em vez de acumular dados na memória.

Status atual

Etapa Status
1. Criar uma explicação Concluído
2. Criar um rascunho inicial da especificação Em andamento
3. Coletar feedback e iterar o design Em andamento
4. Teste de origem Concluído
5. Lançamento Não iniciado

Como usar a API WebSocketStream

A API WebSocketStream é baseada em promessas, o que facilita o trabalho com ela em um mundo moderno de JavaScript. Você começa construindo um novo WebSocketStream e transmitindo o URL do servidor WebSocket. Em seguida, aguarde a conexão ser opened, resultando em uma ReadableStream e/ou uma WritableStream.

Ao chamar o método ReadableStream.getReader(), você finalmente recebe um ReadableStreamDefaultReader, que pode ser usado para read() até que o fluxo seja concluído, ou seja, até que ele retorne um objeto do formulário {value: undefined, done: true}.

Assim, ao chamar o método WritableStream.getWriter(), você finalmente recebe um WritableStreamDefaultWriter, para o qual pode enviar dados write().

  const wss = new WebSocketStream(WSS_URL);
  const {readable, writable} = await wss.opened;
  const reader = readable.getReader();
  const writer = writable.getWriter();

  while (true) {
    const {value, done} = await reader.read();
    if (done) {
      break;
    }
    const result = await process(value);
    await writer.write(result);
  }

Limitação de capacidade

E o recurso de contrapressão prometido? Você recebe o acesso "sem custo financeiro", sem etapas extras. Se o process() levar mais tempo, a próxima mensagem só será consumida quando o pipeline estiver pronto. Da mesma forma, a etapa WritableStreamDefaultWriter.write() só prossegue se for seguro.

Exemplos avançados

O segundo argumento para WebSocketStream é um pacote de opções para permitir extensões futuras. A única opção é protocols, que se comporta da mesma forma que o segundo argumento do construtor WebSocket:

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.opened;

O protocol selecionado e o possível extensions fazem parte do dicionário disponível pela promessa WebSocketStream.opened. Todas as informações sobre a conexão em tempo real são fornecidas por essa promessa, já que não é relevante se a conexão falhar.

const {readable, writable, protocol, extensions} = await chatWSS.opened;

Informações sobre a conexão WebSocketStream fechada

As informações disponíveis nos eventos WebSocket.onclose e WebSocket.onerror na API WebSocket agora estão disponíveis pela promessa WebSocketStream.closed. A promessa é rejeitada no caso de um fechamento incorreto. Caso contrário, ela é resolvida com o código e o motivo enviados pelo servidor.

Todos os códigos de status possíveis e o significado deles são explicados na lista de códigos de status CloseEvent.

const {code, reason} = await chatWSS.closed;

Como fechar uma conexão WebSocketStream

Uma WebSocketStream pode ser fechada com um AbortController. Portanto, transmita um AbortSignal para o construtor WebSocketStream.

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

Como alternativa, você também pode usar o método WebSocketStream.close(), mas o objetivo principal dele é permitir a especificação do código e do motivo enviado ao servidor.

wss.close({code: 4000, reason: 'Game over'});

Aprimoramento progressivo e interoperabilidade

No momento, o Chrome é o único navegador que implementa a API WebSocketStream. Para interoperabilidade com a API WebSocket clássica, não é possível aplicar contrapressão às mensagens recebidas. É possível aplicar a contrapressão às mensagens enviadas, mas isso envolve consultar a propriedade WebSocket.bufferedAmount, que é ineficiente e não ergonômica.

Detecção de recursos

Para verificar se a API WebSocketStream é compatível, use:

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

Demonstração

Em navegadores compatíveis, é possível conferir a API WebSocketStream em ação no iframe incorporado ou diretamente no Glitch.

Feedback

A equipe do Chrome quer saber sobre sua experiência com a API WebSocketStream.

Conte sobre o design da API

Há algo na API que não funciona como esperado? Ou há métodos ou propriedades que faltam para implementar sua ideia? Tem alguma dúvida ou comentário sobre o modelo de segurança? Envie um problema de especificação no repositório do GitHub correspondente ou adicione sua opinião a um problema existente.

Informar um problema com a implementação

Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente da especificação? Registre um bug em new.crbug.com. Inclua o máximo de detalhes possível, instruções simples para reprodução e insira Blink>Network>WebSockets na caixa Components. O Glitch é ótimo para compartilhar casos de reprodução rápidos e fáceis.

Mostrar suporte para a API

Você planeja usar a API WebSocketStream? Seu apoio público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegadores a importância de oferecer suporte a eles.

Envie um tweet para @ChromiumDev usando a hashtag #WebSocketStream e nos informe onde e como você está usando.

Links úteis

Agradecimentos

A API WebSocketStream foi implementada por Adam Rice e Yutaka Hirano.