Como o JavaScript funciona: Aprofundando em WebSockets e HTTP/2 com SSE + como escolher o caminho certo

Robisson Oliveira
React Brasil
14 min readFeb 27, 2019

--

Este é o post nº 5 da série dedicada a explorar o JavaScript e seus componentes de construção. No processo de identificação e descrição dos elementos centrais, também compartilhamos algumas regras gerais que usamos ao criar o SessionStack , um aplicativo JavaScript leve que precisa ser robusto e altamente eficiente para se manter competitivo.

Abaixo estão os posts dessa série publicados até o momento

Desta vez, vamos mergulhar no mundo dos protocolos de comunicação, mapeando e discutindo seus atributos e construindo partes no caminho. Vamos oferecer uma comparação rápida de WebSockets e HTTP/2. No final, compartilhamos algumas ideias sobre como escolher o caminho a seguir quando se trata de protocolos de rede.

Introdução

Hoje em dia, aplicativos web complexos que apresentam UIs ricas e dinâmicas são tomados como garantidos. E não é surpreendente — a internet percorreu um longo caminho desde a sua criação.

Inicialmente, a Internet não foi criada para suportar esses aplicativos da Web dinâmicos e complexos. Foi concebido para ser uma coleção de páginas HTML, ligando-se umas às outras para formar o conceito de “web” que contém informações. Tudo foi construído em grande parte em torno do chamado paradigma de solicitação/resposta do HTTP. Um cliente carrega uma página e nada acontece até que o usuário clique e navegue para a próxima página.

Por volta de 2005, o AJAX foi introduzido e muitas pessoas começaram a explorar as possibilidades de fazer conexões entre um cliente e um servidor bidirecional . Ainda assim, toda a comunicação HTTP foi direcionada pelo cliente, o que exigiu interação do usuário ou pesquisa periódica para carregar novos dados do servidor.

Tornando o HTTP “bidirecional”

As tecnologias que permitem que o servidor envie dados para o cliente “de forma proativa” já existem há algum tempo. “ Push “ e “ Comet “ para citar alguns.

Um dos hacks mais comuns para criar a ilusão de que o servidor está enviando dados para o cliente é chamado long polling(pesquisa longa) . Com o long polling, o cliente abre uma conexão HTTP com o servidor, que o mantém aberto até que uma resposta seja enviada. Sempre que o servidor tiver novos dados que precisam ser enviados, ele os transmite como uma resposta.

Vamos ver como um snippet de long polling muito simples pode parecer:

Esta é basicamente uma função auto-executável que é executada pela primeira vez automaticamente. Ele configura o intervalo de dez (10) segundos e, após cada chamada Ajax assíncrona para o servidor, o retorno de chamada chama ajax novamente.

Outras técnicas envolvem solicitação multipart de Flash ou XHR e os chamados htmlfiles .

Todas essas soluções alternativas compartilham o mesmo problema: elas carregam a sobrecarga do HTTP, o que não as torna adequadas para aplicativos de baixa latência. Pense em jogos de tiro em primeira pessoa multiplayer no navegador ou em qualquer outro jogo online com um componente em tempo real.

A introdução de WebSockets

A especificação WebSocket define uma API que estabelece conexões de “soquete” entre um navegador da web e um servidor. Em palavras simples: há uma conexão persistente entre o cliente e o servidor e ambas as partes podem começar a enviar dados a qualquer momento.

O cliente estabelece uma conexão WebSocket por meio de um processo conhecido como handshake do WebSocket. Esse processo começa com o cliente enviando uma solicitação HTTP regular para o servidor. Um cabeçalho de Upgrade é incluído neste pedido que informa ao servidor que o cliente deseja estabelecer uma conexão WebSocket.

Vamos ver como é a abertura de uma conexão WebSocket no lado do cliente:

URLs de WebSocket usam o esquema ws . Há também o wss para conexões seguras do WebSocket, que é o equivalente do HTTPS .

Este esquema apenas inicia o processo de abertura de uma conexão WebSocket para websocket.example.com.

Aqui está um exemplo simplificado dos cabeçalhos de solicitação inicial.

GET ws://websocket.example.com/ HTTP/1.1 
Origin: http://example.com
Connection: Upgrade
Host: websocket.example.com
Upgrade: websocket

Se o servidor suportar o protocolo WebSocket, ele concordará com a atualização e comunicará isso através do cabeçalho Upgrade na resposta.

Vamos ver como isso pode ser implementado no Node.JS:

Depois que a conexão é estabelecida, o servidor responde atualizando:

HTTP/1.1 101 Protocolos de Comutação 
Date: Wed, 25 Oct 2017 10:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket

Depois que a conexão tiver sido estabelecida, o evento open será disparado em sua instância do WebSocket no lado do cliente:

Agora que o handshake está concluído, a conexão HTTP inicial é substituída por uma conexão WebSocket que usa a mesma conexão TCP/IP subjacente. Neste momento, qualquer uma das partes pode começar a enviar dados.

Com WebSockets, você pode transferir quantos dados quiser sem incorrer na sobrecarga associada às solicitações HTTP tradicionais. Os dados são transferidos por meio de um WebSocket como mensagens , cada uma das quais consiste em um ou mais frames (quadros) contendo os dados que você está enviando (a carga útil). Para garantir que a mensagem possa ser adequadamente reconstruída ao atingir o cliente, cada quadro é prefixado com 4 a 12 bytes de dados sobre a carga útil. O uso desse sistema de mensagens baseado em quadros ajuda a reduzir a quantidade de dados não pagos que são transferidos, levando a reduções significativas na latência.

Nota : É importante notar que o cliente só será notificado sobre uma nova mensagem quando todos os quadros forem recebidos e a carga útil da mensagem original tiver sido reconstruída.

URLs do WebSocket

Nós mencionamos brevemente antes que os WebSockets apresentem um novo esquema de URL. Na realidade, eles introduzem dois novos esquemas: ws://e wss:// .

URLs têm gramática específica do esquema. As URLs do WebSocket são especiais porque não suportam âncoras ( #sample_anchor ).

As mesmas regras se aplicam a URLs de estilo WebSocket e a URLs de estilo HTTP. ws é descriptografado e tem a porta 80 como padrão, enquanto o wssrequer criptografia TLS e possui a porta 443 como padrão.

Protocolo de enquadramento

Vamos dar uma olhada mais profunda no protocolo de enquadramento. Isto é o que o RFC nos fornece:

A partir da versão WebSocket especificada pelo RFC, há apenas um cabeçalho na frente de cada pacote. É um cabeçalho bastante complexo, no entanto. Aqui estão seus blocos de construção explicados:

  • fin ( 1 bit ): indica se esse quadro é o quadro final que compõe a mensagem. Na maioria das vezes, a mensagem cabe em um único quadro e esse bit sempre será definido. As experiências mostram que o Firefox faz um segundo quadro após 32K.
  • rsv1 , rsv2 , rsv3 ( 1 bit cada ): deve ser 0, a menos que seja negociada uma extensão que defina significados para valores diferentes de zero. Se um valor diferente de zero for recebido e nenhuma das extensões negociadas definir o significado de um valor diferente de zero, o terminal de recebimento deverá falhar na conexão.
  • opcode ( 4 bits ): diz o que o quadro representa. Os valores a seguir estão em uso atualmente:
    0x00 : esse quadro continua a carga do anterior.
    0x01 : esse quadro inclui dados de texto.
    0x02 : este quadro inclui dados binários.
    0x08 : este quadro termina a conexão.
    0x09 : este quadro é um ping.
    0x0a : esse quadro é um pong.
    (Como você pode ver, há valores suficientes não utilizados; eles foram reservados para uso futuro).
  • mask ( 1 bit ): indica se a conexão está mascarada. Como está agora, cada mensagem de um cliente para um servidor deve ser mascarada e a especificação desejaria terminar a conexão se ela fosse desmascarada.
  • payload_len ( 7 bits ): o tamanho da carga útil. Quadros WebSocket vêm nos seguintes colchetes de comprimento:
    0 a 125 indica o comprimento da carga útil. 126 significa que os dois bytes seguintes indicam o comprimento, 127 significa que os próximos 8 bytes indicam o comprimento. Portanto, o comprimento da carga vem em suportes de 7 bits, 16 bits e 64 bits.
  • masking-key ( 32 bits ): todos os quadros enviados do cliente para o servidor são mascarados por um valor de 32 bits contido no quadro.
  • payload : os dados reais que provavelmente são mascarados. Seu comprimento é o comprimento do payload_len .

Por que os WebSockets são baseados em quadros e não em fluxo? Eu não sei e gosto de você, eu adoraria aprender mais, então se você tiver uma idéia, fique à vontade para adicionar comentários e recursos nas respostas abaixo. Além disso, uma boa discussão sobre o tópico está disponível no HackerNews .

Dados nos quadros

Como mencionado acima, os dados podem ser fragmentados em vários quadros. O primeiro quadro que transmite os dados tem um código de operação que indica que tipo de dados está sendo transmitido. Isso é necessário porque o JavaScript tem praticamente inexistente suporte para dados binários no momento em que a especificação foi iniciada. 0x01 indica dados de texto codificados utf-8, 0x02 são dados binários. A maioria das pessoas transmitirá JSON, caso em que você provavelmente desejará escolher o opcode de texto. Quando você emite dados binários, ele será representado em um Blob específico do navegador.

A API para enviar dados através de um WebSocket é muito simples:

Quando o WebSocket está recebendo dados (no lado do cliente), um evento de message é disparado. Esse evento inclui uma propriedade chamada dataque pode ser usada para acessar o conteúdo da mensagem.

Você pode explorar facilmente os dados em cada um dos quadros na sua conexão WebSocket usando a guia Rede dentro do Chrome DevTools:

Fragmentação

Os dados da carga útil podem ser divididos em vários quadros individuais. A extremidade de recepção deve armazená-los em buffer até que o bit de finesteja definido. Então você pode transmitir a string “Hello World” em 11 pacotes de 6 (comprimento do cabeçalho) + 1 byte cada. Fragmentação não é permitida para pacotes de controle. No entanto, a especificação quer que você seja capaz de lidar com quadros de controle intercalados . Isso é no caso de pacotes TCP chegarem em ordem arbitrária.

A lógica para unir quadros é aproximadamente a seguinte:

  • recebe o primeiro quadro
  • lembre-se opcode
  • concatenar a carga útil do quadro até que o bit da fin esteja definido
  • afirma que o opcode para cada pacote é zero

O principal objetivo da fragmentação é permitir o envio de uma mensagem de tamanho desconhecido quando a mensagem é iniciada. Com a fragmentação, um servidor pode escolher um buffer de tamanho razoável e, quando o buffer está cheio, gravar um fragmento na rede. Um caso de uso secundário para fragmentação é para multiplexação, em que não é desejável que uma mensagem grande em um canal lógico assuma todo o canal de saída, portanto, a multiplexação precisa estar livre para dividir a mensagem em fragmentos menores para compartilhar melhor a saída canal.

O que é o Hearbeat?

A qualquer momento após o handshake, o cliente ou o servidor pode optar por enviar um ping para a outra parte. Quando o ping é recebido, o destinatário deve devolver um pong o mais rápido possível. Isso é um batimento cardíaco. Você pode usá-lo para garantir que o cliente ainda esteja conectado.

Um ping ou pong é apenas um quadro normal, mas é um quadro de controle. Os pings têm um opcode de 0x9 e os pongs têm um opcode de 0xA .Quando você receber um ping, envie de volta um pong com exatamente os mesmos dados de payload que o ping (para pings e pongs, o tamanho máximo da carga é de 125 ). Você também pode receber um pong sem nunca enviar um ping. Ignore se isso acontecer.

A pulsação pode ser muito útil. Existem serviços (como balanceadores de carga) que terminarão conexões ociosas. Além disso, não é possível para o lado de recebimento ver se o lado remoto foi encerrado. Somente no próximo envio você perceberia que algo deu errado.

Manipulando Erros

Você pode manipular quaisquer erros que ocorram ouvindo o evento de error .

Se parece com isso:

Fechando a conexão

Para fechar uma conexão, o cliente ou servidor deve enviar um quadro de controle com dados contendo um código de operação de 0x8 . Ao receber esse quadro, o outro par envia um quadro Close em resposta. O primeiro ponto então fecha a conexão. Quaisquer dados adicionais recebidos após o fechamento da conexão são então descartados.

É assim que você inicia o fechamento de uma conexão WebSocket do cliente:

Além disso, para executar qualquer limpeza após a conclusão do fechamento, você pode anexar um ouvinte de evento ao evento de close :

O servidor deve escutar no evento de close para processá-lo, se necessário:

Como WebSockets e HTTP/2 se comparam?

Embora o HTTP/2 tenha muito a oferecer, ele não substitui completamente a necessidade de tecnologias push/streaming existentes.

A primeira coisa importante a notar sobre o HTTP/2 é que ele não substitui todo o HTTP. Os verbos, códigos de status e a maioria dos cabeçalhos permanecerão os mesmos de hoje. O HTTP/2 é sobre melhorar a eficiência da maneira como os dados são transferidos no fio.

Agora, se compararmos o HTTP/2 ao WebSocket, podemos ver muitas semelhanças:

Como vimos acima, o HTTP/2 introduz o Server Push, que permite que o servidor envie proativamente recursos para o cache do cliente. Não permite, no entanto, empurrar os dados para o próprio aplicativo cliente. Os envios de servidor são processados ​​apenas pelo navegador e não aparecem no código do aplicativo, o que significa que não há API para o aplicativo receber notificações para esses eventos.

É aqui que o Server-Sent Events (SSE) se torna muito útil. O SSE é um mecanismo que permite ao servidor enviar de forma assíncrona os dados para o cliente quando a conexão cliente-servidor é estabelecida. O servidor pode então decidir enviar dados sempre que um novo “bloco” de dados estiver disponível. Pode ser considerado como um modelo de publish/subscribe unidirecional. Ele também oferece uma API padrão de cliente JavaScript chamada EventSource implementada na maioria dos navegadores modernos como parte do padrão HTML5 pelo W3C . Observe que os navegadores que não suportam a API EventSource podem ser facilmente preenchidos com Polifills.

Como o SSE é baseado em HTTP, ele se ajusta naturalmente ao HTTP/2 e pode ser combinado para obter o melhor de ambos: HTTP/2 manipulando uma camada de transporte eficiente baseada em fluxos multiplexados e SSE fornecendo a API até os aplicativos para ativar empurrar.

Para entender completamente o que é o Streams e o Multiplexing, primeiro vamos dar uma olhada na definição do IETF: um “stream” é uma sequência bidirecional e independente de quadros trocados entre o cliente e o servidor dentro de uma conexão HTTP/2. Uma de suas principais características é que uma única conexão HTTP/2 pode conter vários fluxos abertos simultaneamente, com quadros de intercalação de terminais de vários fluxos.

Temos que lembrar que o SSE é baseado em HTTP. Isso significa que com HTTP/2, não apenas vários fluxos SSE podem ser intercalados em uma única conexão TCP, mas o mesmo também pode ser feito com uma combinação de vários fluxos SSE (servidor para cliente push) e várias solicitações de cliente (cliente para servidor). ). Graças ao HTTP/2 e SSE, agora temos uma conexão HTTP bidirecional pura com uma API simples para permitir que o código do aplicativo se registre nos envios do servidor. A falta de recursos bidirecionais tem sido vista como uma grande desvantagem ao comparar o SSE ao WebSocket. Graças ao HTTP 2, este não é mais o caso. Isso abre a oportunidade de pular WebSockets e manter uma sinalização baseada em HTTP.

Como escolher entre WebSocket e HTTP/2?

WebSockets certamente sobreviverá à dominação do HTTP/2 + SSE, principalmente porque é uma tecnologia já bem adotada e, em casos de uso muito específicos, tem uma vantagem sobre o HTTP/2 já que foi construída para capacidades bidirecionais com menos overhead (por exemplo cabeçalhos).

Digamos que você queira criar um jogo on-line para vários jogadores que precisa de uma enorme quantidade de mensagens de ambas as extremidades da conexão. Nesse caso, WebSockets irá realizar muito, muito melhor.

Em geral, use WebSockets sempre que precisar de uma conexão de baixa latência e quase real em tempo real entre o cliente e o servidor. Tenha em mente que isso pode exigir repensar a forma como você cria seus aplicativos do lado do servidor, além de mudar o foco em tecnologias como filas de eventos.

Se o seu caso de uso exigir a exibição de notícias de mercado em tempo real, dados de mercado, aplicativos de bate-papo etc., confiar no HTTP/2 + SSE fornecerá a você um canal de comunicação bidirecional eficiente, aproveitando os benefícios de permanecer no mundo HTTP:

  • Os WebSockets podem ser uma fonte de dificuldades ao considerar a compatibilidade com a infraestrutura da Web existente, pois atualiza uma conexão HTTP para um protocolo completamente diferente que não tem nada a ver com HTTP.
  • Escala e segurança: Os componentes da Web (Firewalls, detecção de intrusões, balanceadores de carga) são criados, mantidos e configurados com o HTTP em mente, um ambiente que os aplicativos grandes / críticos preferirão em termos de resiliência, segurança e escalabilidade.

Além disso, você deve levar em consideração o suporte ao navegador. Dê uma olhada no WebSocket:

É muito bom, não é?

A situação com o HTTP/2, no entanto, não é a mesma:

  • Apenas TLS (o que não é tão ruim)
  • Suporte parcial no IE 11, mas apenas no Windows 10
  • Apenas suportado no OSX 10.11+ no Safari
  • Suporta somente HTTP/2 se você puder negociá-lo via ALPN (que é algo que seu servidor precisa suportar explicitamente)

O suporte a SSE é melhor embora:

Apenas o IE/Edge não fornece suporte. (Bem, o Opera Mini não suporta SSE nem WebSockets, então podemos tirar tudo da equação). Existem alguns polyfills decentes disponíveis para suporte a SSE no IE/Edge.

Como tomamos a decisão no SessionStack

Nós da SessionStack usamos WebSockets e HTTP, dependendo do caso.Depois de integrar o SessionStack no seu aplicativo da web, ele começa a registrar todas as alterações do DOM, interações do usuário, exceções do JavaScript, rastreamentos de pilha, solicitações de rede com falha e mensagens de depuração, permitindo que você repita problemas em seus aplicativos da Web como vídeos e veja tudo o que aconteceu seus usuários.Tudo está acontecendo em tempo real e precisa acontecer sem impacto no desempenho do seu aplicativo da web.

Isso significa que você pode participar de uma sessão do usuário ao vivo, enquanto o usuário ainda estiver no navegador. Nesse cenário, optamos por aproveitar o HTTP, já que não há comunicação bidirecional (o servidor apenas “transmite” os dados para o navegador). Um WebSocket, neste caso, será um exagero, mais difícil de manter e escalar.

A biblioteca SessionStack que é integrada ao seu aplicativo da web, no entanto, usa um WebSocket (se possível, caso contrário, retorna para HTTP).É em lotes e envio de dados para os nossos servidores, que também é uma comunicação unidirecional. Escolhemos o WebSocket nesse caso, porque alguns dos recursos do produto que estão no roteiro exigiriam uma comunicação bidirecional.

Se você quiser experimentar o SessionStack para entender e reproduzir problemas técnicos e de experiência do usuário em seus aplicativos da Web, fornecemos um plano gratuito que permite que você comece de graça .

Referências

Este é um artigo traduzido com a autorização do autor. O artigo original pode ser lido em https://blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7

Autor do post original — Alexander Zlatkov — Co-founder & CEO @SessionStack

--

--

Robisson Oliveira
React Brasil

Senior Cloud Application Architect at Amazon Web Services(AWS). My personal reflections on software development