Como funcionam os WebSockets: Um guia completo sobre conexões em tempo real

Nos primórdios da web, o navegador era um simples visualizador de documentos. Você solicitava uma página, o servidor a renderizava e a conexão era fechada. Esse ciclo de requisição-resposta é o núcleo do HTTP (Hypertext Transfer Protocol).

No entanto, à medida que as aplicações web evoluíram para experiências ricas e interativas — como chat em tempo real, painéis financeiros ao vivo, edição colaborativa e jogos multiplayer —, o modelo HTTP tradicional começou a mostrar suas limitações.

Para obter atualizações ao vivo, os desenvolvedores inicialmente dependiam de soluções alternativas:

  • Short Polling: O navegador envia repetidamente requisições HTTP ao servidor a cada poucos segundos para perguntar por novos dados. Isso cria uma enorme sobrecarga de cabeçalhos e desperdiça recursos do servidor.
  • Long Polling (Comet): O navegador envia uma requisição e o servidor a mantém aberta até que novos dados estejam disponíveis. Assim que os dados são enviados, a conexão é fechada e o navegador abre imediatamente uma nova requisição. Isso é complexo de gerenciar e ainda incorre em uma sobrecarga significativa na configuração da conexão.

Os WebSockets resolveram essas limitações introduzindo um protocolo padronizado para comunicação persistente, bidirecional e full-duplex sobre uma única conexão TCP.


O que é um WebSocket?

Os WebSockets (definidos na RFC 6455) operam em conjunto com o HTTP. Enquanto o HTTP é um protocolo sem estado onde apenas o cliente pode iniciar requisições, uma conexão WebSocket permanece aberta indefinidamente, permitindo que tanto o cliente quanto o servidor enviem dados um ao outro a qualquer momento com latência mínima.

Aqui está a regra fundamental dos WebSockets:

Uma vez estabelecida a conexão, qualquer um dos lados pode enviar mensagens a qualquer momento, sem iniciar uma nova solicitação de conexão.


Passo a Passo: O Ciclo de Vida da Conexão

Uma conexão WebSocket passa por três fases distintas: o Handshake (aperto de mão), a Transferência de Dados e o Fechamento.

WebSocket Connection Lifecycle Diagram

1. O HTTP Handshake (Upgrade do Protocolo)

Como os firewalls e roteadores estão configurados para permitir o tráfego web padrão nas portas 80 (HTTP) e 443 (HTTPS), os WebSockets começam sua jornada como uma requisição HTTP/1.1 padrão. Isso é chamado de Upgrade Handshake.

A Requisição do Cliente

O cliente envia uma requisição HTTP GET com cabeçalhos específicos solicitando a mudança de protocolo:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
  • Upgrade: websocket e Connection: Upgrade: Informam ao servidor que o cliente deseja mudar de protocolo.
  • Sec-WebSocket-Key: Um valor aleatório de 16 bytes codificado em Base64. Ele é usado para provar que o servidor recebeu o aperto de mão e entende o protocolo WebSocket.
  • Sec-WebSocket-Version: Especifica a versão do protocolo WebSocket (geralmente 13).
  • Origin: Usado pelo servidor para decidir se permite a conexão (verificação de segurança contra sites não autorizados).

A Resposta do Servidor

Se o servidor suportar WebSockets, ele valida a requisição e responde com um código de status HTTP 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • Como o Servidor Calcula Sec-WebSocket-Accept:
    1. O servidor pega o Sec-WebSocket-Key do cliente (dGhlIHNhbXBsZSBub25jZQ==).
    2. Ele o concatena com uma string mágica padrão GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
    3. Calcula o hash SHA-1 da string combinada.
    4. Codifica o hash resultante em Base64.
    5. Se o cliente verificar que este valor corresponde às suas expectativas, o handshake é bem-sucedido, a conexão HTTP muda para um socket TCP bruto e ambos os lados passam a usar o protocolo WebSocket.

2. Enquadramento e Transferência de Dados

Ao contrário do HTTP, que envia cabeçalhos em texto simples seguidos por um corpo, os WebSockets transmitem dados em pacotes binários estruturados chamados frames (quadros).

Um frame WebSocket tem um cabeçalho muito leve (variando de 2 a 14 bytes) seguido pela carga útil (payload). Esse cabeçalho contém:

  • Bit FIN (1 bit): Indica se este é o frame final de uma mensagem.
  • Opcode (4 bits): Define o tipo de frame:
    • 0x1: Frame de texto (codificado em UTF-8)
    • 0x2: Frame binário
    • 0x8: Solicitação de fechamento de conexão
    • 0x9: Ping
    • 0xA: Pong
  • Bit Mask (1 bit): Especifica se os dados da carga útil estão mascarados.
  • Payload Length: O tamanho dos dados.
  • Masking Key (4 bytes): Requisito Crucial de Segurança: Todos os frames enviados do cliente para o servidor devem ser mascarados (ofuscados por XOR) usando uma chave aleatória de 4 bytes. Isso impede que caches proxy leiam o tráfego ou executem ataques de envenenamento de cache. Frames de servidor para cliente não devem ser mascarados.

Heartbeats (Ping/Pong)

Para evitar que roteadores e balanceadores de carga fechem conexões ociosas, qualquer um dos lados pode enviar um frame de Ping. O lado receptor deve responder imediatamente com um frame de Pong contendo a mesma carga útil.

3. Fechando a Conexão

Para fechar uma conexão de forma limpa:

  1. Um peer envia um frame de Close contendo um código de status (por exemplo, 1000 para fechamento normal, 1006 para fechamento anormal) e um motivo opcional em texto.
  2. O outro peer responde com seu próprio frame de Close.
  3. O socket TCP subjacente é fechado.

Exemplo de Código: Implementação de WebSockets em Node.js

Para ver os WebSockets em ação, vamos escrever uma aplicação simples em Node.js. Criaremos um servidor WebSocket local que devolve qualquer mensagem que receber, junto com um script cliente para se conectar a ele.

O Servidor WebSocket (server.js)

const { WebSocketServer } = require('ws');
const http = require('http');

// 1. Criar um servidor HTTP padrão
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('HTTP Server running. Use WebSocket to connect.\n');
});

// 2. Acoplar um servidor WebSocket ao servidor HTTP
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
    const clientIp = req.socket.remoteAddress;
    console.log(`[Servidor] Novo cliente conectado de ${clientIp}`);

    // Enviar mensagem de boas-vindas ao cliente
    ws.send(JSON.stringify({ type: 'welcome', message: 'Conectado ao servidor WebSocket da Ghaznix!' }));

    // Ouvir mensagens vindas deste cliente
    ws.on('message', (message) => {
        console.log(`[Servidor] Recebido: ${message}`);
        
        try {
            const data = JSON.parse(message);
            ws.send(JSON.stringify({
                type: 'echo',
                message: `Eco do servidor: ${data.text.toUpperCase()}`,
                timestamp: new Date().toISOString()
            }));
        } catch (e) {
            ws.send(JSON.stringify({ type: 'error', message: 'Formato JSON inválido' }));
        }
    });

    // Tratar desconexão do cliente
    ws.on('close', (code, reason) => {
        console.log(`[Servidor] Cliente desconectado (Código: ${code}, Razão: ${reason.toString() || 'Nenhuma'})`);
    });

    ws.on('error', (error) => {
        console.error(`[Servidor] Erro de socket: ${error.message}`);
    });
});

server.listen(8080, () => {
    console.log('Servidor WebSocket escutando em ws://localhost:8080');
});

O Cliente do Navegador (JavaScript do lado do cliente)

Você pode executar este cliente diretamente no console do seu navegador:

// 1. Estabelecer conexão com o servidor
const socket = new WebSocket('ws://localhost:8080');

// 2. Manipulador de conexão aberta
socket.addEventListener('open', (event) => {
    console.log('[Cliente] Conectado ao servidor.');
    
    const payload = JSON.stringify({ text: 'olá, servidor!' });
    socket.send(payload);
    console.log(`[Cliente] Enviado: ${payload}`);
});

// 3. Ouvir mensagens do servidor
socket.addEventListener('message', (event) => {
    const response = JSON.parse(event.data);
    console.log('[Cliente] Mensagem recebida do servidor:', response);
});

// 4. Ouvir o fechamento da conexão
socket.addEventListener('close', (event) => {
    console.log(`[Cliente] Conexão fechada (Código: ${event.code})`);
});

// 5. Ouvir erros
socket.addEventListener('error', (error) => {
    console.error('[Cliente] Erro no WebSocket:', error);
});

HTTP vs. WebSockets: Uma Comparação Detalhada

Característica HTTP/1.1 WebSockets
Comunicação Unidirecional (iniciada pelo cliente) Bidirecional (cliente ou servidor)
Modelo de Conexão Requisição-Resposta (curta duração) Persistente (longa duração)
Sobrecarga Alta (cabeçalhos enviados a cada requisição) Muito baixa (sobrecarga mínima de enquadramento)
Estado Sem estado Com estado (contexto da conexão é mantido)
Protocolo http:// ou https:// ws:// ou wss://
Ideal para Obtenção de documentos, APIs REST Chats em tempo real, painéis, feeds ao vivo

Considerações de Segurança para WebSockets

Como os WebSockets ignoram o roteamento HTTP padrão após o handshake, eles introduzem vetores de segurança exclusivos:

  1. Use WebSocket Secure (wss://): Sempre execute WebSockets sobre TLS/SSL (porta 443). O WSS criptografa a carga útil do frame, evitando interceptações e espionagem de intermediários.
  2. Validação de Origem: Os WebSockets não são restringidos pela Same-Origin Policy (SOP). Sempre valide o cabeçalho Origin no servidor durante o handshake para impedir acesso não autorizado.
  3. Autenticação no Handshake: Autentique os usuários antes que a conexão seja estabelecida. Isso é comumente feito transmitindo um token (como um JWT) nos parâmetros da consulta ou verificando cookies de sessão.
  4. Sanitização de Entradas: Trate cada mensagem recebida via WebSockets como entrada não confiável. Valide e sanitize as cargas úteis para evitar Cross-Site Scripting (XSS).

Resumo

Os WebSockets transformaram as aplicações web em tempo real ao eliminar a sobrecarga do polling HTTP tradicional. Ao manter uma única conexão TCP persistente, eles permitem a troca instantânea de mensagens bidirecionais, alimentando os modernos painéis ao vivo, jogos multiplayer e aplicativos de chat de hoje. Compreender o upgrade do HTTP, a arquitetura dos frames e as principais práticas de segurança garante que você crie serviços em tempo real rápidos e seguros.


Explore mais tutoriais e guias para desenvolvedores no Blog da Ghaznix →