Skip to main content
Table of Contents

Custom connection data

A server that only broadcasts can treat its clients as anonymous, but most real servers need to know who is on each connection: a user ID, a display name, the room a user joined, the topics a device subscribed to, or a timestamp of the last activity. TTMSFNCWebSocketServerConnection carries a UserData object for exactly that, with optional automatic cleanup when the client leaves. This chapter covers attaching data to a connection, the two usual moments to do it — when the client tells you who it is, or when the server assigns an identity itself — and how that data turns into message routing. Use it whenever a message handler or a send selector needs to tell one client from another.

Attaching data to a client

UserData holds any TObject you assign; define a small class with the fields you need. Set OwnsUserData to True and the connection frees the object when it is destroyed after the client disconnects. With OwnsUserData at False you own the object and must free it yourself.

In event handlers that pass a TTMSFNCWebSocketConnection (the message, ping, pong, and close events), cast AConnection to TTMSFNCWebSocketServerConnection to reach UserData; OnAllow and OnHandshakeResponseSent already pass the server connection type.

Storing data the client sends

When the data comes from the client itself — a login, a registration, a room to join — the message handler is the place to store it. The example accepts a JSON registration message and keeps the client's ID on its connection.

Store client-supplied data on the connectionPascal
uses
  System.JSON;

type
  TMyDataObject = class
  private
    FID: string;
  public
    property ID: string read FID write FID;
  end;

procedure TForm1.TMSFNCWebSocketServer1MessageReceived(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
var
  Json: TJSONValue;
  Conn: TTMSFNCWebSocketServerConnection;
  MsgType, ID: string;
begin
  // On the server, every connection is a TTMSFNCWebSocketServerConnection.
  Conn := AConnection as TTMSFNCWebSocketServerConnection;
  Json := TJSONObject.ParseJSONValue(AMessage);
  if not Assigned(Json) then
    Exit;
  try
    if not Json.TryGetValue<string>('type', MsgType) then
      Exit;

    // {"type":"user-data","id":"42"}
    if (MsgType = 'user-data') and not Assigned(Conn.UserData) then
    begin
      Json.TryGetValue<string>('id', ID);
      Conn.OwnsUserData := True;             // freed together with the connection
      Conn.UserData := TMyDataObject.Create;
      TMyDataObject(Conn.UserData).ID := ID;
    end
    else if MsgType = 'join' then
    begin
      // handle other message types here
    end;
  finally
    Json.Free;
  end;
end;

Assigning data when the connection opens

When the server assigns the data — a session ID, a sequence number, a default room — assign it in OnHandshakeResponseSent, which fires when the server has answered the handshake and the connection is established. Every client then has data from the start, and message handlers never meet a connection without it.

Assign server-generated data when the handshake completesPascal
procedure TForm1.TMSFNCWebSocketServer1HandshakeResponseSent(Sender: TObject;
  AConnection: TTMSFNCWebSocketServerConnection);
begin
  // The connection is established: attach data the server owns.
  AConnection.OwnsUserData := True;
  AConnection.UserData := TMyDataObject.Create;
  TMyDataObject(AConnection.UserData).ID := TGUID.NewGuid.ToString;

  AConnection.Send(Format('{"type":"your-id","id":"%s"}',
    [TMyDataObject(AConnection.UserData).ID]));
end;

Data that arrives with the handshake itself — a token in the query string, a cookie in the headers — can be read from HandshakeRequest in the same event.

Combining assigned IDs with message routing

Connection data pays off in send selectors. The example delivers a direct message by looking up the recipient among the IDs assigned when each connection opened.

Send a direct message by connection IDPascal
uses
  System.JSON;

procedure TForm1.TMSFNCWebSocketServer1MessageReceived(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
var
  Json: TJSONValue;
  TargetID, Text, SenderID: string;
begin
  // {"type":"direct","to":"<id>","text":"Hi"}
  Json := TJSONObject.ParseJSONValue(AMessage);
  if not Assigned(Json) then
    Exit;
  try
    if Json.TryGetValue<string>('to', TargetID) and Json.TryGetValue<string>('text', Text) then
    begin
      // The IDs were assigned in OnHandshakeResponseSent.
      SenderID := TMyDataObject((AConnection as TTMSFNCWebSocketServerConnection).UserData).ID;
      TMSFNCWebSocketServer1.SendMessageTo(
        Format('{"type":"direct","from":"%s","text":"%s"}', [SenderID, Text]),
        function(AClient: TTMSFNCWebSocketServerConnection): Boolean
        begin
          Result := Assigned(AClient.UserData) and
            (TMyDataObject(AClient.UserData).ID = TargetID);
        end);
    end;
  finally
    Json.Free;
  end;
end;

Pitfalls

  • Decide who frees the object. Set OwnsUserData := True for data that lives exactly as long as the connection; otherwise free it yourself, or it leaks.
  • Check before casting. A connection has no UserData until you assign it; test Assigned(UserData) in handlers and selectors that can run earlier.
  • Data is freed with the connection. With OwnsUserData, the object is gone once the client has disconnected; copy anything you need to keep while the client is still connected.
  • Mind the threads. Without AutoSyncEvents, handlers for different clients run concurrently; shared structures that several connections update need locking.

See also