Evolução de um Servidor WebSocket para Padrão Profissional
Reestruturação completa de um servidor WebSocket server-side em Delphi: de uma implementação simples e limitada para uma biblioteca modular, segura, com 61 testes automatizados e totalmente compatível com a RFC 6455 — parsing byte a byte, sem modo "automático" de biblioteca nenhuma.
O desafio
A versão original (V1) funcionava apenas em condições perfeitas: o handshake aceitava praticamente qualquer requisição, a leitura de frames só entendia um caso específico do protocolo, e não havia validação de UTF-8, suporte a fragmentação nem tratamento de PING/PONG/CLOSE. Faltavam requisitos essenciais de produção:
- Segurança
- Conformidade com protocolos formais
- Robustez em produção
- Observabilidade (logs e telemetria)
- Compatibilidade com dispositivos reais
A arquitetura da solução
A V1 vivia inteira dentro de uma única unit. A versão atual é uma biblioteca modular de 5 units, cada uma com responsabilidade única — infraestrutura pura, agnóstica de negócio:
WebSocket/
├── WebSocketServer.pas Servidor (sobre Indy TCP) + API pública Ler/EnviarWS*
├── WebSocketHandshake.pas Upgrade HTTP fortificado + telemetria
├── WebSocketFrames.pas Parser/writer de frames — TEXT/BINARY/PING/PONG/CLOSE
├── WebSocketProtocol.pas Constantes da RFC (opcodes, close codes, limites) + validadores
└── UTF8Automato.pas DFA de Höhrmann para validação UTF-8 (RFC 3629)
Cada cliente é atendido em sua própria thread; timeout, keepalive e desconexão silenciosa devolvem o controle
ao consumidor sem exceção — exceção significa protocolo violado, e nesse caso a lib já enviou
o frame CLOSE com o status code correto antes de lançá-la. Quem consome a lib não sabe o que é um frame:
só chama LerWSString / EnviarWSString.
O que foi feito
Handshake fortificado
Leitura fragmentada com limites rígidos (4 KB · 2 s · 20 fragmentos · 50 headers), headers proibidos bloqueados, chave validada byte a byte, resposta 426 para versão errada e negociação de subprotocolo/extensão conforme a §4.2.2.
Parser completo de frames
Validação de FIN, RSV e opcode, máscara obrigatória, comprimentos 7/16/64 bits com codificação mínima exigida, limite de 2 MB — e cada violação fecha a conexão com o status code correto da RFC (1002, 1007, 1009).
Protocolo integral
PONG automático ecoando o payload do PING — inclusive no meio de mensagem fragmentada. CLOSE validado (código e reason) e ecoado. Fragmentação suportada na leitura, com continuation e control frames intercalados.
UTF-8 por autômato (DFA)
Validador próprio com o DFA de Bjoern Höhrmann: O(n), 2 indexações de tabela por byte, zero branches, tabela de 400 bytes. Rejeita overlong, surrogates, codepoints > U+10FFFF e truncamentos — CLOSE 1007.
Observabilidade profissional
Logs estruturados por estágio ([SYS/WS-HS], [RX/WS-FRAME], [TX/WS-PONG]…) e telemetria do handshake — tempo até o 1º byte, duração, bytes e fragmentos, base para detectar ataques slowloris.
61 testes automatizados
13 testes unitários do autômato UTF-8 e 48 de integração em loopback real: um cliente TCP cru monta handshakes e frames byte a byte, e cada violação é conferida lendo o CLOSE devolvido no socket. O Sec-WebSocket-Accept é validado contra o vetor da própria RFC (§1.3).
Antes × Depois
| Aspecto | Antes (V1) | Depois (V2) |
|---|---|---|
| Arquitetura | tudo em uma unit | 5 units modulares |
| Segurança | inexistente | múltiplas camadas + limites |
| RFC 6455 | parcial | completa |
| Handshake | básico | validado, protegido, com telemetria |
| Frames | interpretação errada | parser completo (7/16/64 bits) |
| Fragmentação | não suportada | continuation + control intercalado |
| PING/PONG/CLOSE | não suportado | implementado com eco |
| UTF-8 | sem validação | DFA de Höhrmann (1007) |
| Logs | nenhum | estruturados por estágio |
| Telemetria | inexistente | detalhada (anti-slowloris) |
| Testes | nenhum | 61 (unitários + loopback) |
| Robustez | baixa | alta |
Código: a evolução na prática
Trechos que demonstram a evolução técnica da V1 para a versão profissional. Os exemplos V2 são código real da lib em produção.
V1 Handshake simplificado
if ParsedHeaders.ContainsKey('Upgrade') and (ParsedHeaders['Upgrade'] = 'websocket') then
begin
SecWebSocketKey := ParsedHeaders['Sec-WebSocket-Key'];
Hash := TIdEncoderMIME.EncodeBytes(
HashSHA1.HashString(SecWebSocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'));
c.Write('HTTP/1.1 101 Switching Protocols'#13#10 +
'Upgrade: websocket'#13#10 +
'Connection: Upgrade'#13#10 +
'Sec-WebSocket-Accept: ' + Hash + #13#10#13#10);
c.Tag := 1;
end;
V2 Handshake fortificado com telemetria
// Leitura fragmentada com limites rígidos: 4 KB, 2 s, 20 fragmentos
if ManipuladorIO.InputBuffer.Size > 0 then begin
Inc(QtdeFragmentos);
if QtdeFragmentos > QTDE_MAX_FRAGMENTOS then
raise TInnerWSException.CreateComInfo('Fragmentação excessiva detectada no handshake', 'Handshake');
...
end;
if ConstrutorRaw.Length > TAM_MAX_HANDSHAKE then
raise TInnerWSException.CreateComInfo('Handshake maior que limite seguro (4KB). Possível ataque.', 'Handshake');
// Validação estrita da request line
if not SameText(Metodo, 'GET') then
raise TInnerWSException.CreateComInfo('Método inválido no handshake (esperado GET)', 'Handshake');
if Pos(#0, CaminhoReq) > 0 then
raise TInnerWSException.CreateComInfo('Caracter NUL no path', 'Handshake');
// Telemetria: base para detectar slowloris e fragmentação agressiva
TLogging.Escrever(Format('[SYS/WS-HS-TM] Telemetria: PrimeiroByte=%d ms | Total=%d ms | Bytes=%d | Fragmentos=%d',
[DuracaoPrimeiroByte, DuracaoTotal, TotalBytesLidos, QtdeFragmentos]), nlInfo, '0', 'WS');
V1 Leitura de frames (parcial e incorreta)
if ReadByte = $81 then
begin
l := ReadByte;
DecodedSize := l - 128;
Mask[0] := ReadByte;
Mask[1] := ReadByte;
Mask[2] := ReadByte;
Mask[3] := ReadByte;
inherited ReadBytes(TIdBytes(result), DecodedSize, False);
end;
V2 Decoder completo e seguro
Byte1 := ManipuladorIO.ReadByte;
Fin := (Byte1 and $80) <> 0;
// RSV bits devem ser 0 — extensões não suportadas (§5.2)
if (Byte1 and $70) <> 0 then
FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Extensões WebSocket não suportadas');
CodigoOp := Byte1 and $0F;
Byte2 := ManipuladorIO.ReadByte;
// Cliente DEVE mascarar (§5.1)
if (Byte2 and $80) = 0 then
FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Frame sem máscara (cliente deve mascarar)');
TamBase := Byte2 and $7F;
case TamBase of
126: begin
BytesExt := LerNBytes(ManipuladorIO, 2);
TamPayload := (UInt64(BytesExt[0]) shl 8) or UInt64(BytesExt[1]);
// RFC exige codificação mínima: length < 126 não pode usar formato estendido
if TamPayload < 126 then
FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Payload length com codificação estendida não mínima');
end;
127: begin
BytesExt := LerNBytes(ManipuladorIO, 8);
if (BytesExt[0] and $80) <> 0 then
FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Payload length de 64 bits inválido');
for J := 0 to 7 do
TamPayload := (TamPayload shl 8) or UInt64(BytesExt[J]);
if TamPayload < 65536 then
FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Payload length com codificação estendida não mínima');
end;
else
TamPayload := TamBase;
end;
if TamPayload > WS_TAM_MAX_FRAME then
FalharWebSocket(WS_CLOSE_MENSAGEM_GRANDE, 'Frame maior que limite permitido');
// Control frames: nunca fragmentados, payload ≤ 125 bytes (§5.5)
if CodigoOp in [WS_OPCODE_CLOSE, WS_OPCODE_PING, WS_OPCODE_PONG] then begin
if not Fin then FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Control frame fragmentado é inválido');
if TamPayload > WS_TAM_MAX_PING then FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Control frame maior que limite (RFC: 125 bytes)');
end;
V2 Tratamento profissional de PING/PONG
if CodigoOp = WS_OPCODE_PING then begin
TLogging.Escrever('[RX/WS-PING] PING recebido', nlInfo, '0', 'WS');
Dados := LerPayloadDesmascarado(TamPayload);
ManipuladorIO.Write(Byte($8A)); // opcode PONG (§5.5.3)
ManipuladorIO.Write(Byte(TamPayload));
if TamPayload > 0 then ManipuladorIO.Write(Dados); // eco do payload
// PING no meio de mensagem fragmentada? A leitura continua de onde parou
if MensagemFragmentada then Continue;
Exit;
end;
V2 Validação UTF-8 sem branches (DFA de Höhrmann)
// 2 indexações de tabela por byte. Sem if/else. O(n).
function TUTF8Automato.PushByte(B: Byte): Boolean;
var
TypeVal: Cardinal;
begin
TypeVal := UTF8d[B]; // classifica o byte
FState := UTF8d[256 + FState * 16 + TypeVal]; // transição da DFA
Result := (FState <> UTF8_REJECT);
end;
// Uso no pipeline: frame TEXT que termina "pendente" é truncamento → CLOSE 1007
UTF8.Reset;
if not (UTF8.PushBuffer(Dados) and UTF8.IsValidFinal) then
FalharWebSocket(WS_CLOSE_UTF8_INVALIDO, 'Payload UTF-8 inválido ou truncado');
Números do projeto
| Métrica | Valor |
|---|---|
| Units modulares | 5 — server, handshake, frames, protocolo, UTF-8 |
| Testes automatizados | 61 — 13 unitários + 48 integração em loopback |
| Close codes implementados | 1002 · 1003 · 1007 · 1009 (+ faixa validada da §7.4) |
| Limite de frame/mensagem | 2 MB, com codificação mínima obrigatória |
| Validação UTF-8 | O(n), tabela de 400 bytes, zero branches |
| Proteções de handshake | 4 KB · 2 s · 20 fragmentos · 50 headers · headers proibidos |
Resultado
O sistema evoluiu de um protótipo que "funcionava quando dava" para uma biblioteca de infraestrutura de nível profissional, que hoje sustenta a comunicação com dispositivos embarcados reais em produção — incluindo terminais de reconhecimento facial que se conectam via WebSocket. Essa evolução demonstra:
- Proficiência em protocolos de rede e programação de baixo nível
- Conhecimento profundo da RFC 6455 (e RFC 3629 / algoritmo de Höhrmann)
- Capacidade de engenharia e arquitetura — modularização, responsabilidade única
- Atenção a segurança, escalabilidade e estabilidade
- Cultura de testes: cada regra do protocolo verificada fim-a-fim no socket
- Escrita de código limpo, modular e de fácil manutenção
Quer conversar sobre os detalhes?
Caso tenha interesse em entender o código completo ou detalhes técnicos adicionais, estou disponível para conversar sobre o projeto.
jhonathan93.dev@gmail.com