Tras muchos años respondiendo tickets de soporte, el mismo puñado de problemas representa la inmensa mayoría de los reportes "mi conexión WebSocket se está comportando raro". Ninguno son bugs en la librería — todos son errores de configuración que llevan cinco minutos arreglar una vez que sabes que existen. Este post lista los diez más comunes, con el síntoma, la causa y la línea que resuelve cada uno.
Si sólo lees un error de esta lista, que sea el #2. Bloquear el evento OnMessage es responsable de más tickets de "la librería es lenta" que el resto de categorías juntas, y es invisible hasta que escalas más allá de unos pocos cientos de clientes concurrentes. Ahórrate la sesión de depuración nocturna en producción y arréglalo antes de lanzar.
Error 1: WatchDog de reconexión desactivado
Síntoma: el cliente conecta al arrancar, funciona bien durante horas y luego deja silenciosamente de recibir mensajes tras un bache de Wi-Fi, una reconexión de VPN o un reinicio del servidor.
Causa: la propiedad WatchDog está apagada por defecto. La librería no asume que quieras reconexiones automáticas — algunas apps quieren comportamiento fail-fast. La mayoría no.
oClient.WatchDog.Enabled := True; oClient.WatchDog.Interval := 10; // try every 10 seconds oClient.WatchDog.Attempts := 0; // 0 = unlimited
Fire-and-forget. Combínalo con logging de OnDisconnect para ver en campo con qué frecuencia disparan las reconexiones. Para despliegues móviles o portátiles donde el usuario roamea entre redes, conecta también la notificación de cambio de red del SO (SystemEvents.NetworkAvailabilityChanged en .NET, la ruta WMI en Windows-Delphi) para disparar una reconexión inmediata en lugar de esperar al siguiente tick de intervalo.
Error 2: bloquear el evento OnMessage
Síntoma: el throughput se desploma bajo carga. El servidor se siente "atascado". La CPU está baja, pero los mensajes se acumulan.
Causa: OnMessage corre en el hilo worker de I/O. Si llamas a una BD lenta, a una API HTTP externa o a un parser de larga ejecución dentro, matas de hambre a cada otra conexión que comparta ese worker.
// BAD: blocks the worker
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oDb.Query('INSERT INTO events VALUES(?)', [Text]); // 50 ms
oHttp.Get('https://billing/track?event=' + Text); // 200 ms
end;
// GOOD: hand off to a worker queue
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oWorkQueue.Push(TWorkItem.Create(Connection.Guid, Text));
end;
Error 3: ignorar HeartBeat
Síntoma: las conexiones fantasma se acumulan. Connections.Count sigue creciendo aunque sepas que los clientes han muerto.
Causa: sin heartbeats a nivel de aplicación, el temporizador TCP keepalive del SO dispara tras unas 2 horas. Para entonces tienes decenas de miles de sockets zombi comiendo memoria.
oServer.HeartBeat.Enabled := True; oServer.HeartBeat.Interval := 30; oServer.HeartBeat.Timeout := 90;
Error 4: versiones TLS desajustadas
Síntoma: el cliente obtiene "handshake failed" o "SSL_ERROR_SYSCALL". La conexión funciona en una caja de dev pero falla en producción detrás de un proxy corporativo.
Causa: las opciones SSL del cliente tienen por defecto TLS 1.0–1.2 por compatibilidad hacia atrás; los servidores modernos requieren sólo TLS 1.2/1.3. O las DLLs OpenSSL están antiguas.
oClient.TLSOptions.Version := tlsTLSv1_2; // or tlsTLSv1_3 oClient.TLSOptions.OpenSSL_Options.LibPath := ExtractFilePath(ParamStr(0)); // Bundle the latest libcrypto-3.dll / libssl-3.dll with your installer.
Error 5: no manejar mensajes fragmentados
Síntoma: el servidor recibe la primera parte de un documento JSON grande, tu parser falla y la conexión se cierra.
Causa: por defecto, sgcWebSockets reensambla los fragmentos por ti y sólo dispara OnMessage con el payload completo. Pero si has fijado ReadOptions.FragmentMode := frgPartial por razones de memoria, debes reensamblar.
// Default (recommended for most apps)
oServer.ReadOptions.FragmentMode := frgComplete;
// If you opted into partial delivery, reassemble manually
procedure TForm1.OnFragment(Connection: TsgcWSConnection;
const aPartial: TBytes; aIsFinal: Boolean);
begin
Buffers[Connection.Guid].Append(aPartial);
if aIsFinal then
Process(Buffers[Connection.Guid].ToBytes);
end;
Error 6: usar API síncrona en el hilo GUI
Síntoma: la UI se congela durante varios segundos al conectar o al llamar a WriteData en un enlace lento.
Causa: llamadas bloqueantes en el hilo principal. Usa siempre el patrón async en apps VCL/FMX o llama desde un hilo worker.
// BAD: blocks the UI
oClient.Active := True;
// GOOD: connect asynchronously
TTask.Run(procedure
begin
oClient.Active := True;
end);
// Or use the event-driven API
oClient.OnConnect := DoConnected;
oClient.Connect; // returns immediately
Error 7: olvidar seleccionar un sub-protocolo
Síntoma: la conexión tiene éxito pero el par rechaza cada frame, o responde con un formato distinto al esperado.
Causa: muchos servidores WebSocket (MQTT-sobre-WS, STOMP, GraphQL-WS, Phoenix) requieren un sub-protocolo específico en el handshake. Sin él, el servidor cae a un protocolo distinto o te tira.
// MQTT over WebSocket
oClient.Specifications.Hixie76 := False;
oClient.Specifications.RFC6455 := True;
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: mqttv3.1');
// GraphQL over WS
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: graphql-ws');
Error 8: tamaños de buffer demasiado pequeños (o demasiado grandes)
Síntoma: alta CPU en un servidor empujando muchos mensajes pequeños, o out-of-memory en un servidor empujando grandes.
Causa: los buffers send/receive por defecto están dimensionados para uso general. Ajústalos a la forma de tu tráfico.
// Small messages (chat, telemetry): smaller buffers reduce per-conn memory oServer.IOHandler.RecvBufferSize := 4096; oServer.IOHandler.SendBufferSize := 4096; // Big messages (file transfer, media): larger buffers reduce syscalls oServer.IOHandler.RecvBufferSize := 65536; oServer.IOHandler.SendBufferSize := 65536;
Error 9: sin comprobación de Origin en servidores públicos
Síntoma: la auditoría de seguridad marca "cualquier sitio web puede conectar a tu WebSocket vía el navegador de un usuario".
Causa: el protocolo WebSocket no fuerza same-origin. Tu servidor debe validar la cabecera Origin.
procedure TForm1.ServerBeforeConnect(Connection: TsgcWSConnection;
var Continue: Boolean);
var
vOrigin: string;
begin
vOrigin := Connection.Headers.Values['Origin'];
Continue := (vOrigin = 'https://app.example.com')
or (vOrigin = 'https://admin.example.com');
end;
Error 10: loguear datos sensibles en OnMessage
Síntoma: el auditor encuentra API keys, JWTs o PII en archivos de log. Problema de cumplimiento.
Causa: el fácil LogMemo.Lines.Add(Text) en OnMessage escribe cada payload a disco para siempre.
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
// Hash / redact before logging
LogMemo.Lines.Add(Format('[%s] %d bytes (sha=%s)',
[Connection.Guid, Length(Text), ShortHash(Text)]));
Process(Text);
end;
Bonus: no leer el History.txt
Cada release distribuye un history.txt que lista cada cambio, fix y nota de breaking desde 2013. Cinco minutos leyéndolo por encima tras cada upgrade te ahorra horas de "por qué dejó de funcionar esto" después.
Bonus 2: mezclar versiones de componentes entre proyectos
Los desarrolladores Delphi a veces copian un único .pas de una release sgcWebSockets más nueva a un proyecto más antiguo, "sólo este archivo". Esto funciona hasta que no funciona — el archivo depende de tipos que cambiaron dos releases atrás, y el linker falla misteriosamente o, peor, enlaza pero crashea en runtime. Actualiza siempre toda la librería junta. Los 30 segundos ahorrados copiando un archivo no valen las cuatro horas de depuración cuando algo aguas abajo se rompe.
Bonus 3: tratar WebSocket como fire-and-forget
WebSocket no es una cola de mensajes. Es un stream de bytes bidireccional sobre TCP. Si la red cae a mitad de mensaje, el frame se pierde y nunca se reentrega automáticamente. Para mensajes críticos de negocio debes añadir tu propio protocolo de acknowledgement encima — normalmente un UUID por mensaje, un ACK explícito del receptor y un reenvío del emisor tras un timeout. Saltarse esta capa está bien para notificaciones "el usuario está escribiendo" y es fatal para "el usuario pagó el carrito".
Bonus 4: dejar que los memos sangren memoria
Síntoma: el memo de diagnóstico de tu form de debug es lo primero que dispara OOM en tu cliente tras unas horas de tráfico. Culpas a sgcWebSockets; sgcWebSockets es inocente.
Causa: un TMemo retiene cada línea jamás añadida. A 100 líneas por segundo eso son 360.000 líneas por hora. Cada línea asigna un string. La VCL renderiza miles de líneas invisibles en cada WM_PAINT. Tu proceso se ralentiza hasta parar mientras la librería no está haciendo nada mal.
// Cap the diagnostic memo
procedure TForm1.LogLine(const aText: string);
const
cMaxLines = 500;
begin
TThread.Queue(nil, procedure
begin
while Memo1.Lines.Count > cMaxLines do
Memo1.Lines.Delete(0);
Memo1.Lines.Add(aText);
end);
end;
Aún mejor: loguea a un archivo rotativo vía LoggerPro y sólo espeja las últimas 200 líneas al memo para depuración visual. El código de producción nunca debería escribir a un control UI desde un hilo de red.
Bonus 5: no cerrar el servidor antes de Application.Terminate
Síntoma: al cerrar la app el proceso se cuelga durante 30 segundos, o el SO reporta excepciones no manejadas en los logs del cliente porque las conexiones se cayeron sin gracia.
Causa: el destructor del servidor envía un close frame a cada conexión y espera a que el SO libere el puerto de escucha. Si llamas a Application.Terminate antes de oServer.Active := False, las conexiones mueren a mitad de handshake y el puerto del SO se queda en TIME_WAIT, bloqueando un reinicio rápido.
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if oServer.Active then
begin
oServer.Broadcast('{"event":"shutdown","reconnect_after":5}');
Sleep(200); // let frames flush
oServer.Active := False; // graceful close
end;
end;
Para servidores de consola, engancha SetConsoleCtrlHandler en Windows o SIGTERM en Linux y corre la misma secuencia de apagado. Empareja esto con un bucle HUP/reinicio en tu gestor de servicios y tienes despliegues con cero conexiones caídas.
El patrón detrás de los patrones
La mayoría de estos errores tienen una raíz común: asumir que la red es fiable. No lo es. Las conexiones TCP medio abiertas pasan. Las redes móviles caen. Los proxies corporativos rompen TLS. El Wi-Fi roamea. Los servidores reinician. Los load balancers de la nube terminan conexiones inactivas tras 60 segundos. Una app WebSocket que no sobrevive a estas condiciones no está terminada — es una demo del happy path. La buena noticia: la librería expone un control para cada uno de estos escenarios. La mala: la mayoría están apagados por defecto por compatibilidad hacia atrás. Dos horas leyendo las páginas de opciones WatchDog, HeartBeat, Reconnect y TLS de la documentación es el seguro más barato que comprarás jamás.
Patrón de segundo orden: respeta el hilo de I/O. Cualquier cosa que tarde más de un milisegundo — consulta a BD, I/O de archivo, llamada HTTP externa, regex sobre un string largo, parse JSON sobre un payload de 100 KB — pertenece a un hilo worker, no a OnMessage. Haz esta regla absoluta. Los desarrolladores junior la violarán dentro de tres meses; una checklist de code-review que incluya "no bloquear en OnMessage" lo captura antes de que llegue a producción.
Por dónde seguir
Si estás ajustando un servidor de alto tráfico, lee a continuación Ajuste de rendimiento de sgcWebSockets. ¿Nuevo en la librería? Empieza en el hub de Primeros Pasos y conecta tu primer WebSocket en cinco minutos.