Net-Base Revista

01.06.2026

Delphi Cliente WebSocket: conectar de forma robusta, encerrar de forma limpa, depurar de forma confiável

Um Delphi WebSocket Client fica „de alguma forma conectado“ rapidamente – mas em operação o que conta são reconexões, heartbeats, encerramento limpo e facilidade de depuração. Com um wrapper adequado para produção baseado em System.Net.WebSockets (com fallback) e um trecho de código‑fonte para gerenciamento de threads e...

01.06.2026

Do tema da revista à prática do projeto

Páginas de serviços e técnicas correspondentes ao artigo

Por que um cliente WebSocket Delphi na prática é mais do que „Connect“

Um Delphi WebSocket Client monta‑se em minutos: URL, Connect, SendText, pronto. Em software empresarial personalizado e em soluções de negócio próximas ao processo, porém, a questão normalmente só aparece em operação: o reverse proxy fecha conexões inativas, enlaces móveis ou VPN têm timeouts NAT curtos, certificados são trocados e, ao encerrar, o processo pode travar porque um Receive-Loop ainda está bloqueado. Além disso: um WebSocket é um canal de longa duração e com estado — aplicam‑se outras regras do que no HTTP/REST clássico (Request/Response, de curta duração).

Neste trecho de código não se trata de „Hello WebSocket“, mas de um Client‑Wrapper adequado para a prática com:

  • start/stop limpos (sem travamentos no shutdown),
  • Receive‑Loop com Cancellation (sinal de cancelamento) em vez de “Thread kill”,
  • Reconnect com Backoff (reativação controlada),
  • heartbeat como padrão de aplicação (porque Ping/Pong nem sempre está disponível),
  • hooks de debug e trace que realmente ajudam em casos de suporte.

A implementação baseia‑se em System.Net.WebSockets (Delphi RTL; API de cliente WebSocket com TClientWebSocket). Onde essa camada RTL não está disponível em versões antigas ou é demasiado restrita, um fallback via biblioteca (p.ex. ICS) costuma ser sensato — há uma contextualização mais abaixo.

Esboço de arquitetura: um Wrapper em vez de chamadas WebSocket dispersas

Um erro frequente em aplicações Delphi já consolidadas: formulários de UI ou módulos de serviço “falam diretamente com o WebSocket” e acabam espalhando timers, threads e tratadores de exceção por todo o lado. É preferível um bloco claro com eventos bem definidos e uma pequena máquina de estados.

Termos brevemente explicados: Backoff designa um tempo de espera que cresce progressivamente após falhas (p.ex. 1s, 2s, 4s …), para não sobrecarregar servidor e rede. CancellationToken é um sinal de cancelamento do mundo .NET; em Delphi não existe um padrão idêntico, mas podemos simulá‑lo com TEvent e uma flag “StopRequested”. TThread.Queue agenda código para execução no thread principal (UI) sem bloquear o worker; Synchronize bloqueia e é frequentemente a causa de deadlocks em caminhos de shutdown.

Trecho de código: Delphi WebSocket Client com Stop, Reconnect e Message-Dispatch

O código a seguir foi conscientemente concebido como um “componente operacional”: uma classe que pode ser usada em VCL/FMX ou em um Windows- e Windows- und Linux-Services (dependendo da versão/plataforma Delphi) de forma similar. O núcleo é um worker‑thread que mantém o Receive‑Loop e reporta à aplicação via Events.

unit Net.WebSocketClientEx;

interface

uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Generics.Collections,
System.Net.URLClient,
System.Net.WebSockets;

type
TWsLogLevel = (llDebug, llInfo, llWarn, llError);

TWsLogEvent = reference to procedure(Level: TWsLogLevel; const Msg: string);
TWsTextEvent = reference to procedure(const Text: string);
TWsStateEvent = reference to procedure(const State: string);

TDelphiWebSocketClient = class
private
FUrl: string;
FOnLog: TWsLogEvent;
FOnText: TWsTextEvent;
FOnState: TWsStateEvent;

FStopEvent: TEvent;
FWorker: TThread;

FMinBackoffMs: Integer;
FMaxBackoffMs: Integer;
FHeartbeatSec: Integer;

procedure Log(Level: TWsLogLevel; const Msg: string);
procedure State(const S: string);

procedure Run;
function NextBackoffMs(const Prev: Integer): Integer;
function NowUtcStr: string;
public
constructor Create(const AUrl: string);
destructor Destroy; override;

procedure Start;
procedure Stop(TimeoutMs: Cardinal = 5000);

property OnLog: TWsLogEvent read FOnLog write FOnLog;
property OnText: TWsTextEvent read FOnText write FOnText;
property OnState: TWsStateEvent read FOnState write FOnState;

property MinBackoffMs: Integer read FMinBackoffMs write FMinBackoffMs;
property MaxBackoffMs: Integer read FMaxBackoffMs write FMaxBackoffMs;
property HeartbeatSec: Integer read FHeartbeatSec write FHeartbeatSec;
end;

implementation

type
TBytesBuffer = record
Data: TBytes;
Len: Integer;
end;

{ TDelphiWebSocketClient }

constructor TDelphiWebSocketClient.Create(const AUrl: string);
begin
inherited Create;
FUrl := AUrl;
FStopEvent := TEvent.Create(nil, True, False, “);

FMinBackoffMs := 500;
FMaxBackoffMs := 15000;
FHeartbeatSec := 20; // App-Heartbeat: útil contra timeouts de inatividade atrás de proxies
end;

destructor TDelphiWebSocketClient.Destroy;
begin
Stop;
FStopEvent.Free;
inherited;
end;

procedure TDelphiWebSocketClient.Start;
begin
if Assigned(FWorker) then
Exit;

FStopEvent.ResetEvent;
FWorker := TThread.CreateAnonymousThread(
procedure
begin
Run;
end);
FWorker.FreeOnTerminate := False;
FWorker.Start;
end;

procedure TDelphiWebSocketClient.Stop(TimeoutMs: Cardinal);
var
W: TThread;
begin
FStopEvent.SetEvent;

W := FWorker;
if Assigned(W) then
begin
if W.WaitFor(TimeoutMs) = wrTimeout then
Log(llWarn, ‚Stop: Worker não respondeu dentro do timeout; possível bloqueio na pilha de rede‘);
FreeAndNil(FWorker);
end;
end;

procedure TDelphiWebSocketClient.Log(Level: TWsLogLevel; const Msg: string);
begin
if Assigned(FOnLog) then
TThread.Queue(nil,
procedure
begin
FOnLog(Level, NowUtcStr + ‚ ‚ + Msg);
end);
end;

procedure TDelphiWebSocketClient.State(const S: string);
begin
if Assigned(FOnState) then
TThread.Queue(nil,
procedure
begin
FOnState(S);
end);
end;

function TDelphiWebSocketClient.NowUtcStr: string;
begin
Result := FormatDateTime(‚yyyy-mm-dd“T“hh:nn:ss.zzz“Z“‚, TTimeZone.Local.ToUniversalTime(Now));
end;

function TDelphiWebSocketClient.NextBackoffMs(const Prev: Integer): Integer;
var
N: Integer;
begin
if Prev <= 0 then Exit(FMinBackoffMs); N := Prev * 2; if N < FMinBackoffMs then N := FMinBackoffMs; if N > FMaxBackoffMs then N := FMaxBackoffMs;
Result := N;
end;

procedure TDelphiWebSocketClient.Run;
var
WS: TClientWebSocket;
Backoff: Integer;
LastHeartbeat: UInt64;
Msg: string;
Buf: TBytes;
Received: TWebSocketReceiveResult;
SB: TStringBuilder;
WaitRes: TWaitResult;

function StopRequested: Boolean;
begin
Result := (FStopEvent.WaitFor(0) = wrSignaled);
end;

procedure SafeClose;
begin
try
if WS.State = TWebSocketState.Open then
WS.Close(TWebSocketCloseStatus.NormalClosure, ‚client shutdown‘);
except
on E: Exception do
Log(llDebug, ‚Close: ‚ + E.ClassName + ‚: ‚ + E.Message);
end;
end;

begin
Backoff := 0;
LastHeartbeat := 0;

while not StopRequested do
begin
WS := TClientWebSocket.Create;
try
State(‚connecting‘);
Log(llInfo, ‚Connect to ‚ + FUrl);

try
// Nota: TClientWebSocket.Connect é síncrono e pode bloquear dependendo de DNS/TLS.
// Por isso isto é executado no worker.
WS.Connect(FUrl);
except
on E: Exception do
begin
State(‚connect_failed‘);
Log(llWarn, ‚Connect failed: ‚ + E.ClassName + ‚: ‚ + E.Message);
Backoff := NextBackoffMs(Backoff);
WaitRes := FStopEvent.WaitFor(Backoff);
if WaitRes = wrSignaled then Break;
Continue;
end;
end;

State(‚open‘);
Backoff := 0;

SetLength(Buf, 16 * 1024);
SB := TStringBuilder.Create;
try
while (WS.State = TWebSocketState.Open) and (not StopRequested) do
begin
// Heartbeat como mensagem de aplicativo, porque Ping/Pong não está exposto de forma consistente em todas as versões do Delphi.
if (FHeartbeatSec > 0) then
begin
if (LastHeartbeat = 0) or (TThread.GetTickCount64 – LastHeartbeat >= UInt64(FHeartbeatSec) * 1000) then
begin
try
WS.Send(‚ping‘);
LastHeartbeat := TThread.GetTickCount64;
Log(llDebug, ‚Heartbeat ping sent‘);
except
on E: Exception do
begin
Log(llWarn, ‚Heartbeat send failed: ‚ + E.Message);
Break;
end;
end;
end;
end;

// Receive: baseado em frames, por isso StringBuilder para fragmentação.
try
Received := WS.Receive(Buf);
except
on E: Exception do
begin
Log(llWarn, ‚Receive failed: ‚ + E.ClassName + ‚: ‚ + E.Message);
Break;
end;
end;

case Received.Kind of
TWebSocketMessageKind.Text:
begin
SB.Append(TEncoding.UTF8.GetString(Buf, 0, Received.BytesReceived));
if Received.EndOfMessage then
begin
Msg := SB.ToString;
SB.Clear;

if Assigned(FOnText) then
TThread.Queue(nil,
procedure
begin
FOnText(Msg);
end);
end;
end;

TWebSocketMessageKind.Binary:
begin
// Em muitos protocolos de negócios, texto/JSON é o padrão.
// Binary pode ser aqui bufferizado de forma semelhante ou repassado diretamente.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

TWebSocketMessageKind.Close:
begin
Log(llInfo, ‚Server requested close‘);
Break;
end;
end;

// Mini-sleep para poupar CPU em loops muito rápidos.
// Não muito grande, caso contrário a latência piora.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Reconnect nach sauberem Close oder nach Fehlern
Backoff := NextBackoffMs(Backoff);
Log(llInfo, ‚Reconnect in ‚ + Backoff.ToString + ‚ ms‘);
WaitRes := FStopEvent.WaitFor(Backoff);
if WaitRes = wrSignaled then
Break;
end;

State(’stopped‘);
Log(llInfo, ‚Worker stopped‘);
end;

end.

O que o código faz deliberadamente „diferente“ em relação a exemplos típicos

  • Stop sem força bruta: Em vez de matar Threads, Stop dispara um Event. O Worker encerra Loops em pontos definidos. Isso reduz travamentos no encerramento e evita vazamentos de recursos na pilha de sockets.
  • Queue em vez de Synchronize: Logs e eventos são enviados via TThread.Queue para o Mainthread. Isso é importante quando Stop/Shutdown vem da UI ou de manipuladores do Service Control. Synchronize pode bloquear se o Mainthread estiver aguardando.
  • Fragmentação considerada: WebSocket-Text pode chegar fragmentado em Frames. Por isso o TStringBuilder e a verificação de EndOfMessage.
  • Heartbeat como protocolo de aplicação: Muitos setups morrem por Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Um texto leve de „ping“ funciona frequentemente como alavanca operacional mais eficaz do que confiar em „TCP keepalive“ ou numa API Ping/Pong que não está disponível em todos os lugares.

Condições e armadilhas em operação

1) DNS, TLS e Proxy: Connect pode bloquear

TClientWebSocket.Connect é síncrono. Dependendo da resolução DNS, do TLS-Handshake, da verificação de certificados ou do ambiente com proxy, isso pode levar vários segundos. O código coloca isso propositalmente em um Worker. Se você precisar de timeouts rígidos adicionais, deve verificar na API se sua versão Delphi fornece opções de timeout, ou encapsular Connect em uma Thread separada e abortar via lógica de processo. Importante: um “abbrechen” aqui geralmente significa “marcar a conexão como quebrada e recriar o Worker”, não “matar imediatamente a operação de socket”.

2) Idle-Timeouts: por que o Heartbeat é frequentemente obrigatório

Em redes corporativas, um WebSocket frequentemente termina atrás de um Reverse Proxy (nginx, IIS ARR) ou de um Load Balancer. Muitas dessas componentes fecham conexões se não houver tráfego por longos períodos. TCP-Keepalive nem sempre pode ser configurado com intervalo curto (e sob Windows costuma ser minutos em vez de segundos). Um Heartbeat em nível de aplicação é, portanto, um workaround estável. Certifique-se de que servidor e cliente compartilhem o mesmo conceito (p. ex. „ping“/„pong“ como texto ou JSON).

3) Threading e UI: eventos devem permanecer desacoplados

Se o processamento de OnText for pesado (JSON-Parsing, acessos a BD com BDE-Ablosung mit nativer Anbindung, atualizações de UI), ele não deve bloquear tudo no Mainthread. O wrapper entrega apenas a mensagem. Um padrão típico é: OnText coloca o payload em uma fila (por exemplo TThreadedQueue<string>), um Worker separado processa com backpressure (ou seja, tamanho limitado da fila). Isso evita que, em picos de carga, a UI trave ou o recebimento entre em colapso.

Depuração: o que você deve registrar quando isso falha „às vezes“

WebSockets são notórios por “funcionar por dias e depois parar”. Sem logs é difícil localizar a causa. Pontos de log sensatos:

  • Carimbo de data/hora (UTC), URL e transições de estado (connecting/open/closed).
  • Close-Reason, se disponível (servidor solicita Close vs. erro de rede).
  • Erros de envio do Heartbeat e exceções no recebimento, incluindo o tipo da Exception.
  • Opcional: tamanhos das mensagens recebidas (não os conteúdos), para detectar explosões de dados.

Se você termina sobre TLS: verifique também se mudanças de certificado (expiração, novo emissor) se correlacionam temporalmente com os erros. Em ambientes hardenizados, boxes de proxy e DPI (Deep Packet Inspection) também são candidatos.

Varianten: wann System.Net.WebSockets é suficiente – e quando não

System.Net.WebSockets é suficiente para muitos casos de integração, sobretudo quando se trata de texto/JSON, carga moderada e estratégias de reconexão claras. Os limites aparecem dependendo da versão de Delphi e do alvo da plataforma:

  • Suporte Ping/Pong ausente/limitado: Nesse caso, o padrão robusto continua sendo o heartbeat da aplicação.
  • Ausência de timeouts/cancelamento em Connect/Receive: Então é preciso projetar a arquitetura de forma que um worker travado permaneça isolado e a aplicação ainda possa encerrar-se corretamente (por exemplo via watchdog de processo ou instâncias de worker separadas).
  • Carga alta ou streams binários: Nesse caso vale a pena um conceito mais robusto de framing/buffering (por exemplo ring buffer, evento binário separado, montador de mensagens com limites).

Para situações legadas (gerações mais antigas de Delphi, requisitos TLS/Proxy muito específicos), bibliotecas como ICS são em alguns projetos mais pragmáticas. O importante não é tanto “qual biblioteca”, mas que você trate Shutdown, Reconnect e Observability (logs/métricas) como temas de primeira classe.

Conclusão: um Delphi WebSocket Client é um componente operacional – com limites claros

Um WebSocket é excelente para eventos push, status em tempo real, mensagens de máquinas ou de processo e como canal de retorno para portais e serviços. O wrapper demonstrado foca nos pontos que frequentemente fazem a diferença em soluções empresariais digitais: reconexão controlada, heartbeat para evitar idle timeouts, processamento de texto seguro contra fragmentação e um caminho de parada que não trava durante deployment ou update.

Limites de aplicação permanecem: se você precisa de garantias rígidas de cancelamento de Connect/Receive em janelas de tempo muito estreitas ou opera com taxas de dados extremamente altas, é necessário aprofundar em timeouts, particularidades da plataforma e, se for o caso, stacks alternativos. Para a maioria dos cenários de integração e modernização, contudo, um cliente bem encapsulado e bem logado como o acima é uma base sólida que pode ser integrada em sistemas Delphi já existentes.

Se você for inserir um componente desses em uma arquitetura existente (p. ex. Layer-3 arquitetura com camadas claras de serviço e UI) ou precisar depurar desconexões esporádicas em condições reais, podemos ajudá-lo a classificar isso de forma direcionada: Entrar em contato.

No contexto técnico, heartbeat e Ping/Pong também desempenham um papel importante quando integrações, fluxos de dados e evolução precisam funcionar de forma coordenada.

Discutir projeto ou iniciativa de modernização com Net-Base.

Próximo passo

Quando um tema se torna um projeto real, arquitetura, sistemas existentes e operação devem ser considerados em conjunto desde o início.

Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.

  • Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
  • REST, o acesso a dados, os portais e o Rollout não são adiados para uma fase posterior.
  • Você vê cedo qual caminho é economicamente e operacionalmente viável.

Partilhar publicação

Compartilhar esta publicação diretamente

LinkedIn, X, XING, Facebook, WhatsApp e e‑mail estão imediatamente disponíveis. Para o Instagram, preparamos o link e um texto curto de imediato.

E-mail

O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.