WebSocketStream: como integrar streams à API WebSocket

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

Contexto

A API WebSocket fornece uma interface JavaScript para o protocolo WebSocket, que permite 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 a um servidor e receber respostas orientadas por eventos sem fazer polling do servidor para uma resposta.

API Streams

A API Streams permite que o JavaScript acesse programaticamente fluxos de blocos de dados recebidos pela rede e os processe conforme desejado. Um conceito importante no contexto de streams é a contrapressão. É o processo pelo qual um único stream ou uma cadeia de pipes 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 partes, ele envia um sinal para trás na cadeia para diminuir a entrega conforme necessário.

O problema com a API WebSocket atual

Não é possível aplicar 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 tratamento de dados sempre que uma nova mensagem é recebida. Você provavelmente configuraria o fluxo de maneira semelhante ao código abaixo e, como await o resultado da chamada process(), tudo estaria bem, 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 contrapressão. Quando as mensagens chegam mais rápido do que o método process() pode processá-las, o processo de renderização preenche a memória armazenando em buffer essas mensagens, fica sem resposta devido ao uso de 100% da CPU ou ambos.

Aplicar contrapressão às mensagens enviadas não é ergonômico

É possível aplicar contrapressão às mensagens enviadas, mas isso envolve a sondagem da propriedade WebSocket.bufferedAmount, o que é ineficiente e não ergonômico. 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 quando todos os dados enfileirados 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 contrapressão inexistente ou não ergonômica integrando streams com a API WebSocket. Isso significa que a contrapressão pode ser aplicada "sem custo financeiro".

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 vídeo e compartilhamento de tela.
  • Da mesma forma, a captura de vídeo e outros aplicativos que geram muitos dados no navegador precisam ser enviados para o servidor. Com a contrapressão, o cliente pode parar de produzir dados em vez de acumulá-los na memória.

Status atual

Etapa Status
1. Criar explicação Concluído
2. Criar o 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 JavaScript moderno. Comece construindo um novo WebSocketStream e transmitindo o URL do servidor WebSocket. Em seguida, aguarde a conexão ser opened, o que resulta em um ReadableStream e/ou um WritableStream.

Ao chamar o método ReadableStream.getReader(), você finalmente recebe um ReadableStreamDefaultReader, que pode read() dados 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, que pode ser usado para write() dados.

  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 recurso "sem custo financeiro", sem precisar fazer nada. Se 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 de WebSocketStream é um conjunto 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 os possíveis extensions fazem parte do dicionário disponível pela promessa WebSocketStream.opened. Todas as informações sobre a conexão ativa 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 uma 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 em caso de 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 seus significados são explicados na lista de códigos de status CloseEvent.

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

Como fechar uma conexão WebSocketStream

Um WebSocketStream pode ser fechado com um AbortController. Portanto, transmita um AbortSignal para o construtor WebSocketStream. AbortController.abort() funciona apenas antes do handshake, não depois.

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 enviados ao servidor.

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

Aprimoramento progressivo e interoperabilidade

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

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 suas experiências com a API WebSocketStream.

Fale sobre o design da API

Há algo na API que não funciona como esperado? Ou há métodos ou propriedades ausentes que você precisa implementar sua ideia? Tem uma dúvida ou um comentário sobre o modelo de segurança? Registre um problema de especificação no repositório do GitHub correspondente ou adicione suas ideias 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 Componentes.

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 conte para nós onde e como você está usando.

Links úteis

Agradecimentos

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