WebSocketStream: integrando streams com a API WebSocket

Evite que o app fique sobrecarregado por mensagens WebSocket ou inunde um servidor WebSocket com mensagens aplicando a contrapressão.

Contexto

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 como quiser. 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 diminuir a entrega, conforme apropriado.

O problema com a API WebSocket atual

Não é possível aplicar pressão de retorno às mensagens recebidas.

Com a API WebSocket atual, a reação a uma mensagem acontece em WebSocket.onmessage, uma EventHandler chamada 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 rapidamente do que o método process() consegue processá-las, o processo de renderização preenche a memória armazenando essas mensagens em buffer, não responde 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 pressão de retorno às mensagens enviadas, mas envolve a pesquisa da 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 que ainda não foram 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 subindo.

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 "de graça", 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 no 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. Para começar, construa uma nova WebSocketStream e transmita a ela o URL do servidor WebSocket. Em seguida, aguarde até que a conexão seja opened, o que resulta em uma ReadableStream e/ou uma WritableStream.

Ao chamar o método ReadableStream.getReader(), você finalmente recebe um ReadableStreamDefaultReader, do qual você pode read() dados até que o stream seja concluído, ou seja, até ele retornar um objeto no formato {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);
  }

Contrapressão

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

Exemplos avançados

O segundo argumento para WebSocketStream é um pacote de opções que permite 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 extensions em potencial 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 disponibilizadas pelos 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 não limpo. 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 a finalidade principal dele é permitir a especificação do código e da razão para o envio 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 pressão de retorno às mensagens enviadas, mas envolve a pesquisa da 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 ver 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.

Fale sobre o design da API

Há algo na API que não funciona como esperado? Ou faltam métodos ou propriedades que você precisa para implementar sua ideia? Tem alguma dúvida ou comentário sobre o modelo de segurança? Registre um problema de especificação no repositório do GitHub (link em inglês) correspondente ou adicione sua opinião a um problema.

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 digite Blink>Network>WebSockets na caixa Components. O Glitch é ótimo para compartilhar casos de reprodução rápidos e fáceis.

Mostrar suporte à 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 informe onde e como você está usando essa hashtag.

Links úteis

Agradecimentos

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