WebSocketStream: integrando streams com a API WebSocket

Aplique uma pressão de retorno para impedir que seu aplicativo se afogue em mensagens WebSocket ou sobrecarregue um servidor WebSocket com mensagens.

Contexto

API WebSocket

A API WebSocket fornece uma interface JavaScript para o protocolo WebSocket, que possibilita a abertura de 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 baseadas em eventos sem pesquisar uma resposta no servidor.

API Streams

A API Streams permite que o JavaScript acesse de maneira programática os fluxos de blocos de dados recebidos pela rede e os processe conforme desejado. Um conceito importante no contexto de streams é a contrapressão. Esse é o processo em que um único stream ou cadeia de pipeline regula a velocidade de leitura ou gravação. Quando o próprio stream ou um stream mais adiante na cadeia de pipelines ainda estiver ocupado e não estiver pronto para aceitar mais blocos, ele enviará um sinal para trás na cadeia para diminuir a velocidade da entrega, conforme apropriado.

O problema com a atual API WebSocket

É impossível aplicar a pressão de retorno à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 executar operações pesadas de processamento de dados sempre que uma nova mensagem é recebida. Você provavelmente configuraria o fluxo de maneira semelhante ao código abaixo, e, como você aplica await no resultado da chamada process(), está tudo pronto.

// 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 pressão de retorno. Quando as mensagens chegam mais rápido do que o método process() pode processá-las, o processo de renderização enche a memória armazenando essas mensagens em buffer, para de responder devido ao uso total da CPU ou ambos.

A opção de pressão de retorno nas mensagens enviadas não é ergonômica

É possível aplicar a pressão de retorno às mensagens enviadas, mas isso 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 colocados na fila usando chamadas para WebSocket.send(), mas ainda não transmitidos para a rede. Esse valor será redefinido como zero quando todos os dados na fila forem enviados. No entanto, se você continuar chamando WebSocket.send(), ele vai continuar subindo.

O que é a API WebSocketStream?

A API WebSocketStream lida com o problema da pressão de retorno inexistente ou não ergonômica ao integrar fluxos com a API WebSocket. Isso significa que a pressão de retorno pode ser aplicada "sem custo financeiro", sem nenhum custo extra.

Casos de uso sugeridos para a API WebSocketStream

Exemplos de sites que podem usar essa API incluem:

  • Aplicativos WebSocket de alta largura de banda que precisam manter interatividade, em especial o compartilhamento de vídeo e tela.
  • Da mesma forma, a captura de vídeo e outros aplicativos que 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 | [Completo][explicador] | | 2. Criar rascunho inicial da especificação | [Em andamento][spec] | | 3. Colete feedback e repita o design | [Em andamento](#feedback) | | 4. Teste de origem | [Concluído][ot] | | 5. Lançamento | Não iniciado |

Como usar a API WebSocketStream

Exemplo introdutório

A API WebSocketStream é baseada em promessas, o que faz com que lidar com ela pareça natural em um mundo moderno do JavaScript. Comece construindo um novo WebSocketStream e transmita a ele o URL do servidor WebSocket. Em seguida, aguarde a conexão ser opened, o que resulta em um ReadableStream e/ou uma WritableStream.

Ao chamar o método ReadableStream.getReader(), você finalmente recebe um ReadableStreamDefaultReader, do qual pode enviar dados read() até que o stream seja concluído, ou seja, até retornar um objeto do formulário {value: undefined, done: true}.

Da mesma forma, ao chamar o método WritableStream.getWriter(), você finalmente consegue um WritableStreamDefaultWriter, para receber dados de 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 pressão de retorno prometido? Como escrevi acima, você recebe "sem custo financeiro", sem necessidade de outras etapas. Se o process() demorar mais, a próxima mensagem só será consumida quando o pipeline estiver pronto. Da mesma forma, a etapa WritableStreamDefaultWriter.write() só vai prosseguir se for seguro.

Exemplos avançados

O segundo argumento para WebSocketStream é um bag de opções para permitir extensão futura. Atualmente, a única opção é protocols, que se comporta da mesma forma que o segundo argumento para o construtor do 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 ativa são fornecidas por essa promessa, porque não será 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 que estavam 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 não limpo. Caso contrário, ela será 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 encerrar uma conexão do WebSocketStream

Um WebSocketStream pode ser fechado 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 que é enviado ao servidor.

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

Aprimoramento progressivo e interoperabilidade

O Chrome é atualmente o único navegador a implementar a API WebSocketStream. Para interoperabilidade com a API WebSocket clássica, não é possível aplicar pressão de retorno a mensagens recebidas. É possível aplicar a pressão de retorno às mensagens enviadas, mas isso 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, você pode ver 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

Algo na API não funciona como esperado? Ou há métodos ou propriedades ausentes que você precisa para implementar sua ideia? Você tem alguma dúvida ou 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. O Glitch funciona muito bem para compartilhar casos de reprodução rápidos e fáceis.

Mostrar suporte para a API

Você pretende usar a API WebSocketStream? Seu suporte público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegador como esse suporte é essencial.

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

Links úteis

Agradecimentos

A API WebSocketStream foi implementada por Adam Rice e Yutaka Hirano. Imagem principal de Daan Mooij no Unsplash (links em inglês).