Skip to main content
Table of Contents

Connection health

Clients disappear without warning: a laptop lid closes, a phone switches networks, a browser tab is killed. Each vanished client leaves a connection behind that the server keeps serving until the operating system gives up on it, which can take a long time. The WebSocket protocol provides ping and pong control frames to test liveness in both directions, and a close frame for an orderly goodbye. This chapter covers how the server answers client pings, how to take over that reply, how to ping clients yourself and drop the ones that stopped answering, and which events report a client leaving. Use it for any server that runs unattended or holds many long-lived connections.

Answering client pings

When a client sends a ping, the server answers it automatically with a pong that carries the same payload and then raises OnPing with that payload in AData. Nothing is required to keep clients happy.

Replying to pings manually

Add twsoManualPong to Options while the server is inactive to take over the reply — for example to throttle clients that ping too often. OnPing is then where you answer, by sending a pong frame with SendSimpleFrame and the ping's payload. Send focPong, not focPing: answering a ping with another ping never satisfies the client.

Answer client pings manuallyPascal
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Options can only change while the server is not active.
  TMSFNCWebSocketServer1.Options := TMSFNCWebSocketServer1.Options + [twsoManualPong];
end;

procedure TForm1.TMSFNCWebSocketServer1Ping(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AData: TBytes);
begin
  // Reply with a pong (not a ping) carrying the same payload.
  AConnection.SendSimpleFrame(focPong, AData);
end;

Pinging clients

The server component has no Ping method of its own; send a ping frame through the client's connection with SendSimpleFrame(focPing, ...). The client answers with a pong, which raises OnPong. Recording the time of the last pong in each connection's UserData tells you which clients are still alive.

Detecting clients that leave

Three events report a client leaving:

  • OnClose fires when a client sends a close frame. AData carries the close status code and an optional reason, which TransformCloseData decodes. The server replies with its own close frame automatically unless twsoManualClose is in Options.
  • OnDisconnect fires when the connection is gone, whether it was closed properly or dropped.
  • A client that vanished without closing is only noticed when the operating system reports the broken connection — or when it stops answering your pings.

Combining pings, connection data, and client removal

The example keeps the time of each client's last pong in its connection data, pings all clients from a timer, and closes the clients that have not answered for a minute with DisconnectClient. The ping itself is sent from a SendMessageTo selector, which visits every connected client; the selector returns False, so no message is broadcast.

Ping all clients and drop unresponsive onesPascal
type
  TClientState = class
  public
    LastPong: TDateTime;
  end;

procedure TForm1.TMSFNCWebSocketServer1HandshakeResponseSent(Sender: TObject;
  AConnection: TTMSFNCWebSocketServerConnection);
begin
  AConnection.OwnsUserData := True;
  AConnection.UserData := TClientState.Create;
  TClientState(AConnection.UserData).LastPong := Now;
end;

procedure TForm1.HeartbeatTimerTimer(Sender: TObject);
var
  Deadline: TDateTime;
begin
  Deadline := IncSecond(Now, -60);

  // Close clients that have not answered for a minute.
  TMSFNCWebSocketServer1.DisconnectClient(
    function(AConnection: TTMSFNCWebSocketServerConnection): Boolean
    begin
      Result := Assigned(AConnection.UserData) and
        (TClientState(AConnection.UserData).LastPong < Deadline);
    end);

  // Ping the others. The selector visits every connected client; it sends
  // the ping itself and returns False, so no message is broadcast.
  TMSFNCWebSocketServer1.SendMessageTo('',
    function(AConnection: TTMSFNCWebSocketServerConnection): Boolean
    begin
      try
        AConnection.SendSimpleFrame(focPing, TEncoding.UTF8.GetBytes('heartbeat'));
      except
        // The socket is already gone; the server cleans it up.
      end;
      Result := False;
    end);
end;

procedure TForm1.TMSFNCWebSocketServer1Pong(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AData: TBytes);
var
  Conn: TTMSFNCWebSocketServerConnection;
begin
  Conn := AConnection as TTMSFNCWebSocketServerConnection;
  if Assigned(Conn.UserData) then
    TClientState(Conn.UserData).LastPong := Now;
end;

Pitfalls

  • Answer a ping with a pong. SendSimpleFrame(focPing) in OnPing sends a new ping instead of an answer; use focPong.
  • Echo the payload. Pass the received AData to the pong.
  • twsoManualPong means you must answer. Clients disconnect servers that stop answering their pings.
  • Change Options while inactive. Setting Options on an active server raises an exception.
  • Treat OnDisconnect as final. The connection object is released after it; do not keep references to it.

See also