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.
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.
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.
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.
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
OnConnectgo nowhere. The handshake is not complete yet; send welcome messages fromOnHandshakeResponseSent. OnAllowis never synchronized. Do not touch controls in it, even withAutoSyncEventsenabled.- Enable
AutoSyncEventsfor user interface handlers. Without it, handlers that update controls run on a background thread. - Change
Portwhile inactive. A new port takes effect the next time the server starts;OptionsandUseSSLcannot change at all while it is active. - Do not hold on to connections. A connection object is freed after its
client disconnects; with
AutoSyncEventsenabled, use theAConnectionof a queuedOnDisconnectonly as an identity, not to read its data. - Open the port. A firewall that blocks
Portlooks to clients like a server that is not running.
Related API
TTMSFNCWebSocketServer,TTMSFNCWebSocketServerConnection,TTMSFNCWebSocketRequestActive,Port,BindingIP,PathName,Options,AutoSyncEventsOnConnect,OnAllow,OnHandshakeResponseSent,OnClose,OnDisconnect,TransformCloseData
See also
- Broadcasting messages — send to all or selected clients.
- Custom connection data — attach your own data to each client.
- Connection health — ping, pong, and dropping dead clients.
- Secure connections — serve
wss://with a certificate. - Connecting — the client side of the connection.