WebSockets no Horse
July 16, 2026 · View on GitHub
O Horse possui suporte nativo e de alto desempenho a conexões WebSocket bidirecionais (em total conformidade com a especificação RFC 6455). A funcionalidade é integrada diretamente no core do framework, operando sem dependências de pacotes externos, fornecendo uma API fluida para upgrade de conexões, gerenciamento thread-safe de sessões e broadcasts.
🚀 Como Funciona
Ao contrário de abordagens tradicionais que exigem servidores separados ou portas adicionais, o suporte a WebSocket do Horse adota o padrão de Porta Única (Port Sharing). O handshake de upgrade do protocolo ocorre no mesmo socket TCP e porta HTTP principal da aplicação, garantindo facilidade no gerenciamento de firewalls e balanceadores de carga.
Provedores Compatíveis
O suporte a WebSockets está disponível para todos os provedores baseados em sockets físicos controlados pela aplicação:
- Indy (Console, VCL, Daemon)
- IOCP (Windows)
- Epoll (Linux)
- FPC (
fphttpserver)
Restrições e Comportamento Fail-Fast
Alguns provedores, por sua natureza arquitetural, não suportam WebSocket direto:
- HttpSys: O driver de modo kernel (
http.sys) do Windows impede o sequestro do socket bruto, exigindo APIs específicas do SO. - Apache / ISAPI / CGI / FastCGI: Servidores web externos gerenciam o ciclo de vida das conexões, não permitindo manter sockets abertos indefinidamente na thread de execução do Horse.
Para estes casos, o Horse implementa uma política de Fail-Fast. Caso um cliente tente realizar o handshake WebSocket nesses ambientes, a API detecta a ausência do mecanismo de upgrade no provedor, define o status HTTP 501 Not Implemented ou 400 Bad Request de forma transparente no cliente, e interrompe o pipeline de execução imediatamente através da exceção EHorseCallbackInterrupted, evitando falhas silenciosas ou vazamento de recursos.
🛠️ API e Uso Básico
A ativação do WebSocket é feita chamando o método UpgradeToWebSocket no objeto THorseResponse.
uses
Horse,
Horse.Core.WebSocket;
begin
THorse.Get('/ws',
procedure(Req: THorseRequest; Res: THorseResponse)
begin
Res.UpgradeToWebSocket(
procedure(const AConn: IHorseWebSocketConnection)
begin
// Evento de Conexão Estabelecida
Writeln('Conectado: ', AConn.ClientIP);
// Evento de Recebimento de Mensagem
AConn.OnMessage :=
procedure(const AConnection: IHorseWebSocketConnection; const AText: string)
begin
Writeln('Mensagem: ', AText);
// Envia de volta para o mesmo cliente (Echo)
AConnection.SendText('Eco: ' + AText);
end;
// Evento de Desconexão
AConn.OnDisconnect :=
procedure(const AConnection: IHorseWebSocketConnection)
begin
Writeln('Desconectado: ', AConnection.ClientIP);
end;
// Evento de Erro
AConn.OnError :=
procedure(const AConnection: IHorseWebSocketConnection; const AException: Exception)
begin
Writeln('Erro: ', AException.Message);
end;
end);
end);
THorse.Listen(9000);
end.
📡 Gerenciamento de Conexões e Broadcasts
O Horse possui a classe global thread-safe THorseWebSocketManager que gerencia automaticamente as conexões ativas associadas a cada rota.
Broadcast Simples
Para enviar uma mensagem a todos os clientes conectados em uma rota específica:
// Envia para todos na rota /ws
THorseWebSocketManager.Broadcast('/ws', 'Olá a todos!');
Broadcast Excluindo o Remetente
É muito útil em sistemas de chat não ecoar a mensagem de volta para o cliente que a originou:
AConn.OnMessage :=
procedure(const AConnection: IHorseWebSocketConnection; const AText: string)
begin
// Envia a mensagem a todos no canal, exceto para o remetente (AConnection)
THorseWebSocketManager.Broadcast('/ws', AText, AConnection);
end;
🎯 Unicast: Enviando para um Cliente Específico (Mensagem Direta)
O Horse permite enviar mensagens para clientes individuais de duas formas principais:
1. Resposta Direta no Evento OnMessage
Ao receber uma mensagem, o callback do evento OnMessage fornece a referência direta da conexão ativa que disparou o evento (AConnection: IHorseWebSocketConnection). Para responder apenas a esse cliente, basta usar os métodos SendText ou SendBinary dessa instância:
AConn.OnMessage :=
procedure(const AConnection: IHorseWebSocketConnection; const AText: string)
begin
// Responde exclusivamente para o cliente emissor
AConnection.SendText('Recebido: ' + AText);
end;
2. Notificação Direcionada a partir de Eventos Externos (Workaround de Sessão)
Quando é necessário enviar mensagens para um cliente específico a partir de outros contextos da aplicação (como em um endpoint REST /notificar ou em uma thread de background), deve-se implementar um gerenciador de conexões thread-safe.
Você pode criar uma classe gerenciadora utilizando um dicionário sincronizado por uma seção crítica:
unit MyWSManager;
interface
uses
System.SysUtils, System.Generics.Collections, System.SyncObjs,
Horse.Core.WebSocket;
type
TMyWSManager = class
private
class var FSync: TCriticalSection;
class var FClients: TDictionary<string, IHorseWebSocketConnection>;
class constructor Create;
class destructor Destroy;
public
class procedure RegisterClient(const AClientId: string; const AConn: IHorseWebSocketConnection);
class procedure UnregisterClient(const AClientId: string);
class function SendToClient(const AClientId: string; const AMessage: string): Boolean;
end;
implementation
class constructor TMyWSManager.Create;
begin
FSync := TCriticalSection.Create;
FClients := TDictionary<string, IHorseWebSocketConnection>.Create;
end;
class destructor TMyWSManager.Destroy;
begin
FClients.Free;
FSync.Free;
end;
class procedure TMyWSManager.RegisterClient(const AClientId: string; const AConn: IHorseWebSocketConnection);
begin
FSync.Enter;
try
FClients.AddOrSetValue(AClientId, AConn);
finally
FSync.Leave;
end;
end;
class procedure TMyWSManager.UnregisterClient(const AClientId: string);
begin
FSync.Enter;
try
FClients.Remove(AClientId);
finally
FSync.Leave;
end;
end;
class function TMyWSManager.SendToClient(const AClientId: string; const AMessage: string): Boolean;
var
LConn: IHorseWebSocketConnection;
begin
Result := False;
FSync.Enter;
try
if FClients.TryGetValue(AClientId, LConn) then
begin
if LConn.IsConnected then
begin
LConn.SendText(AMessage);
Result := True;
end;
end;
finally
FSync.Leave;
end;
end;
end.
E na definição das rotas do seu servidor Horse:
uses Horse, Horse.Core.WebSocket, MyWSManager;
begin
THorse.Get('/ws',
procedure(Req: THorseRequest; Res: THorseResponse)
var
LClientId: string;
begin
LClientId := Req.Query.Items['clientId']; // Ex: /ws?clientId=ClienteX
if LClientId.IsEmpty then
begin
Res.Send('clientId is required').Status(400);
Exit;
end;
Res.UpgradeToWebSocket(
procedure(const AConn: IHorseWebSocketConnection)
begin
// Registra o cliente e sua conexão WebSocket correspondente
TMyWSManager.RegisterClient(LClientId, AConn);
AConn.OnDisconnect :=
procedure(const AConnection: IHorseWebSocketConnection)
begin
// Remove o cliente da lista ao desconectar
TMyWSManager.UnregisterClient(LClientId);
end;
end);
end);
💓 Heartbeat e Keep-Alive
Para evitar que conexões inativas sejam encerradas prematuramente por proxies, firewalls ou balanceadores de carga, o Horse possui um mecanismo nativo de Heartbeat periódico.
Por padrão, ao fazer o upgrade da conexão, o Horse inicia uma thread de monitoramento em segundo plano que envia frames Ping (Opcode $09) ao cliente em um intervalo determinado (o padrão é 30 segundos). Se o cliente não responder com um frame Pong ou se a conexão cair, a conexão é encerrada localmente liberando a memória imediatamente de forma segura.
O desenvolvedor pode customizar o intervalo de Heartbeat (em segundos) passando o parâmetro correspondente no Upgrader se necessário.
🔒 Thread-Safety e Boas Práticas
- Exclusão Mútua de Escrita: O método
SendText/SendBinaryé thread-safe. A escrita no socket físico é protegida internamente por umTCriticalSection(FSyncWrite), permitindo que múltiplas threads chamemSendconcorrentemente sem risco de corrupção ou interleaving de bytes no socket. - Ciclo de Vida do Request: Não tente reter variáveis de
THorseRequestouTHorseResponsedentro das closures do WebSocket. O ciclo de vida da requisição HTTP é encerrado no momento em queUpgradeToWebSocketfaz o upgrade da conexão. A partir desse momento, utilize apenas a interfaceIHorseWebSocketConnection. - Exceções em Callbacks: Sempre trate exceções dentro dos eventos do WebSocket (
OnMessage, etc.). Falhas não tratadas dentro desses callbacks podem forçar o encerramento abrupto do socket do cliente correspondente.