HTTP/2 e HTTP/3 explicados

2024-03-11 · 28 min read

Read in english

No início da década de 1990, Tim Berners-Lee e sua equipe no CERN trabalharam juntos para formar a base da World Wide Web, definindo quatro peças-chaves para a rede mundial de computadores:

  • Um formato de documento para hipertexto (o HTML)
  • Um protocolo de transmissão de dados (o HTTP)
  • Um navegador de internet para exibir hipertexto (o primeiro navegador, WorldWideWeb)
  • Um servidor para transmitir esses dados (uma versão inicial do httpd)

O HTTP, em questão, aproveitou os protocolos TCP/IP já existentes como meio de transporte de dados. Os bytes de uma mensagem HTTP ficam na camada de aplicação, em azul claro na imagem abaixo.

Modelo OSI

HTTP/0.9

Foi a primeira versão de rascunho do HTTP. O único método existente era o GET; não havia cabeçalhos nem status codes; e o único formato possível de resposta era o HTML. Assim como no HTTP/1.0 e no HTTP/1.1, as mensagens HTTP seguiam um formato de texto em ASCII.

Exemplo de requisição HTTP/0.9:

GET /mypage.html

Exemplo de resposta:

<html>
  A very simple HTML page
</html>

HTTP/1.0

Esta versão deu ao HTTP a sua estrutura atual, parecida com a de um memorando, com cabeçalhos e conteúdo, também introduzindo novos métodos (HEAD e POST), MIME types, status codes e versionamento do protocolo.

Exemplo de requisição HTTP/1.0:

GET /mypage.html HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)

Exemplo de resposta:

200 OK
Date: Tue, 15 Nov 1994 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/html

<HTML>
A page with an image
  <IMG SRC="/myimage.gif">
</HTML>

HTTP/1.1

Esta versão surgiu no início de 1997, poucos meses depois da predecessora. As principais mudanças foram:

  • Reaproveitamento de conexões TCP (keep-alive), poupando recursos da máquina e de rede. Na versão anterior, uma conexão nova era criada para cada requisição e encerrada logo após a resposta.
  • Cabeçalho Host, permitindo que mais de um servidor esteja alocado sob um mesmo IP.
  • Convenções de cabeçalhos de encoding, cache, idioma e MIME type.

Exemplo de requisição HTTP/1.1:

GET /api/fruit/orange HTTP/1.1
Host: www.fruityvice.com
Accept-Encoding: gzip, deflate, br

Exemplo de resposta:

HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Sun, 10 Mar 2024 20:44:25 GMT
Transfer-Encoding: chunked
Connection: keep-alive
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Pragma: no-cache
X-Frame-Options: DENY
Content-Type: application/json
Expires: 0

{"name":"Orange","id":2,"family":"Rutaceae","order":"Sapindales","genus":"Citrus","nutritions":{"calories":43,"fat":0.2,"sugar":8.2,"carbohydrates":8.3,"protein":1.0}}
HTTP1 em pacotes TCP

HTTP/2

Em 2015, após muitos anos de observação e estudos sobre a performance da internet em geral, foi proposto e criado o HTTP/2, uma nova versão, baseada no SPDY do Google.

Dentre as principais mudanças, estão a multiplexação de várias mensagens dentro de um único pacote TCP; formato binário das mensagens; e compressão dos cabeçalhos, com HPACK.

No HTTP/1.1, duas requisições HTTP não podem trafegar juntas em uma mesma conexão TCP - é necessário que a primeira delas termine para que a subseqüente se inicie. Isso se chama bloqueio de cabeça de fila (head-of-line blocking, em inglês). No diagrama abaixo, a requisição 2 não pode começar até que a resposta 1 tenha chegado, considerando que apenas uma conexão TCP é usada.

sequenceDiagram Cliente->>+Servidor: req 1 Servidor-->>-Cliente: res 1 Cliente->>+Servidor: req 2 Servidor-->>-Cliente: res 2

Com o HTTP/2, esse problema é resolvido através de streams: cada stream corresponde a uma mensagem. Vários streams podem estar entremeados dentro de um mesmo pacote TCP. Se um stream não puder emitir dados por algum motivo, outros podem aproveitar e entrar em seu lugar no pacote TCP.

Os streams são divididos em frames, cada um contendo: o tipo do frame, o stream ao qual pertence, e o comprimento em bytes. No diagrama abaixo, um retângulo colorido é um pacote TCP e um ✉ é um frame HTTP/2. O primeiro e o terceiro pacotes TCP carregam frames de streams diferentes.

sequenceDiagram rect rgb(239, 190, 125) Cliente->>+Servidor: req1: #9993;1/1<br>+<br>req2: #9993;1/1 end rect rgb(197, 234, 189) Servidor-->>Cliente: res1: #9993;1/2 end rect rgb(197, 234, 189) Servidor-->>-Cliente: res1: #9993;2/2<br>+<br>res2: #9993;1/1 end

A imagem abaixo mostra como os frames entram em pacotes TCP. O stream 1 representa uma resposta HTTP de um arquivo JavaScript e o stream 2 representa uma resposta HTTP de um arquivo CSS, transmitidos via HTTP/2.

Frames HTTP2 em pacotes TCP

HTTP/3

O HTTP/3 surgiu diante de um novo protocolo de transporte proposto pelo Google, o QUIC, em 2012. O QUIC é encapsulado dentro do UDP, e comparado ao TCP, propõe:

  • menos roundtrips (idas-e-voltas) de pacotes para estabelecimento de conexão e estabelecimento de criptografia TLS;
  • ter conexões mais resilientes quanto a perda de pacotes;
  • resolver o bloqueio de cabeça de fila que existe no protocolo TCP e no TLS.

O HTTP/2 consegue resolver o bloqueio de cabeça de fila relacionado ao HTTP, porém, esse tipo de bloqueio também existe no protocolo TCP e no TLS. O TCP entende que os dados que deve enviar fazem parte de uma seqüência de pacotes contígüos, e se um desses pacotes for perdido, ele deve ser reenviado para o destinatário, a fim de que se preserve a integridade da informação. No TCP, pacotes subseqüentes não podem ser enviados enquanto o pacote perdido não chegar com sucesso no destino.

O diagrama abaixo explica visualmente como isso ocorre no HTTP/2. O segundo pacote tinha frames apenas da resposta 1, porém a perda dele atrasa tanto a resposta 1 como a resposta 2 - ou seja, não há paralelismo nesse caso.

sequenceDiagram rect rgb(239, 190, 125) Cliente->>+Servidor: req1: #9993;1/1<br>+<br>req2: #9993;1/1 end rect rgb(197, 234, 189) Servidor--xCliente: res1: #9993;1/2 end Note over Cliente,Servidor: pacote TCP perdido<br>precisa ser reenviado.<br>atrasa tanto res1 como res2 rect rgb(197, 234, 189) Servidor-->>Cliente: res1: #9993;1/2 end rect rgb(197, 234, 189) Servidor-->>-Cliente: res1: #9993;2/2<br>+<br>res2: #9993;1/1 end

Para resolver o bloqueio de cabeça de fila do TCP, o QUIC opta por utilizar o UDP como protocolo de transporte, pois este é um protocolo sem garantias de recebimento. A responsabilidade de garantia de integridade, que no TCP fica na camada de transporte, passa no QUIC para a camada de aplicação, de modo que os frames de uma mensagem podem chegar fora de ordem, sem bloquear streams não-relacionados.

sequenceDiagram rect rgb(255, 179, 217) Cliente->>Servidor: req1: #9993;1/1<br>+<br>req2: #9993;1/1 end rect rgb(179, 205, 230) Servidor--xCliente: res1: #9993;1/2 end Note over Cliente,Servidor: pacote QUIC perdido<br>não bloqueia outros pacotes rect rgb(179, 205, 230) Servidor-->>Cliente: res1: #9993;2/2<br>+<br>res2: #9993;1/1 end Note over Cliente,Servidor: reenvio do pacote perdido.<br>res2 não foi afetado rect rgb(179, 205, 230) Servidor-->>Cliente: res1: #9993;1/2 end
Pacotes QUIC HTTP3

O bloqueio de cabeça de fila relacionado ao TLS (criptografia SSL) ocorre no TCP porque a criptografia é geralmente aplicada sobre a mensagem inteira, de modo que todos os seus pacotes precisam chegar ao destino para então ocorrer a decriptação. No caso do QUIC, a criptografia é individual para cada pacote QUIC, que é decriptado na chegada, sem haver a necessidade de receber todos os pacotes primeiro.

TLS com TCP:

  1. Dados de entrada: A+B+C
  2. Dados encriptados: crypt(A+B+C) = D+E+F
  3. Pacotes: D, E, F
  4. Recebimento: decrypt(D+E+F)
  5. A+B+C

TLS com QUIC:

  1. Dados de entrada: A+B+C
  2. Dados encriptados: crypt(A) = X, crypt(B) = Y, crypt(C) = Z
  3. Pacotes: X, Y, Z
  4. Recebimento: decrypt(X) + decrypt(Y) + decrypt(Z)
  5. A+B+C

Tabela de comparação

HTTP/1.1HTTP/2HTTP/3
Protocolo
de transporte
TCP,
conexão persistente
TCP,
conexão persistente
UDP,
conexão persistente
Bloqueio de
cabeça de fila
(HOL blocking)
HTTP/1.x HOL
TCP HOL
TLS HOL
TCP HOL
TLS HOL
-
Formato das mensagenstexto em ASCIIbináriobinário
Compressão de cabeçalhos-HPACKQPACK
Nº de idas-e-voltas
para iniciar
(handshakes)
3
1 do TCP
+2 do TLS 1.2*
2
1 do TCP
+1 do TLS 1.3*
0
0 do UDP
+0 do TLS 1.3 com 0-RTT*
Identificação de conexãoIP e porta de origemIP e porta de origemconnection ID**,
resistente a mudanças de IP
Criptografianão obrigatória;
aplicada na mensagem inteira
não obrigatória;
aplicada na mensagem inteira
TLS 1.3 embutido;
aplicada por pacote QUIC

* O TLS 1.2 requer 2 roundtrips para handshake criptográfico e o TLS 1.3 requer apenas 1, com a opção de 0-RTT (zero roundtrip time resumption), em que não há necessidade de handshake prévio. Porém, o 0-RTT possibilita ataques de replay e por isso é inseguro. Ele é opcional e pode ser deixado desabilitado.

** O connection ID do QUIC pode ser usado para fingerprinting, colocando em risco a privacidade dos usuários, segundo pesquisa.

Qual é a melhor versão?

As duas melhores versões atualmente são o HTTP/2 e o HTTP/3.

O HTTP/3 foi desenhado para conexões instáveis, como redes de telefonia celular e de satélite. Para contornar instabilidades de rede, o QUIC tem grande independência dos fluxos de dados e resiliência caso pacotes sejam perdidos. Porém, o HTTP/3 tem desvantagens de performance, em razão de 1) o protocolo UDP não ter sido otimizado pelos sistemas operacionais e roteadores ao longo das últimas décadas, devido ao baixo uso dele em geral, tornando-o comparativamente mais lento do que o TCP; e 2) a criptografia pacote-por-pacote no QUIC requer um número maior de operações matemáticas, tornando-a menos eficiente do que a criptografia de mensagem inteira no TCP. Há ainda o problema de o protocolo UDP (usado pelo QUIC) ser restringido em algumas redes para proteger contra, por exemplo, ataque de inundação de UDP e ataque de amplificação de DNS.

Em conexões confiáveis e plenas, o HTTP/2 muitas vezes oferece performance melhor do que o HTTP/3.

Para evitar o bloqueio de cabeça de fila no HTTP/1.x, muitos navegadores e HTTP clients abrem várias conexões TCP, para que as requisições corram em paralelo. Em cenários com muitas requisições e respostas pesadas em paralelo, essa técnica pode fazer com que o HTTP/1.x ofereça maior taxa de transferência (throughput) comparado ao HTTP/2 ou HTTP/3, porém, é uma forma menos eficiente de se resolver o problema. Uma solução alternativa é ter múltiplas conexões HTTP/2-3 ao mesmo tempo (exemplo em C#).

De modo geral, recomenda-se realizar testes de compatibilidade e de performance para decidir qual é a versão mais indicada, além disso, um servidor pode aceitar conexões tanto de HTTP/2 como de HTTP/3, cabendo ao cliente decidir qual versão usar.

Ferramenta de testes

Recomendo o Pororoca (feito por mim 🙂).

Bibliografia

A
AlexandreHTRB

Campinas/SP,
Brasil