Connection health
A WebSocket connection is a long-lived TCP connection, and TCP does not tell a client when the other side has disappeared without saying goodbye: a server that crashes, a dropped Wi-Fi link, or a proxy that silently discards idle connections all leave the client believing it is still connected. The WebSocket protocol answers this with ping and pong control frames. This chapter covers how the client answers the server's pings, how to take over that reply yourself, and how to build a heartbeat that detects a dead connection and reconnects. Use it for any client that must stay connected for hours, runs on a mobile network, or must react quickly when the server goes away.
Answering the server's pings
Servers send ping frames to check that a client is still there, and the client
answers them automatically. When a ping arrives, the client sends back a pong
with the same payload and then raises
OnPing with that payload in
AData. You do not have to do anything to stay responsive.
Replying to pings manually
Take over the reply when you want to decide whether and when to answer — for
example to count pings, to delay the answer, or to stop answering while the
application is paused. Add twsoManualPong to
Options before connecting;
the client then no longer answers pings itself, and OnPing is where you reply
with Pong, echoing the ping's
payload. Pong sends a masked control frame, as the protocol requires for
frames a client sends.
procedure TForm1.FormCreate(Sender: TObject);
begin
// Set before connecting: the client no longer replies to pings by itself.
TMSFNCWebsocketClient1.Options := TMSFNCWebsocketClient1.Options + [twsoManualPong];
end;
procedure TForm1.TMSFNCWebsocketClient1Ping(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AData: TBytes);
begin
Inc(FPingCount);
// A pong must echo the payload of the ping it answers. Pong sends a masked
// control frame, as RFC 6455 requires for client-to-server frames.
if not FPaused then
TMSFNCWebsocketClient1.Pong(AData);
end;
procedure TForm1.TMSFNCWebsocketClient1Pong(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AData: TBytes);
begin
// Fires when the server answers one of our own pings.
FLastPong := Now;
end;
Pinging the server
Send your own ping with Ping,
passing an optional message (or TBytes) as payload. A healthy server answers
with a pong, which raises
OnPong with the echoed payload.
A missing pong is the most reliable sign that the connection is gone.
Detecting a lost connection
OnDisconnect fires when
the connection is closed in an orderly way — by Disconnect, by Active := False, or by a close frame from the server. It does not fire when the
connection is lost abruptly, because nothing arrives to report the loss. Two
signals reveal that case:
- A failing send. Writing to a connection whose peer is gone raises an
exception, so every
Send,SendMasked,Ping, andPongbelongs in atry..exceptblock. - A missing pong. Ping periodically and treat an unanswered ping as a lost connection.
When you detect a loss, release the socket with Disconnect(False) — a close
handshake cannot complete over a dead link — and connect again.
procedure TForm1.SendStatus(const AStatus: string);
begin
try
TMSFNCWebsocketClient1.Send(AStatus);
except
on E: Exception do
begin
// The peer is gone; OnDisconnect was not raised for this loss.
TMSFNCWebsocketClient1.Disconnect(False);
StatusLabel.Text := 'Connection lost: ' + E.Message;
ReconnectTimer.Enabled := True;
end;
end;
end;
Combining a ping heartbeat with reconnecting
The example combines both signals into a heartbeat timer: every tick pings the server, a tick that finds the previous ping unanswered or a ping that raises marks the connection as lost, and the next tick reconnects. A timer interval of 10 to 30 seconds suits most applications; choose it shorter than the idle timeout of any proxy between client and server.
procedure TForm1.HeartbeatTimerTimer(Sender: TObject);
begin
if not TMSFNCWebsocketClient1.Active then
begin
Reconnect;
Exit;
end;
// No pong since the previous ping: the connection is gone even though
// no OnDisconnect was raised.
if FAwaitingPong then
begin
ConnectionLost;
Exit;
end;
try
FAwaitingPong := True;
TMSFNCWebsocketClient1.Ping('heartbeat');
except
// Writing to a closed socket raises: the connection is gone.
ConnectionLost;
end;
end;
procedure TForm1.TMSFNCWebsocketClient1Pong(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AData: TBytes);
begin
FAwaitingPong := False;
end;
procedure TForm1.ConnectionLost;
begin
StatusLabel.Text := 'Connection lost';
// Do not attempt a close handshake over a dead link.
TMSFNCWebsocketClient1.Disconnect(False);
FAwaitingPong := False;
end;
procedure TForm1.Reconnect;
begin
try
TMSFNCWebsocketClient1.Connect;
if TMSFNCWebsocketClient1.Active then
StatusLabel.Text := 'Connected';
except
// Server still unreachable; the next timer tick tries again.
end;
end;
Pitfalls
OnDisconnectis not a loss detector. Never rely on it alone to notice that the server went away; ping periodically.- Echo the payload. A pong that does not carry the ping's payload can be rejected by strict servers.
twsoManualPongmeans you must answer. Servers disconnect clients that stop answering their pings; reply fromOnPingunless dropping the connection is the intent.- Abort, then reconnect. Calling
Disconnectwith a close frame on a dead connection waits for a reply that never comes; useDisconnect(False). - Set
Optionsbefore connecting. They cannot change while the client is connected.
Related API
See also
- Connecting — opening, closing, and aborting the connection.
- Sending and receiving — messages and the event thread.
- Server connection health — the server side of ping and pong.