WebSocketStream: integrando streams com a API WebSocket

Evite que seu app se afogue em mensagens WebSocket ou inunde um servidor WebSocket com mensagens aplicando pressão de retorno.

Contexto

A API WebSocket

A API WebSocket fornece uma interface JavaScript para o protocolo WebSocket, o 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, você pode enviar mensagens a um servidor e receber respostas orientadas a eventos sem consultar o servidor por uma resposta.

A API Streams

A API Streams permite que o JavaScript acesse, programaticamente, fluxos de blocos de dados recebidos pela rede e processá-las como quiser. Um conceito importante no contexto de streams é contrapressão. É o processo pelo qual um único stream ou uma cadeia de pipe regula a velocidade da leitura ou escrita. Quando o stream em si ou um stream posterior na cadeia do pipeline ainda está ocupado e ainda não estiver pronto para aceitar mais partes, ele envia um sinal para trás pela cadeia para diminuir a velocidade da entrega, conforme apropriado.

O problema com a atual API WebSocket

É impossível aplicar pressão de retorno às mensagens recebidas

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

Vamos supor que você tenha um aplicativo que precise realizar operações pesadas de processamento de dados sempre que uma nova mensagem é recebida. Você provavelmente configuraria um fluxo semelhante ao código abaixo, e já que você await o resultado da chamada process() está bom, 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 atual API WebSocket é que não há como aplicar pressão de retorno. Quando as mensagens chegam mais rápido 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 respondem devido ao uso de 100% da CPU ou ambos.

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

É possível aplicar pressão de retorno às mensagens enviadas, mas envolve a sondagem do WebSocket.bufferedAmount , que é ineficiente e não ergonômica. Esta 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 à rede. Esse valor é redefinido como zero assim que todos os dados na fila são enviados, mas se você continuar chamando WebSocket.send(), ela continuará subindo.

O que é a API WebSocketStream?

A API WebSocketStream lida com o problema de pressão de retorno inexistente ou não ergonômica integrando streams 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:

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

Status atual

Etapa Status
1. Criar explicação Concluído
2. Criar um rascunho inicial da especificação Em andamento
3. Colete feedback e iterar no design Em andamento
4. Teste de origem Concluído
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 ela pareça natural. 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 um ReadableStream e/ou um WritableStream

Ao chamar o método ReadableStream.getReader() você finalmente consegue uma ReadableStreamDefaultReader, e você pode read() até que o stream seja concluído, ou seja, até retornar um objeto com o formato {value: undefined, done: true}

Da mesma forma, ao chamar o método WritableStream.getWriter() você finalmente consegue uma WritableStreamDefaultWriter, e você pode write() aos 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);
  }

Contrapressão

E o recurso de contrapressão prometido? Como eu escrevi acima, você recebe "sem custo financeiro", nenhuma etapa adicional é necessária. 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ó vai prosseguir se for seguro.

Exemplos avançados

O segundo argumento para WebSocketStream é um pacote de opções que permite extensões futuras. Atualmente, a única opção é protocols, que se comporta da mesma forma segundo argumento para o 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íveis pela promessa WebSocketStream.opened. Todas as informações sobre a conexão ativa são fornecidas por essa promessa, porque isso não é relevante em caso de falha na conexão.

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

Informações sobre a conexão WebSocketStream fechada

As informações disponíveis no WebSocket.onclose e WebSocket.onerror eventos na API WebSocket agora está disponível pela promessa WebSocketStream.closed. A promessa é rejeitada no caso de um fechamento com erros, caso contrário, ele é resolvido 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 uma AbortController Portanto, transmita um AbortSignal. ao 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 seu principal objetivo é permitir a especificação código e o motivo que é enviado ao servidor.

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

Aprimoramento progressivo e interoperabilidade

Atualmente, o Chrome é 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 às mensagens recebidas. É possível aplicar pressão de retorno às mensagens enviadas, mas envolve a sondagem do 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 suas experiências com a API WebSocketStream.

Fale sobre o design da API

Alguma coisa na API não funciona como você esperava? Ou faltam métodos ou propriedades que você precisa para implementar sua ideia? Tem uma pergunta ou comentário sobre o modelo de segurança? Registre um problema de especificação no repositório do GitHub correspondente. ou adicionar ideias a um problema.

Informar um problema com a implementação

Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente das especificações? Registre um bug em new.crbug.com. Certifique-se de incluir o máximo de detalhes possível, instruções simples para reprodução, e insira Blink>Network>WebSockets na caixa Componentes. 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 navegador como é fundamental oferecer suporte a eles.

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

Links úteis

Agradecimentos

A API WebSocketStream foi implementada por Adam Rice e Yutaka Hirano (em inglês). Imagem principal de Daan Mooij no Abrir a página.