Skip to main content
Table of Contents

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.

Answer pings manuallyPascal
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, and Pong belongs in a try..except block.
  • 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.

Detect a lost connection while sendingPascal
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.

Detect a lost connection with a ping heartbeat and reconnectPascal
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

  • OnDisconnect is 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.
  • twsoManualPong means you must answer. Servers disconnect clients that stop answering their pings; reply from OnPing unless dropping the connection is the intent.
  • Abort, then reconnect. Calling Disconnect with a close frame on a dead connection waits for a reply that never comes; use Disconnect(False).
  • Set Options before connecting. They cannot change while the client is connected.

See also