Pular para o conteúdo
Quiz
← Início

Protocolos

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

Resumo teórico

Quando você pede para um amigo passar o sal, vocês dois seguem regras escondidas: você diz por favor, ele olha para você, entrega o sal e você agradece. Se um dos dois quebrasse as regras, o sal nunca chegaria. Os computadores também precisam de regras assim. Um protocolo é um conjunto de regras que diz quem fala primeiro, como é a cara de uma mensagem e o que cada resposta significa. Como todos os programas seguem as mesmas regras, um navegador escrito em um país consegue conversar com um servidor escrito por outra pessoa em outro país.

Esta área trata dos protocolos que você usa toda vez que abre uma página, assiste a um vídeo, conversa em um chat ou chama o servidor de um aplicativo. Começamos pelo HTTP, a língua da web: seus métodos, suas respostas numeradas chamadas códigos de status e seus bilhetes chamados cabeçalhos (headers). Depois vemos como a web economiza tempo com caches, como os cookies lembram de você e como os navegadores protegem você com o CORS. Em seguida vem o TLS, o cadeado que guarda os segredos, e as três versões do HTTP (1.1, 2 e 3), que levam as mesmas mensagens de jeitos cada vez mais rápidos.

Em cima do HTTP as pessoas constroem APIs, que são cardápios de coisas que um programa pode pedir para outro programa fazer. Você vai conhecer os estilos REST, GraphQL, JSON-RPC e gRPC, e também o WebSocket e os server-sent events, que deixam o servidor falar com você sem ser perguntado primeiro. No fim você vai saber para que serve cada um e quanto cada um custa, que é exatamente o que o quiz pergunta.

O panorama: conversar pela rede

Pense em um restaurante. Você é o cliente (client): pede coisas. O garçom leva o pedido à cozinha, que é o servidor (server): ela prepara e manda de volta. Na web é igual. O navegador envia uma requisição (request: 'me dê esta página') e o servidor envia uma resposta (response: 'aqui está'). A conversa sempre começa pelo cliente.

As mensagens não voam por mágica. Elas passam por camadas, como uma carta que vai dentro de um envelope, que vai dentro de um saco, que vai em um caminhão. Cada camada tem o seu trabalho.

  seu programa     'GET /foto'            <- HTTP (esta área)
       |
  criptografia     embaralha os bytes     <- TLS
       |
  transporte       entrega os bytes       <- TCP ou QUIC (UDP)
       |
  a rede           leva pacotes           <- IP, cabos, Wi-Fi
As camadas que uma requisição web atravessa, do seu programa até o cabo.
PalavraSignificado
ProtocoloUm conjunto de regras para uma conversa entre programas.
Confiável, mas faz você esperar pela primeira peça que faltou.
Rápido e simples. O QUIC é construído em cima dele.
A unidade que usamos para contar como um handshake é lento.
LatênciaO atraso da rede. Latência alta significa que cada round trip é lento.
↑ Voltar ao sumário

Semântica do HTTP: métodos, códigos de status e cabeçalhos

O HTTP (HyperText Transfer Protocol) é a língua da web. Toda mensagem HTTP tem três partes: uma primeira linha que diz o que se pede (ou como foi), depois os cabeçalhos (headers), que são pequenos bilhetes, e por fim um corpo (body) opcional, que é o conteúdo de verdade, como uma foto ou um texto.

GET /foto.png HTTP/1.1
Host: exemplo.test
Accept: image/png
Uma requisição (os caracteres invisíveis de fim de linha foram omitidos). A primeira linha é a request line: método, alvo, versão.
  • Toda linha termina com dois caracteres invisíveis, CR e LF (escritos \r\n). Uma linha vazia significa 'os cabeçalhos acabaram'.
  • A primeira linha de uma resposta é a status line: a versão, um código de status de três dígitos e um texto curto.
  • No HTTP/1.1 o cabeçalho Host é obrigatório. Um computador pode hospedar muitos sites, e o Host diz qual site você quer. Sem ele (ou com dois), o servidor deve responder 400 Bad Request.

Métodos: os verbos

O método diz o que você quer fazer. As regras oficiais (RFC 9110) descrevem três propriedades. Um método é seguro (safe) se só lê e não pede nenhuma mudança. É idempotente se fazer várias vezes tem o mesmo efeito de fazer uma vez. É cacheável se a resposta pode ser guardada para reutilização.

Todo método seguro é idempotente, mas o contrário não vale. PUT e DELETE são idempotentes, mas não seguros.
MétodoO que fazSeguroIdempotente
GETLê um recursosimsim
HEADIgual ao GET, mas só os cabeçalhos voltam, sem corposimsim
OPTIONSPergunta o que é permitidosimsim
PUTSubstitui um recurso pelo que você envianãosim
DELETERemove um recursonãosim
POSTEnvia algo para ser processado, muitas vezes para criarnãonão
PATCHMuda parte de um recursonãonão

Idempotente fala do efeito, não da resposta. O primeiro DELETE pode responder 204 e o segundo 404, e o método continua idempotente, porque o estado final é o mesmo: a coisa sumiu.

Códigos de status: como foi

O primeiro dígito diz a família: 1xx informação, 2xx sucesso, 3xx vá para outro lugar, 4xx o cliente errou, 5xx o servidor errou.

CódigoSignificadoQuando
200 / 201 / 204OK / Created / No ContentCreated vem com um cabeçalho Location com a nova URL
206Partial ContentResposta a uma requisição Range (retomar um download)
301, 302, 303, 307, 308Redirecionamentos301 e 308 são permanentes; 302 e 307, temporários. 307 e 308 mantêm o método e o corpo; 303 sempre quer dizer 'agora faça um GET em outra página'
304Not ModifiedSua cópia guardada ainda é boa
400Bad RequestA mensagem está quebrada (linha errada, falta Host)
401UnauthorizedNa verdade quer dizer 'não autenticado': diga quem você é. Vem com WWW-Authenticate
403ForbiddenSei quem você é, e a resposta é não
404 / 405Not Found / Method Not AllowedNão existe esse caminho / o caminho existe, mas não para esse método
406 / 415Not Acceptable / Unsupported Media TypeO servidor não consegue dar o formato do Accept / o servidor não aceita o Content-Type que você enviou
409 / 412Conflict / Precondition FailedBate de frente com o estado atual / uma condição If-Match era falsa
413 / 414 / 431Grande demaisO corpo / a URL / os cabeçalhos são grandes demais
422Unprocessable ContentFormato certo, mas o conteúdo quebra uma regra
500 / 501 / 505Erro do servidorBug / não implementado / versão do HTTP não suportada

Onde o corpo termina?

No HTTP/1.1 a conexão fica aberta (isso se chama keep-alive e é o padrão: abrir uma conexão custa um round trip, então a reaproveitamos). A mensagem seguinte vem logo depois da primeira, então quem lê precisa saber exatamente onde cada corpo para. Há duas maneiras.

PerguntaContent-LengthTransfer-Encoding: chunked
Quando usarO tamanho é conhecido antes de enviarO tamanho ainda não é conhecido (um fluxo)
Como terminaLer exatamente essa quantidade de bytesChunks, cada um com seu tamanho em ; um chunk de tamanho 0 encerra
No fiohello worldb\r\nhello world\r\n0\r\n\r\n

Se uma mensagem tem os dois cabeçalhos, o Transfer-Encoding vence e o servidor deve de preferência recusá-la com 400. Por quê? Se um proxy acredita em um e o servidor atrás dele acredita no outro, os dois discordam sobre onde a requisição termina, e os bytes que sobram são lidos como uma nova requisição. Esse ataque se chama request smuggling. Fechar a conexão no fim do corpo também funciona, mas aí ela não pode ser reaproveitada.

Cabeçalhos úteis

  • Content-Type descreve o corpo que você está enviando; Accept diz quais formatos você está disposto a receber.
  • ETag é uma etiqueta (uma impressão digital) da versão atual de um recurso. Mandá-la de volta em If-Match em um PUT quer dizer 'só escreva se ninguém mudou desde que eu li'. Se alguém mudou, o servidor responde 412. Isso evita a atualização perdida (lost update), em que duas pessoas editam e a segunda apaga o trabalho da primeira.
  • Range: bytes=1000- pede o resto de um arquivo. If-Range: "a" acrescenta: 'só se a versão ainda for "a"'. Se for, você recebe 206 com a parte; se o arquivo mudou, recebe 200 com o arquivo novo inteiro.
↑ Voltar ao sumário

Cache HTTP, cookies e CORS

Cache: não carregue a mesma água duas vezes

Um cache guarda uma cópia de uma resposta para que a próxima pessoa que pedir a receba rápido, sem incomodar o servidor. O seu navegador tem um cache privado (só para você). Uma ou um tem um cache compartilhado (para muita gente). O servidor controla tudo isso com o Cache-Control.

Cache-ControlO que diz aos caches
max-age=600A cópia está fresca (fresh) por 600 segundos. Nesse tempo ela é reutilizada sem rede. Depois fica velha (stale), e o cache tenta conferi-la, não jogá-la fora
no-cachePode guardar, mas deve perguntar ao servidor antes de cada reutilização
no-storeNão guarde de jeito nenhum (use para segredos)
privateSó o navegador do próprio usuário pode guardar, não os caches compartilhados
public, s-maxageCaches compartilhados podem guardar. Só para conteúdo igual para todo mundo

Quando uma cópia está velha, o cache a revalida com um GET condicional. Ele envia a etiqueta que guardou: If-None-Match: "abc" (do ETag) ou If-Modified-Since (do Last-Modified). Se nada mudou, o servidor responde 304 Not Modified só com cabeçalhos, e a cópia volta a ficar fresca. Se mudou, um 200 traz o corpo novo.

a cópia do cache está velha
   cache  --- GET /app.css  If-None-Match: "abc" --->  servidor
   cache  <-- 304 Not Modified (sem corpo) ----------  servidor   (reusa a cópia)
   cache  <-- 200 OK + corpo novo + novo ETag -------  servidor   (mudou)
Revalidar custa um round trip, mas quase nenhum byte.

Um cache normalmente acha uma cópia por método + URL. Se o servidor manda versões diferentes para a mesma URL (por exemplo, compressão gzip ou Brotli conforme o Accept-Encoding), ele precisa acrescentar Vary: Accept-Encoding. Aí esse cabeçalho passa a fazer parte da chave, e um cliente que só entende gzip nunca recebe Brotli.

Cookies: um adesivo que o navegador lembra

O HTTP esquece de você entre uma requisição e outra. Um cookie é um bilhete que o servidor entrega com Set-Cookie, e o navegador o devolve nas requisições seguintes. É assim que um site sabe que você está logado.

  • Cookie de sessão: não tem Expires nem Max-Age. Some quando a sessão do navegador termina.
  • Cookie persistente: tem Max-Age (segundos a partir de agora) ou Expires (uma data). Sobrevive ao fechar o navegador. Se existem os dois, o Max-Age vence.
  • Secure: só é enviado por HTTPS (limita o canal).
  • HttpOnly: o JavaScript não consegue ler; só o navegador o anexa às requisições (limita quem lê).
  • SameSite: limita quando é enviado em requisições que começam em outro site.
O Lax bloqueia a maior parte dos ataques CSRF e ainda deixa links de fora manterem você logado.
SameSiteÉ enviado em requisições que vêm de outro site?
StrictNunca
LaxSó quando o usuário navega até o seu site com um método seguro, como clicar em um link. Não em imagens, fetch ou um POST de formulário do outro site
NoneSempre (e exige Secure)

Cuidado: 'mesmo site' não é 'mesma origem'. shop.example.com e other.example.net são sites diferentes, então um cookie Lax não é enviado quando a outra página faz uma requisição.

CORS: a regra de vizinhança do navegador

A origem (origin) de uma página são três coisas juntas: esquema + host + porta. https://app.example.com e https://app.example.com:8443 são origens diferentes. O caminho não conta. A same-origin policy diz que um script de uma origem não pode ler respostas de outra origem. O CORS (Cross-Origin Resource Sharing) é como um servidor abre uma exceção, usando cabeçalhos como Access-Control-Allow-Origin.

  • Uma requisição simples (GET, HEAD ou POST só com cabeçalhos seguros e um Content-Type de formulário) sai direto, porque uma página já podia fazer isso com um formulário antes de o CORS existir.
  • Qualquer outra (um Content-Type JSON, um cabeçalho personalizado, PUT ou DELETE) dispara antes um preflight: o navegador envia uma requisição OPTIONS para perguntar 'você aceita isto?'.
  • Access-Control-Allow-Origin: * junto com Access-Control-Allow-Credentials: true é proibido. Com cookies, o servidor deve citar uma origem específica (conferida contra uma lista) e acrescentar Vary: Origin.
↑ Voltar ao sumário

TLS: o cadeado da conexão

O HTTP normal viaja em texto puro. Qualquer um no caminho (o dono de um Wi-Fi, um roteador) poderia ler ou mudar a mensagem. O TLS (Transport Layer Security) envolve a conversa em um cadeado. O HTTP dentro do TLS é o HTTPS. Antes de qualquer dado da web andar, os dois lados fazem uma dança curta chamada handshake.

Um handshake concluído dá três coisas: os dois combinam uma versão e os métodos de embaralhar segredos, o cliente autentica o servidor (confere se ele é mesmo quem diz ser) e os dois lados ficam com a mesma chave secreta, que mais ninguém conhece. Fazer o cliente provar quem é com um certificado também existe, mas na web é opcional e raro.

Dois jeitos de guardar segredo

TipoAssimétricaSimétrica
IdeiaUma chave pública e uma chave privada que andam juntasUma chave só, compartilhada pelos dois lados
VelocidadeLenta e caraMuito rápida
Usada no TLS paraAssinar (provar identidade) e (combinar um segredo) durante o handshakeProteger todos os dados depois, com um modo AEAD que dá sigilo e detecção de adulteração ao mesmo tempo

Como o cliente confia no servidor

O servidor mostra um certificado: uma carteira de identidade com o nome e a chave pública dele, assinada por uma autoridade certificadora (CA). Normalmente a cadeia tem três elos: uma CA raiz assina uma intermediária, que assina o certificado do site. O seu sistema operacional ou navegador já vem com uma lista de raízes confiáveis. O cliente confere as assinaturas subindo a cadeia, se as datas valem e se o nome no certificado bate com o host que ele queria.

Extensões úteis na primeira mensagem (ClientHello)

  • SNI (Server Name Indication): o cliente escreve o nome do host na primeira mensagem. O servidor precisa dele para escolher o certificado certo antes de ler qualquer HTTP, então não pode esperar pelo cabeçalho Host. É o virtual hosting, uma camada abaixo.
  • ALPN: o cliente lista os protocolos de aplicação que fala (h2, http/1.1) e o servidor escolhe um. Não custa nenhum round trip extra. Se o servidor não conhece o ALPN, a conexão simplesmente continua em HTTP/1.1.

Quantos round trips?

Aqui contamos depois do handshake do TCP, sem retomada de sessão. O TLS 1.2 precisa de 2 round trips. O TLS 1.3 precisa de só 1: o cliente adivinha o método de chaves e já envia a sua parte (key_share) na primeira mensagem, então o servidor consegue responder tudo de uma vez.

TLS 1.3                           TLS 1.2
 cliente                servidor   cliente                servidor
   |-- ClientHello + chave->|        |-- ClientHello ------->|
   |<-- ServerHello, cert,  |        |<-- ServerHello, cert -|   round trip 1
   |    Finished -----------|        |-- troca de chaves --->|
   |-- Finished + dados --->|        |<-- Finished ----------|   round trip 2
   |   (1 round trip)       |        |-- dados ------------->|
O TLS 1.3 economiza um round trip inteiro.

Forward secrecy e 0-RTT

Imagine que um espião grava a sua conversa criptografada durante meses e depois rouba a chave privada do servidor. Com forward secrecy (sigilo futuro) as conversas antigas continuam seguras, porque cada conexão usou um par Diffie-Hellman novo e descartável, apagado depois, e a chave do certificado só assina. O TLS 1.2 antigo com troca de chaves RSA estática não tinha isso: chave roubada, todas as gravações legíveis. O TLS 1.3 removeu o RSA estático e tornou o forward secrecy obrigatório.

Quando você reconecta, o TLS 1.3 pode usar um segredo guardado (um PSK, vindo de um ticket) e enviar dados já no primeiro voo. É o 0-RTT, ou early data. É mais rápido, mas o servidor não consegue acrescentar um valor novo, então um atacante pode repetir (replay) esse primeiro voo. O early data também não tem forward secrecy. Por isso só deve levar requisições que são seguras de repetir.

↑ Voltar ao sumário

HTTP/2: muitas requisições em uma conexão

O HTTP/1.1 tem um problema de fila: uma conexão leva uma requisição por vez. Para andar mais rápido, os navegadores abrem cerca de seis conexões por origem. Uma página com 200 imagens pequenas precisa então de 34 rodadas de seis, e cada rodada custa um round trip. Com 50 ms de atraso isso soma segundos.

Frames, streams e mensagens

O HTTP/2 mantém o mesmo significado (métodos, códigos de status, cabeçalhos) e muda só como tudo é enviado. Ele é binário, não texto. A menor peça é o frame (quadro), com um cabeçalho fixo de 9 bytes que tem tamanho, tipo, flags e um número de stream. Um stream é um canal lógico de duas vias dentro da conexão. Uma mensagem (uma requisição ou uma resposta) é o grupo de frames de um stream. Frames de streams diferentes são misturados, o que se chama multiplexação.

uma conexão TCP:
 [S1 HEADERS][S3 HEADERS][S1 DATA][S5 HEADERS][S3 DATA][S1 DATA]...
    ^ stream 1     ^ stream 3 ... frames de vários streams intercalados
Muitas requisições dividem uma conexão.
  • Os números de stream têm 31 bits. O cliente usa números ímpares, o servidor usa pares, e o 0 é para a própria conexão. Eles só crescem e nunca são reutilizados.
  • O HTTP/2 não tem request line nem status line. Elas viram pseudo-cabeçalhos que começam com dois-pontos: :method, :scheme, :authority (substitui o Host), :path e :status na resposta.
  • Tipos de frame: HEADERS e DATA levam a mensagem. RST_STREAM cancela um stream (no HTTP/1.1 era preciso fechar a conexão). GOAWAY encerra a conexão. SETTINGS, PING e WINDOW_UPDATE cuidam de configurações, de checar se o outro lado está vivo e do controle de fluxo.
  • Você começa o HTTP/2 sobre TLS usando o ALPN com o identificador h2. O h2c é HTTP/2 sem TLS, usado entre serviços internos, não por navegadores.

HPACK: encolher os cabeçalhos

Os cabeçalhos se repetem muito: o mesmo user-agent e o mesmo cookie vão em toda requisição. O HTTP/2 os comprime com o HPACK. Ele mantém duas tabelas. A tabela estática tem 61 entradas fixas para cabeçalhos muito comuns, como :method: GET. A tabela dinâmica começa vazia e se enche com os cabeçalhos vistos nesta conexão (os mais antigos saem quando ela enche). Os dois lados mantêm a mesma tabela, então um cabeçalho já visto custa só um pequeno número de índice.

Por que não usar simplesmente o gzip, como fazia o SPDY, mais antigo? Porque comprimir um segredo (como um cookie) junto com texto que o atacante controla vaza informação pelo tamanho. É o ataque CRIME. O HPACK nunca procura repetições parciais dentro de um valor: ele troca o cabeçalho inteiro por um índice ou usa um fixo, e pode marcar campos sensíveis como 'nunca indexar'.

Controle de fluxo

O controle de fluxo (flow control) impede que um emissor rápido afogue um receptor lento. Ele vale só para frames DATA e funciona com duas janelas: uma para cada stream e uma para a conexão inteira. As duas começam em 65.535 bytes. Cada byte enviado é subtraído de ambas. O receptor devolve crédito com WINDOW_UPDATE. O emissor pode enviar no máximo o menor dos dois saldos. (Prioridade é outra coisa: só uma dica sobre qual stream atender primeiro.)

janela da conexão: 65535
stream 1 envia 16384 + 16384 + 7232 = 40000 bytes
janela da conexão restante: 25535
janela do stream 3 (nunca usado): 65535
stream 3 pode enviar: min(65535, 25535) = 25535 bytes
O menor saldo vence.

O que não deu certo

  • Server push: o servidor enviava recursos que o cliente ainda não tinha pedido. Mas o servidor não sabe o que o cliente já tem no cache, então muitas vezes desperdiçava dados. Os navegadores o removeram. O substituto é o 103 Early Hints e o rel=preload, em que o navegador decide.
  • Domain sharding (repartir imagens por vários nomes de host, um truque do HTTP/1.1) agora atrapalha: cada novo host custa uma consulta de , um handshake TCP e TLS e um novo slow start, e quebra a única conexão aquecida e a única tabela HPACK.
  • Head-of-line blocking no TCP: o HTTP/2 resolveu a fila na aplicação, mas todos os streams dividem um único fluxo de bytes ordenado do TCP. Se um pacote se perde, o TCP segura tudo o que veio depois, até frames de streams que não perderam nada. Em um link com perdas, uma conexão HTTP/2 pode ser pior que seis conexões HTTP/1.1.
Rodadas necessárias para pedir 200 imagens
HTTP/1.1 (6 conexões)
34 rodadas
HTTP/2 (1 conexão)
1 rodadas
↑ Voltar ao sumário

HTTP/3 e QUIC

Para acabar com o problema da fila única do TCP, a Internet construiu um transporte novo chamado QUIC. Ele roda em cima do UDP, mas o UDP é só um envelope que atravessa a rede que já existe. Todo o resto vive dentro do QUIC: streams, reenvio de dados perdidos, controle de fluxo, e criptografia. Como o QUIC costuma rodar em espaço de usuário (dentro do navegador ou do servidor, não no sistema operacional), ele evolui com uma atualização do aplicativo. O custo: usa mais CPU por pacote que o TCP do kernel. O HTTP/3 é o HTTP levado sobre o QUIC.

CaracterísticaHTTP/1.1HTTP/2HTTP/3
TransporteTCPTCPQUIC sobre UDP
FormatoTextoFrames bináriosFrames binários
Requisições ao mesmo tempo em uma conexão1MuitasMuitas
Compressão de cabeçalhosnenhumaHPACKQPACK
Round trips antes da primeira requisição (TLS 1.3, primeira visita)221
Um pacote perdido atrasaa sua conexãotodos os streamssó o seu próprio stream

Três grandes ganhos

  • Sem head-of-line blocking no transporte. Cada stream do QUIC é ordenado por conta própria. Quando um pacote se perde, só esperam os streams cujos dados ele levava. Os outros continuam chegando.
  • Um handshake mais curto. Com o TCP, primeiro vem o TCP (1 round trip) e depois o TLS 1.3 (1 round trip): 2 no total antes da primeira requisição HTTP. O QUIC leva o handshake do TLS 1.3 dentro dos seus primeiros pacotes: 1. (Contado para uma primeira visita, sem retomada e sem TCP Fast Open. Em uma conexão retomada os dois podem usar 0-RTT, com o risco de replay que você viu na seção de TLS.)
  • Migração de conexão. Uma conexão TCP está amarrada a quatro coisas: dois endereços IP e duas portas. Se o seu celular passa do Wi-Fi para os dados móveis, o IP muda e o TCP se perde. O QUIC usa connection IDs levados nos pacotes, então o servidor confere o novo caminho e segue com as mesmas chaves e os mesmos streams.
TCP + TLS 1.3                  QUIC
 cliente         servidor       cliente         servidor
  |--SYN--------->|             |--Initial+ClientHello->|
  |<--SYN/ACK-----|  1 RTT      |<--Initial+ServerHello-|  1 RTT
  |--ACK+Hello--->|             |--Finished + request-->|
  |<--ServerHello-|  2 RTT      |
  |--Finished+GET>|
Contando os round trips completos antes de a primeira requisição ser enviada.

Não existe QUIC sem criptografia: o handshake do TLS 1.3 faz parte de conectar, e versões anteriores do TLS não são aceitas. O QUIC também criptografa quase toda a sua própria informação de controle (números de pacote, confirmações). O motivo é a ossificação (ossification): quando caixas no meio da Internet se acostumam a ler o formato de um protocolo, elas quebram qualquer mudança, mesmo uma válida. O TCP sofreu com isso porque o seu cabeçalho está às claras. O QUIC deixa visível só o mínimo, como o connection ID, para que a rede não dependa do resto.

QPACK, Alt-Svc e fallback

  • O QPACK substitui o HPACK. A tabela do HPACK é alterada pelos próprios blocos de cabeçalhos, então eles precisam ser decodificados na ordem exata, mas o QUIC só ordena dados dentro de cada stream. O QPACK envia as atualizações da tabela em um stream separado, e cada bloco diz de quais entradas precisa, então o decodificador sabe se deve esperar.
  • Descobrir que o HTTP/3 existe. Um endereço https:// não diz a versão, então o navegador começa pelo TCP. O servidor então anuncia alt-svc: h3=":443"; ma=86400 (protocolo, porta UDP, por quanto tempo vale). Outro caminho é um registro DNS especial do tipo HTTPS. Aí a próxima conexão pode usar QUIC.
  • É só uma dica. Algumas redes bloqueiam o UDP. O navegador tenta o QUIC e, se não funcionar, usa a conexão TCP (HTTP/2 ou HTTP/1.1), geralmente disputando os dois em paralelo para o usuário não esperar um timeout.
Round trips antes da primeira requisição HTTP (primeira visita, convenção do TLS 1.3)
TCP + TLS 1.3 (HTTP/1.1 ou HTTP/2)
2 round trips
QUIC (HTTP/3)
1 round trips

Quando o HTTP/3 é melhor? Quando a latência é alta (o handshake economiza um round trip) ou quando há perda de pacotes (os streams não se bloqueiam), como nas redes móveis. Em redes rápidas e limpas, HTTP/2 e HTTP/3 rendem mais ou menos igual, e o QUIC em espaço de usuário pode até gastar mais CPU.

↑ Voltar ao sumário

REST: uma API feita de coisas

Uma API é um cardápio de coisas que um programa pode pedir a outro programa. REST é um estilo de construir APIs web, descrito por Roy Fielding na sua tese de doutorado (2000). A ideia principal: não pense em ações, pense em coisas (chamadas recursos), como livros, pedidos ou usuários. Toda coisa tem um endereço, uma URL.

A URL é o substantivo, o método HTTP é o verbo.
Quero...RequisiçãoResposta
Ler o livro 42GET /books/42200 OK e o livro
Criar um livroPOST /books201 Created com um cabeçalho Location com a nova URL
Substituir o livro 42PUT /books/42 (o livro inteiro)200 ou 204
Mudar um campoPATCH /books/42 (só as mudanças)200 ou 204
Remover o livro 42DELETE /books/42204 No Content

Não coloque a ação no caminho (/getBook?id=42). Fazer isso joga fora o que navegadores, caches e proxies já sabem sobre os métodos: por exemplo, que o GET é seguro e cacheável.

Stateless (sem estado)

No REST o servidor não guarda memória da conversa com o cliente. Cada requisição leva tudo o que é preciso para ser entendida (credenciais, parâmetros), então qualquer cópia do servidor pode respondê-la. Isso ajuda a escalar com muitos servidores e a se recuperar de falhas. O custo: alguns dados se repetem em cada requisição. Isto fala do estado de sessão; o servidor continua guardando recursos como livros normalmente.

Modelo de maturidade de Richardson

  1. Nível 0: um único endereço usado como túnel para tudo.
  2. Nível 1: recursos separados, cada um com a sua URL.
  3. Nível 2: métodos HTTP e códigos de status de verdade.
  4. Nível 3: controles de hipermídia (HATEOAS): a resposta diz ao cliente o que ele pode fazer em seguida, com links.

Escolhendo o status certo

400 para uma requisição malformada, 404 quando o alvo não existe, 409 quando uma requisição válida bate de frente com o estado atual (por exemplo, um segundo livro com um ISBN que deve ser único) e 422 quando o formato está certo mas o conteúdo quebra uma regra. Códigos precisos deixam os clientes reagirem sem ler o texto do erro.

Repetições: DELETE e Idempotency-Key

Depois de um timeout o cliente não sabe se o servidor terminou o serviço. Repetir um DELETE é seguro: o segundo pode devolver 404, e isso não quebra a idempotência, porque o estado final é o mesmo. O POST não é idempotente, e repeti-lo pode criar dois pedidos. A solução é uma chave de idempotência: o cliente inventa um valor único (Idempotency-Key: abc123) e o repete em cada nova tentativa. O servidor reconhece a repetição e devolve o resultado da primeira execução. Esse cabeçalho é uma convenção comum, não faz parte do HTTP básico.

PUT ou PATCH?

O PUT substitui o estado pela representação que você envia, então você precisa mandar o recurso completo, e ele é idempotente. O PATCH envia só as mudanças, o que economiza bytes e evita sobrescrever campos que outras pessoas mudaram, mas por definição não é idempotente.

Paginação

Listas grandes são devolvidas em páginas. A paginação por offset diz 'pule 3, me dê 3'. Se dois itens novos são adicionados no topo entre duas requisições de página, a janela se desloca e você recebe itens que já tinha visto. A paginação por cursor diz 'me dê 3 itens depois do id 8', então continua a partir do último item visto e fica estável, mas você não consegue pular direto para a página 50.

mais novo primeiro:  12 11 10 9 8 7 6 5 4 3 2 1     (12 e 11 entraram depois da página 1)
 a página 1 foi:  10 9 8
 offset=3 :   pula 12 11 10  -> devolve 9 8 7   (9 e 8 repetidos)
 before=8 :   id < 8         -> devolve 7 6 5   (nada repetido)
Por que cursores são mais estáveis.

Over-fetching e under-fetching

No REST quem decide o formato de cada resposta é o servidor. Se ele dá mais do que a tela precisa, bytes são desperdiçados: over-fetching. Se dá menos, o cliente precisa fazer mais requisições: under-fetching. Exemplo: uma tela com um autor, seus 5 livros e a editora de cada livro (5 diferentes) precisa de 1 + 5 + 5 = 11 requisições com endpoints que devolvem só ids. Soluções: endpoints compostos, um parâmetro expand ou o GraphQL.

↑ Voltar ao sumário

GraphQL: peça exatamente o que você quer

O GraphQL tem um endereço só e um schema tipado, uma lista de todos os tipos e campos que existem. O cliente envia uma query que lista os campos exatos que quer, até atravessando relações, e a resposta tem o mesmo formato. Uma requisição pode substituir as várias requisições do REST.

query {
	book(id: 7) {
		title
		author { name }
	}
}
O cliente escolhe os campos (um selection set). A resposta espelha esse formato.

O schema e os tipos raiz

  • Todo tipo é anulável (nullable) por padrão. Um ! o torna não nulo: title: String! significa que o servidor promete nunca devolver null ali (em um argumento, o cliente é obrigado a informar um valor).
  • Um schema tem até três tipos de operação raiz: Query (ler), Mutation (escrever e depois ler) e Subscription (um fluxo longo de eventos). Só o Query é obrigatório. Os campos deles são as portas de entrada da API.

Resolvers

O servidor executa a query campo por campo. Para cada campo ele chama uma função chamada resolver, passando o objeto pai e os argumentos. O schema descreve o formato dos dados; os resolvers dizem de onde os dados vêm.

Erros e null

Uma resposta é um objeto com data e, se algo falhou, errors. Como uma operação pode ter sucesso em parte, a convenção usual (JSON sobre HTTP) responde `200` mesmo com erros, então o cliente precisa sempre ler errors, não só o código HTTP.

Quando um resolver falha, o seu campo vira null. Se o campo é ! (não nulo), null não é permitido, então o null sobe para o pai, e de novo, até o primeiro campo anulável. Se nenhum é encontrado, o data inteiro vira null. Por isso marcar tudo com ! tem um custo: uma pequena falha pode apagar uma grande parte da resposta.

type User { name: String!  posts: [Post!]! }
type Post { title: String! }

o title do post 2 falha:
  title  é String!    -> o post 2 não pode ser null
  [Post!]!            -> a lista não pode ter item null, então não pode ser null
  User.posts é !      -> posts não pode ser null
  user(id) é anulável -> user vira null

resultado:  { "data": { "user": null }, "errors": [ ... ] }
O null sobe até achar um campo que pode ser null.

Variáveis, aliases e ordem de execução

  • Variáveis ($term) mantêm a estrutura da query separada dos dados. Os valores viajam em um mapa JSON ao lado do documento e são conferidos contra o tipo declarado. Se você colasse o texto dentro da query, um usuário poderia injetar pedaços de query. As variáveis também deixam o mesmo documento ser guardado em cache ou registrado como persisted query.
  • Aliases (first: user(id: 1) e second: user(id: 2)) mudam a chave na resposta. Sem eles, pedir o mesmo campo duas vezes com argumentos diferentes é rejeitado, porque os dois usariam a chave user.
  • Ordem. Os campos de uma query podem rodar em qualquer ordem, até em paralelo, porque não têm efeitos colaterais. Os campos do topo de uma mutation rodam um depois do outro, na ordem em que foram escritos. Isso vale só para o topo; os subcampos rodam normalmente.

O problema N+1

Como cada campo tem o seu resolver, uma lista de N itens com um campo aninhado faz 1 consulta para a lista e depois N consultas para o campo aninhado. Com 20 posts e seus autores, são 1 + 20 = 21 consultas SQL, mesmo que haja só 4 autores diferentes. A solução é um DataLoader: os resolvers pedem uma chave ao loader, o loader reúne todas as chaves do passo atual, remove as repetidas e faz uma consulta só (WHERE id IN (...)). Agora são 1 + 1 = 2. Um loader vive por uma requisição, então nunca vaza linhas para outros clientes.

Comandos SQL para uma lista de 100 livros com autores e avaliações (mini-projeto deste repositório)
Sem batching
201 comandos
Com loader que agrupa
3 comandos

Custo, profundidade e cache

Como o cliente escreve a query, o cliente também decide o quanto ela custa. Um tipo que aponta para si mesmo permite aninhamento sem fim, e o custo se multiplica: quatro níveis de friends(first: 100) pedem 100 x 100 x 100 x 100 = 100^4 = 100 milhões de objetos. Contar requisições não basta, porque uma só requisição pode ser abusiva. Os servidores analisam a profundidade (depth) e uma complexidade estimada do documento e o recusam antes de executar.

O cache é mais difícil que no REST. O cache HTTP foi pensado em torno do GET e de uma URL por recurso, mas o GraphQL típico envia tudo com POST para /graphql, e a diferença está no corpo. Saídas: enviar queries por GET, usar persisted queries identificadas por um hash na URL, ou fazer cache no cliente por tipo e id.

Tamanho da resposta de uma lista no benchmark deste repositório
REST (livros inteiros)
27.6 kB
GraphQL (2 campos)
3 kB
↑ Voltar ao sumário

JSON-RPC e gRPC: chamar uma função distante

Um RPC (Remote Procedure Call, chamada de procedimento remoto) faz chamar outro computador parecer chamar uma função comum. Você não pensa em recursos ou campos: você nomeia uma ação (book.get, sum) e passa parâmetros.

JSON-RPC 2.0

Uma especificação bem pequena. Uma requisição tem jsonrpc: "2.0", method, params opcional e um id. A resposta tem jsonrpc, o mesmo id (para você casar respostas com requisições) e exatamente um entre result e error, nunca os dois. Um error tem um code inteiro, uma message e data opcional.

{ "jsonrpc": "2.0", "method": "book.get", "params": { "id": 7 }, "id": 1 }
{ "jsonrpc": "2.0", "result": { "title": "Example" }, "id": 1 }
Uma requisição e a sua resposta.
Código de erroSignificado
-32700Parse error: o texto não é JSON válido
-32600Invalid Request: é JSON, mas não é um objeto de requisição válido
-32601Method not found: o método não existe
-32602Invalid params: por exemplo "abc" onde se espera um inteiro
-32603Internal error: erro interno
-32000 a -32099Reservados para os erros do próprio servidor. Erros da aplicação usam códigos fora da faixa reservada
  • Notificação (notification): uma requisição sem `id`. O servidor não deve responder nada, nem um erro. Serve para eventos e logs, mas o cliente nunca sabe se falhou.
  • Batch (lote): um array de requisições enviadas juntas. O servidor devolve um array com uma resposta para cada requisição que tem id, em qualquer ordem. Notificações não recebem nada. Um erro em um elemento não afeta os outros. Se o lote só tem notificações, o servidor não devolve nada.
  • Um lote só economiza round trips para chamadas que não dependem umas das outras.
O JSON-RPC é simples e independe do transporte, mas perde o que o HTTP dá de graça, como o cache de GET e o significado dos status.
AssuntoRESTJSON-RPC
Remover o livro 42DELETE /books/42POST /rpc com {"method":"book.delete","params":{"id":42},"id":1}
A ação está nométodo HTTPcorpo (method)
O resultado está nocódigo de statusresult ou error no corpo
TransporteHTTPqualquer um (HTTP, sockets...)

gRPC

O gRPC parte de um contrato escrito em Protocol Buffers (um arquivo .proto). O mesmo arquivo define os serviços, as mensagens e a codificação binária compacta, em que cada campo é identificado por um número em vez de um nome. Um compilador gera código tipado em várias linguagens, então chamar o serviço remoto parece chamar uma função local. O gRPC viaja sobre HTTP/2: cada chamada é um stream (cabeçalhos com o nome do método, mensagens em frames DATA e um status final nos trailers), e a multiplexação deixa muitas chamadas dividirem uma conexão.

message Book {
	string title = 1;
	int32 year = 2;
}
service Library {
	rpc Get (BookId) returns (Book);                 // unary
	rpc Upload (stream Chunk) returns (Summary);     // client streaming
}
Os números 1 e 2 são o que viaja no fio.
Tipo de chamadaMensagensAssinatura
Unary1 requisição, 1 respostarpc A (Req) returns (Res)
Server streaming1 requisição, várias respostasrpc A (Req) returns (stream Res)
Client streamingvárias requisições, 1 respostarpc A (stream Req) returns (Res)
Bidirecionalvárias nos dois sentidos, independentesrpc A (stream Req) returns (stream Res)

Deadlines (prazos). O cliente diz até quando está disposto a esperar. Se o tempo acaba, a chamada termina com DEADLINE_EXCEEDED e o servidor é avisado para parar de trabalhar. Em uma cadeia A, B, C o prazo é repassado como tempo restante. Se A dá 500 ms e B usa 200 ms, C recebe 300 ms. Se C precisa de 400 ms, a chamada termina na marca de 500 ms com DEADLINE_EXCEEDED.

t=0     A chama B (prazo = t 500)
t=200   B chama C  (restam 300 ms)
t=500   o prazo estoura -> DEADLINE_EXCEEDED, C é cancelado
        (C terminaria em t 600)
Ninguém continua trabalhando para quem já parou de esperar.
Round trips para ler um livro com o seu autor (mini-projeto deste repositório)
REST
3 round trips
JSON-RPC
2 round trips
GraphQL
1 round trips
↑ Voltar ao sumário

WebSocket e server-sent events: o servidor fala primeiro

No HTTP puro o cliente sempre pergunta primeiro. Mas um chat, um jogo ou um painel de cotações precisa que o servidor envie novidades por conta própria. Há quatro maneiras de fazer isso.

TécnicaComo funcionaBom para
PollingO cliente pergunta de novo a cada poucos segundosMudanças raras
Long pollingO servidor segura a requisição aberta até ter novidade; uma requisição por mensagemNotificações simples
Server-sent events (SSE)Uma resposta HTTP que nunca termina; mão única, do servidor para o clientePainéis, feeds, barras de progresso
WebSocketUma conexão, duas vias, texto ou binárioChat, jogos, colaboração

WebSocket

Um WebSocket começa como um GET HTTP/1.1 comum que pede um upgrade. Se o servidor concorda, responde 101 Switching Protocols, e daí em diante a mesma conexão TCP fala o protocolo WebSocket. O ws:// roda sobre TCP puro e o wss:// roda dentro do TLS (como http:// e https://).

GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: AAECAwQFBgcICQoLDA0ODw==

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: Bz3qJYTGdOe8gUSpLosEdiLKDrk=
O handshake: a requisição e depois o 101.
  • O Sec-WebSocket-Accept é um da chave do cliente mais um texto público fixo, escrito em . Ele só prova que o servidor entendeu WebSocket e respondeu a esta requisição. Não autentica ninguém e não criptografa nada. Isso é trabalho do TLS e do seu aplicativo.
  • Depois do handshake, os dados andam em frames de texto (UTF-8) ou binários. Existem três frames de controle: ping e pong (um batimento cardíaco: 'você está aí?' / 'sim') e close (iniciar uma despedida ordenada com um código e um motivo).
  • Masking (máscara): todo frame vindo do cliente passa por um XOR com uma chave aleatória de 32 bits enviada no próprio frame. Isso não esconde os dados. Protege proxies defeituosos de serem enganados a tratar os bytes como HTTP (cache poisoning), porque não se confia que o script da página escolha os bytes exatos. Os frames do servidor não são mascarados.

Server-sent events

O SSE é só uma resposta HTTP normal que não termina. O servidor usa Content-Type: text/event-stream e continua escrevendo texto. O navegador lê isso pela API EventSource. Para responder, o cliente faz outras requisições HTTP comuns.

: keep-alive            <- um comentário (começa com dois-pontos)
retry: 5000            <- reconecte depois de 5 segundos

id: 42
event: price
data: 11

Um evento é um bloco de linhas terminado por uma linha vazia.
  • Campos: data (o conteúdo), event (o nome; sem ele o nome é message), id (a posição no fluxo) e retry.
  • Um evento é entregue só quando chega a linha vazia que fecha o seu bloco. Várias linhas data no mesmo bloco viram um valor só, unido com quebras de linha. Um bloco que ainda não fechou não foi entregue.
  • Sem event: o handler onmessage é chamado. Com event: update só são chamados os listeners adicionados com addEventListener('update', ...).
  • Se a conexão cai, o EventSource espera o tempo do retry e pede de novo, enviando `Last-Event-ID` com o último id que viu. O servidor consegue retomar sem perder eventos.
Assista ao vídeo: Hussein Nasser: vídeos sobre HTTP, TLS, WebSocket, gRPC e seus trade-offs (em inglês)↑ Voltar ao sumário

Juntando tudo

Agora dá para ver como as peças se empilham. Quando você abre https://exemplo.test pela primeira vez, o navegador inicia uma conexão TCP, faz um handshake TLS 1.3 (o SNI escolhe o certificado, o ALPN escolhe h2) e então envia requisições HTTP/2. As respostas trazem Cache-Control para que da próxima vez seja mais rápido. O servidor pode acrescentar alt-svc para que mais tarde você use HTTP/3.

NecessidadeEscolhaPor quê
API pública e duradoura, com recursos e cache HTTPRESTFerramentas padrão e cache de GET
Muitas telas, cada uma precisando de uma fatia diferente de dados conectadosGraphQLUma requisição, só os campos desejados. Exige batching e limites de custo
Ações que não são recursos, ou serviços internosJSON-RPCO mais simples de construir, independe do transporte
Chamadas rápidas entre serviços, com contrato rígidogRPCBinário, tipado, streaming e deadlines do HTTP/2
O servidor só empurra atualizaçõesSSEHTTP comum, reconexão automática
Os dois lados enviam com frequênciaWebSocketDuas vias, pouco custo por mensagem
CamadaPergunta que respondeIdeias-chave
Semântica do HTTPO que a mensagem significa?Métodos (seguro, idempotente), códigos de status, cabeçalhos, delimitação
Cache, cookies, CORSO que pode ser reaproveitado, lembrado ou lido?max-age, no-store, revalidação, Vary, SameSite, origem
TLSÉ privado e é o servidor certo?Certificados, acordo de chaves, 1 contra 2 round trips, forward secrecy
HTTP/1.1, 2, 3Como as mensagens vão para o fio?Multiplexação, HPACK/QPACK, QUIC, head-of-line blocking
Estilos de APIO que o cliente nomeia e quem molda a resposta?Recurso, campos, procedimento

Erros comuns

  • Achar que no-cache proíbe guardar (ele quer dizer 'confira primeiro').
  • Misturar 401 (quem é você?) com 403 (você não pode).
  • Acreditar que o CORS protege o servidor (ele protege a leitura da resposta, nos navegadores).
  • Achar que erros do GraphQL usam códigos 4xx ou 5xx (a convenção é 200 com errors).
  • Achar que o HTTP/3 é sempre mais rápido (ele ajuda mais com latência alta ou perdas).
  • Esquecer que o WebSocket precisa de uma checagem de Origin no servidor.
  • Reutilizar um número de campo do Protocol Buffers.
↑ Voltar ao sumário