Skip to main content
Table of Contents

Running a server

TTMSFNCWebSocketServer is a non-visual component that hosts a WebSocket endpoint inside a VCL or FMX application, following the RFC 6455 protocol. Use it to push live data to browsers, mobile apps, or other Delphi clients, to build chat and collaboration back ends, or to accept device connections on a local network — anywhere clients should receive data the moment it is available instead of polling. Every client gets its own connection thread, and the server raises events as clients connect, complete the handshake, send messages, and leave. This chapter covers starting and stopping the server, where it listens, which connections it accepts, the order of the lifecycle events, and which thread those events arrive on.

Starting and stopping

Set Active to True to start listening and to False to stop; stopping drops every connected client. When Active is True at design time, the server starts as soon as the form has loaded.

Start a WebSocket serverPascalC++
procedure TForm1.StartServer;
begin
  FServer := TTMSFNCWebSocketServer.Create(Self);
  FServer.Port := 5050;              // default is 8888
  FServer.PathName := 'chat';        // clients connect to ws://<host>:5050/chat
  FServer.AutoSyncEvents := True;    // raise events on the main thread

  FServer.OnConnect := ServerConnect;
  FServer.OnDisconnect := ServerDisconnect;
  FServer.OnMessageReceived := ServerMessageReceived;

  FServer.Active := True;            // begin listening for clients
end;

procedure TForm1.StopServer;
begin
  FServer.Active := False;           // stop listening and drop all clients
end;

procedure TForm1.ServerConnect(Sender: TObject; AConnection: TTMSFNCWebSocketConnection);
begin
  // A TCP connection arrived; the WebSocket handshake has not happened yet.
  Log('Connection from ' + AConnection.PeerIP);
end;

procedure TForm1.ServerDisconnect(Sender: TObject; AConnection: TTMSFNCWebSocketConnection);
begin
  Log('A client disconnected');
end;
void __fastcall TForm1::StartServer()
{
  FServer = new TTMSFNCWebSocketServer(this);
  FServer->Port = 5050;              // default is 8888
  FServer->PathName = "chat";        // clients connect to ws://<host>:5050/chat
  FServer->AutoSyncEvents = true;    // raise events on the main thread

  FServer->OnConnect = ServerConnect;
  FServer->OnDisconnect = ServerDisconnect;
  FServer->OnMessageReceived = ServerMessageReceived;

  FServer->Active = true;            // begin listening for clients
}

void __fastcall TForm1::StopServer()
{
  FServer->Active = false;           // stop listening and drop all clients
}

void __fastcall TForm1::ServerConnect(TObject *Sender, TTMSFNCWebSocketConnection *AConnection)
{
  // A TCP connection arrived; the WebSocket handshake has not happened yet.
  Log("Connection from " + AConnection->PeerIP);
}

void __fastcall TForm1::ServerDisconnect(TObject *Sender, TTMSFNCWebSocketConnection *AConnection)
{
  Log("A client disconnected");
}

Choosing where the server listens

Port selects the TCP port and defaults to 8888; you can also pass a port to the constructor. By default the server listens on all network interfaces; set BindingIP (public) to listen on one interface only. Port and BindingIP are applied when the server starts, so change them while it is inactive and restart it to use new values.

PathName restricts the endpoint to one path: with PathName := 'chat', clients connect to ws://<host>:<port>/chat, and requests for any other path are answered with 404 Not Found. The comparison ignores case and accepts the path with or without a leading slash. Leave PathName empty to accept connections on any path.

Accepting or refusing connections

Every connection must start with a valid WebSocket handshake request; requests without the required headers, or with an unsupported protocol version, are refused with 400 Bad Request. Once a request passes those checks, OnAllow fires with AAllow set to True. Set it to False to refuse the client regardless of the request. The connection's HandshakeRequest (a TTMSFNCWebSocketRequest) exposes what the client sent: Origin, Host, Resource (the requested path), QueryParams, Protocol, and RawHeaders.

Accept or refuse connections during the handshakePascalC++
procedure TForm1.TMSFNCWebSocketServer1Allow(Sender: TObject;
  AConnection: TTMSFNCWebSocketServerConnection; var AAllow: Boolean);
var
  Params: TStringList;
begin
  // AAllow arrives True for every valid handshake request.
  // This event runs on the connection's own thread: do not touch controls here.
  if not SameText(AConnection.HandshakeRequest.Origin, 'https://app.example.com') then
  begin
    AAllow := False;
    Exit;
  end;

  // ws://host:5050/chat?token=abc123  ->  QueryParams = 'token=abc123'
  Params := TStringList.Create;
  try
    Params.Delimiter := '&';
    Params.StrictDelimiter := True;
    Params.DelimitedText := AConnection.HandshakeRequest.QueryParams;
    AAllow := IsValidToken(Params.Values['token']);
  finally
    Params.Free;
  end;
end;
void __fastcall TForm1::TMSFNCWebSocketServer1Allow(TObject *Sender,
  TTMSFNCWebSocketServerConnection *AConnection, bool &AAllow)
{
  // AAllow arrives True for every valid handshake request.
  // This event runs on the connection's own thread: do not touch controls here.
  if (!SameText(AConnection->HandshakeRequest->Origin, "https://app.example.com"))
  {
    AAllow = false;
    return;
  }

  // ws://host:5050/chat?token=abc123  ->  QueryParams = 'token=abc123'
  TStringList *Params = new TStringList();
  try
  {
    Params->Delimiter = '&';
    Params->StrictDelimiter = true;
    Params->DelimitedText = AConnection->HandshakeRequest->QueryParams;
    AAllow = IsValidToken(Params->Values["token"]);
  }
  __finally
  {
    delete Params;
  }
}

Two options relax the handshake checks for non-conforming clients: twsoSkipUpgradeCheck skips the Upgrade header check, and twsoSkipVersionCheck skips the protocol version check. Add them to Options only when a known client needs it.

Understanding the client lifecycle

A client passes through several stages, each with its own event:

Stage Event Notes
TCP connection accepted OnConnect The WebSocket handshake has not happened yet; the client cannot receive messages.
Handshake validated OnAllow Accept or refuse the client.
Handshake response sent OnHandshakeResponseSent The connection is established; from here the client sends and receives messages.
Close frame received OnClose The client started the closing handshake; the server replies automatically.
Connection gone OnDisconnect The client left, by closing handshake or otherwise.

Treat OnHandshakeResponseSent, not OnConnect, as the moment a client has joined. The connection objects also expose PeerIP and PeerPort for logging and filtering. After the connection is gone, the server releases the connection object, so do not keep references to it past OnDisconnect.

Choosing the event thread

Each client is served on its own thread. AutoSyncEvents decides where the events arrive: set it to True to queue the connect, handshake, message, ping, pong, close, and disconnect events to the main thread, so handlers can update controls directly. It is False unless you enable it, in which case those events run on the client's thread and every user interface update must be queued with TThread.Queue or TThread.Synchronize. OnAllow always runs on the client's thread, because its answer is needed before the handshake response is sent.

Leaving AutoSyncEvents off is the better choice for a busy server: each client's messages are processed in parallel on its own thread, and only the user interface updates are queued.

Handle server events on the client threadsPascalC++
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Default: events run on the thread that serves the client.
  TMSFNCWebSocketServer1.AutoSyncEvents := False;
end;

procedure TForm1.TMSFNCWebSocketServer1MessageReceived(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
var
  Reply: string;
begin
  // Runs concurrently for different clients: keep shared state thread-safe.
  Reply := ProcessCommand(AMessage);
  AConnection.Send(Reply);

  TThread.Queue(nil,
    procedure
    begin
      Memo1.Lines.Add('Handled: ' + AMessage);
    end);
end;
void __fastcall TForm1::FormCreate(TObject *Sender)
{
  // Default: events run on the thread that serves the client.
  TMSFNCWebSocketServer1->AutoSyncEvents = false;
}

void __fastcall TForm1::ShowHandled()
{
  Memo1->Lines->Add(FHandled);
}

void __fastcall TForm1::TMSFNCWebSocketServer1MessageReceived(TObject *Sender,
  TTMSFNCWebSocketConnection *AConnection, const System::UnicodeString AMessage)
{
  // Runs concurrently for different clients: keep shared state thread-safe.
  System::UnicodeString Reply = ProcessCommand(AMessage);
  AConnection->Send(Reply);

  FHandled = "Handled: " + AMessage;
  TThread::Queue(nullptr, ShowHandled);
}

Combining the server setup with lifecycle events

The example starts a server on one network interface and logs each stage a client passes through, from the TCP connection to the close frame and the disconnect, reading the close code and reason with TransformCloseData.

Track the connection lifecyclePascalC++
procedure TForm1.StartBtnClick(Sender: TObject);
begin
  TMSFNCWebSocketServer1.BindingIP := '192.168.1.10';  // listen on this interface only
  TMSFNCWebSocketServer1.Port := 8888;
  TMSFNCWebSocketServer1.AutoSyncEvents := True;
  TMSFNCWebSocketServer1.Active := True;
end;

procedure TForm1.TMSFNCWebSocketServer1Connect(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection);
begin
  Log(Format('TCP connect from %s:%d', [AConnection.PeerIP, AConnection.PeerPort]));
end;

procedure TForm1.TMSFNCWebSocketServer1HandshakeResponseSent(Sender: TObject;
  AConnection: TTMSFNCWebSocketServerConnection);
begin
  // The handshake is complete: the client can now send and receive messages.
  Log('WebSocket open on path ' + AConnection.HandshakeRequest.Resource);
  AConnection.Send('{"type":"welcome"}');
end;

procedure TForm1.TMSFNCWebSocketServer1Close(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection; const AData: TBytes);
var
  Reason: string;
  Code: Word;
begin
  Code := TTMSFNCWebSocketServer.TransformCloseData(AData, Reason);
  Log(Format('Client closed the connection (code %d) %s', [Code, Reason]));
end;

procedure TForm1.TMSFNCWebSocketServer1Disconnect(Sender: TObject;
  AConnection: TTMSFNCWebSocketConnection);
begin
  Log('Client disconnected');
end;
void __fastcall TForm1::StartBtnClick(TObject *Sender)
{
  TMSFNCWebSocketServer1->BindingIP = "192.168.1.10";  // listen on this interface only
  TMSFNCWebSocketServer1->Port = 8888;
  TMSFNCWebSocketServer1->AutoSyncEvents = true;
  TMSFNCWebSocketServer1->Active = true;
}

void __fastcall TForm1::TMSFNCWebSocketServer1Connect(TObject *Sender,
  TTMSFNCWebSocketConnection *AConnection)
{
  Log(Format("TCP connect from %s:%d", ARRAYOFCONST((AConnection->PeerIP, (int)AConnection->PeerPort))));
}

void __fastcall TForm1::TMSFNCWebSocketServer1HandshakeResponseSent(TObject *Sender,
  TTMSFNCWebSocketServerConnection *AConnection)
{
  // The handshake is complete: the client can now send and receive messages.
  Log("WebSocket open on path " + AConnection->HandshakeRequest->Resource);
  AConnection->Send("{\"type\":\"welcome\"}");
}

void __fastcall TForm1::TMSFNCWebSocketServer1Close(TObject *Sender,
  TTMSFNCWebSocketConnection *AConnection, const System::DynamicArray<System::Byte> AData)
{
  System::UnicodeString Reason;
  System::Word Code = TTMSFNCWebSocketServer::TransformCloseData(AData, Reason);
  Log(Format("Client closed the connection (code %d) %s", ARRAYOFCONST(((int)Code, Reason))));
}

void __fastcall TForm1::TMSFNCWebSocketServer1Disconnect(TObject *Sender,
  TTMSFNCWebSocketConnection *AConnection)
{
  Log("Client disconnected");
}

Pitfalls

  • Messages sent in OnConnect go nowhere. The handshake is not complete yet; send welcome messages from OnHandshakeResponseSent.
  • OnAllow is never synchronized. Do not touch controls in it, even with AutoSyncEvents enabled.
  • Enable AutoSyncEvents for user interface handlers. Without it, handlers that update controls run on a background thread.
  • Change Port while inactive. A new port takes effect the next time the server starts; Options and UseSSL cannot change at all while it is active.
  • Do not hold on to connections. A connection object is freed after its client disconnects; with AutoSyncEvents enabled, use the AConnection of a queued OnDisconnect only as an identity, not to read its data.
  • Open the port. A firewall that blocks Port looks to clients like a server that is not running.

See also