WebSocket4D
Biblioteca cliente + servidor WebSocket em Delphi (Indy 10, RFC 6455), sem dependência do DMVCFramework. MIT.
- Active · last change 7-20-2026
- ★ 5 on GitHub
README
WebSocket4D
WebSocket client + server library for Delphi (Object Pascal), built on top of Indy 10, following RFC 6455.
TWebSocketServer4D.New
.Port(8080)
.OnMessage(procedure(const AConn: IWebSocketConnection; const AText: string)
begin
AConn.Send('echo: ' + AText);
end)
.Start;Table of contents
- Features
- Dependencies
- Installation
- Your first server
- Your first client
- 1:1 chat (echo — reply to the sender only)
- Broadcast (1:N chat)
- Console example
- Win32 application example (VCL)
- FMX example for Android/iOS
- Threading model
- Middleware
- TLS (
wss://) and OpenSSL by platform - API reference
- Samples
- Tests
- License
Features
- Fluent, interface-first builder API for both client and server (
TWebSocketClient4D.New,TWebSocketServer4D.New). - Text and binary frames, fragmentation, automatic ping/pong, close with code + reason.
- Correct masking (client masks, server does not), per RFC 6455.
- Broadcast to every connected client, plus per-connection session state (
IWebSocketConnection.Items). - Extensible via middleware (
.Use(...)) without touching the library — messages are transformed in/out. - Injectable transport and logger (
.Transport(...),.Logger(...)). - Optional native
wss://(TLS) on the server; also works fine behind a reverse proxy that terminates TLS. - No
permessage-deflatein v1.
Dependencies
WebSocket4D deliberately keeps its dependency surface small. Everything in the Required table ships with RAD Studio — a stock install needs nothing extra to compile and run the library over ws://.
Required
| Dependency | Version | Notes |
|---|---|---|
| RAD Studio / Delphi | 10.3.3 Rio or newer | Verified by compiling and running the full test suite on 10.3.3 Rio, 12 Athens and 13. |
| Indy 10 | Bundled with RAD Studio | The default transport. Units used: IdGlobal, IdIOHandler, IdTCPClient, IdTCPServer, IdContext, IdSSLOpenSSL. |
| Delphi RTL | Bundled | System.SysUtils, System.Classes, System.SyncObjs, System.Generics.Collections, System.Rtti, System.NetEncoding, System.Hash, System.StrUtils. |
Optional
| Dependency | When you need it | Notes |
|---|---|---|
| OpenSSL 1.0.2 | Only for wss:// (TLS) |
Runtime libraries, not compile-time. See TLS and OpenSSL by platform. Plain ws:// needs nothing. |
| DUnitX | Only to run the test suite | Bundled with RAD Studio. Not needed to consume the library. |
Explicitly not required
No third-party package of any kind. There are no external .dpks to install.
Platform support
| Platform | Server | Client |
|---|---|---|
| Windows (Win32 / Win64) | ✅ | ✅ |
| Linux (64-bit) | ✅ | ✅ |
| Android | — | ✅ |
| iOS | — | ✅ |
The client is validated for compilation on Android/iOS through the FMX sample; running on a physical device/emulator is left to the developer's environment.
Installation
WebSocket4D is a source-only library — there is no runtime/design-time .dpk to install into the IDE. You make it available to a project by putting the src folder on that project's unit search path. Both methods below do exactly that; BOSS just automates it.
Manual installation (Search Path)
-
Get the source. Clone the repository, or download a release ZIP and extract it, somewhere accessible to your project:
git clone https://github.com/adrianosantostreina/WebSocket4D.gitA common layout is a shared
libsfolder next to your projects:C:\Dev\ ?????? libs\ ??? ?????? WebSocket4D\ ??? ?????? src\ <-- this is the path you will add ?????? MyApp\ ?????? MyApp.dproj -
Add
srcto the search path. In the RAD Studio IDE:Project ▸ Options ▸ Building ▸ Delphi Compiler ▸ Search path
Add the full path to the
srcfolder, e.g.C:\Dev\libs\WebSocket4D\src.Set the Target dropdown to All configurations - All platforms before adding, otherwise the path only applies to the currently selected configuration and you will get "unit not found" when switching to Release or to a mobile target.
Installation via BOSS
boss install github.com/adrianosantostreina/WebSocket4DOr pinned to a specific version:
boss install github.com/adrianosantostreina/WebSocket4D:1.0.1Note: the
boss.jsonat the repository root describes the library only. The applications undersamples/are not BOSS projects — they reference..\..\srcdirectly (see Samples).
Which units to uses
Whichever installation method you chose, the units you reference are the same. There is no umbrella unit — add what you actually use:
uses
WebSocket4D.Core.Types, // TWSCloseCode, TWSMessage, EWebSocketError...
WebSocket4D.Client, // TWebSocketClient4D, IWebSocketClient
WebSocket4D.Server, // TWebSocketServer4D, IWebSocketServer
WebSocket4D.Server.Connection,// IWebSocketConnection
WebSocket4D.Transport.Indy; // registers the default transport — requiredOne unit matters for transport registration, and forgetting it is the most common first error:
WebSocket4D.Transport.Indymust be in youruses— for a client and for a server. Omitting it raisesEWebSocketError: No ITransportFactory availableon.Connector.Start.
That unit self-registers its factory in its initialization section, so simply naming it in uses is enough — you never call it directly. The registered factory creates both client and server transports, which is why the same single unit covers both roles.
WebSocket4D.Transport.Indy.Server does not need to be in your uses: it holds the server transport class, and WebSocket4D.Transport.Indy already pulls it in through its own implementation section. Listing it explicitly is harmless (the shipped samples do), just unnecessary.
Your first server
The smallest useful server: listen on a port, and reply to whatever arrives.
program MyFirstServer;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
WebSocket4D.Core.Types,
WebSocket4D.Server,
WebSocket4D.Server.Connection,
WebSocket4D.Transport.Indy; // registers the transport — required
var
GServer: IWebSocketServer;
begin
GServer := TWebSocketServer4D.New
.Port(8080)
.Path('/')
.OnConnect(procedure(const AConn: IWebSocketConnection)
begin
Writeln(Format('client #%s connected', [AConn.Id]));
end)
.OnMessage(procedure(const AConn: IWebSocketConnection; const AText: string)
begin
Writeln(Format('#%s -> %s', [AConn.Id, AText]));
AConn.Send('echo: ' + AText);
end)
.OnDisconnect(procedure(const AConn: IWebSocketConnection)
begin
Writeln(Format('client #%s disconnected', [AConn.Id]));
end)
.Start;
Writeln('listening on ws://127.0.0.1:8080/ — press Enter to stop');
Readln;
GServer.Stop;
end.Lifetime. .Start returns an IWebSocketServer; the server runs for as long as you hold that interface. Storing it in a local variable that goes out of scope immediately shuts the server down — keep it in a global, a field, or a variable that stays alive.
.Build vs .Start. .Build materializes the server without listening (useful to configure now and start later); .Start builds and starts listening. Only .Start accepts connections.
Builder methods available on TWebSocketServer4D:
| Method | Purpose |
|---|---|
.Port(AValue: Integer) |
TCP port to listen on. |
.Path(const AValue: string) |
Accepted handshake path; defaults to /. A request to any other path is rejected with 404. |
.TLS(const ACertFile, AKeyFile: string) |
Enables wss:// with the given PEM certificate and key. |
.Use(const AMiddleware: IWebSocketMiddleware) |
Appends a middleware to the pipeline. |
.Transport(const AFactory: ITransportFactory) |
Replaces the transport (testing, or a non-Indy stack). |
.Logger(const ALogger: ILogger) |
Injects a logger. |
.OnConnect / .OnDisconnect |
procedure(const AConn: IWebSocketConnection) |
.OnMessage |
procedure(const AConn: IWebSocketConnection; const AText: string) |
.OnBinary |
procedure(const AConn: IWebSocketConnection; const AData: TBytes) |
.Build / .Start |
Materialize / materialize and listen. |
Your first client
program MyFirstClient;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
WebSocket4D.Core.Types,
WebSocket4D.Client,
WebSocket4D.Transport.Indy; // required
var
GClient: IWebSocketClient;
LLine: string;
begin
GClient := TWebSocketClient4D.New
.URL('ws://127.0.0.1:8080/')
.SyncEvents(False) // console app — see "Threading model"
.OnOpen(procedure
begin
Writeln('connected');
end)
.OnMessage(procedure(AText: string)
begin
Writeln(AText);
end)
.OnClose(procedure(ACode: Integer; AReason: string)
begin
Writeln(Format('closed (code=%d reason=%s)', [ACode, AReason]));
end)
.OnError(procedure(AError: string)
begin
Writeln('error: ', AError);
end)
.Connect;
if not GClient.IsOpen then
begin
Writeln('failed to connect.');
Exit;
end;
while GClient.IsOpen and not Eof do
begin
Readln(LLine);
GClient.Send(LLine);
end;
GClient.Disconnect(wsccNormal);
end.Check IsOpen after .Connect. .Connect does not raise on failure — it dispatches OnError and leaves the client closed. Always test IsOpen before sending; Send on a non-open client raises EWebSocketError.
Don't send from OnOpen if you assign the result. OnOpen fires synchronously inside .Connect, before the assignment to your variable completes — so a handler that touches GClient will hit an unassigned interface. Send your first message after .Connect returns, as the samples do.
Builder methods available on TWebSocketClient4D:
| Method | Purpose |
|---|---|
.URL(const AValue: string) |
ws://host:port/path or wss://.... |
.SubProtocol(const AValue: string) |
Value for Sec-WebSocket-Protocol. |
.SyncEvents(AValue: Boolean) |
True (default) marshals events to the main thread; False runs them on the receive thread. |
.VerifyCertificate(AValue: Boolean) |
TLS peer verification. Defaults to False. |
.Use / .Transport / .Logger |
Same as the server. |
.OnOpen |
procedure |
.OnMessage |
procedure(AText: string) |
.OnBinary |
procedure(AData: TBytes) |
.OnClose |
procedure(ACode: Integer; AReason: string) |
.OnError |
procedure(AError: string) |
.Build / .Connect |
Materialize / materialize and connect. |
1:1 chat (echo — reply to the sender only)
A 1:1 exchange means the server answers only the connection the message came from. That is exactly what AConn.Send does — AConn is the originating connection.
Server:
GServer := TWebSocketServer4D.New
.Port(8080)
.OnMessage(procedure(const AConn: IWebSocketConnection; const AText: string)
begin
AConn.Send('echo: ' + AText); // only this client receives it
end)
.Start;Client:
GClient := TWebSocketClient4D.New
.URL('ws://127.0.0.1:8080/')
.SyncEvents(False)
.OnMessage(procedure(AText: string)
begin
Writeln(AText); // prints "echo: hello"
end)
.Connect;
GClient.Send('hello');Run two clients against this server and each one sees only its own replies. Runnable version: samples/console/echo/.
Broadcast (1:N chat)
Broadcast sends one message to every connected client. IWebSocketServer.Broadcast iterates a snapshot of the live connections and skips any that are no longer open.
This example also shows per-connection session state: IWebSocketConnection.Items is a TDictionary<string, TValue> owned by the connection, used here to remember each client's nickname. The first message a client sends is treated as its nickname; everything after that is a chat line.
Server:
uses
System.Rtti, // required for TValue in Items
WebSocket4D.Core.Types,
WebSocket4D.Server,
WebSocket4D.Server.Connection,
WebSocket4D.Transport.Indy;
var
GServer: IWebSocketServer;
function Nick(const AConn: IWebSocketConnection): string;
begin
if AConn.Items.ContainsKey('nick') then
Result := AConn.Items['nick'].AsString
else
Result := '#' + AConn.Id;
end;
procedure HandleMessage(const AConn: IWebSocketConnection; const AText: string);
begin
if not AConn.Items.ContainsKey('nick') then
begin
// First message from this connection is the nickname.
AConn.Items.AddOrSetValue('nick', AText);
GServer.Broadcast(Format('*** %s joined ***', [AText]));
end
else
GServer.Broadcast(Format('%s: %s', [Nick(AConn), AText]));
end;
procedure HandleDisconnect(const AConn: IWebSocketConnection);
begin
if AConn.Items.ContainsKey('nick') then
GServer.Broadcast(Format('*** %s left ***', [Nick(AConn)]));
end;
begin
GServer := TWebSocketServer4D.New
.Port(8080)
.OnMessage(HandleMessage)
.OnDisconnect(HandleDisconnect)
.Start;
// ...
end.Client: identical to the first client above — it just sends its nickname first:
GClient := TWebSocketClient4D.New
.URL('ws://127.0.0.1:8080/')
.SyncEvents(False)
.OnMessage(procedure(AText: string) begin Writeln(AText); end)
.Connect;
GClient.Send('Alice'); // nickname
GClient.Send('hello everyone'); // broadcast to all: "Alice: hello everyone"Note that the sender also receives its own broadcast message — the server does not exclude the originating connection. If you want sender-exclusive behavior, iterate GServer.Clients yourself and skip the matching Id:
var
LConn: IWebSocketConnection;
begin
for LConn in GServer.Clients do
if LConn.IsOpen and (LConn.Id <> AConn.Id) then
LConn.Send(AText);
end;Thread safety of
Items. Each connection'sItemsdictionary has no internal lock. It is safe as long as you only touch a connection'sItemsfrom that connection's own event handlers (which all run on that connection's thread). Reading another connection'sItemsfrom a different thread — e.g. from a UI thread while building a connection list — is a data race. See Threading model.
Runnable version: samples/console/chat/.
Console example
Console apps have no message loop, so TThread.Queue never drains. Set .SyncEvents(False) so that client events run directly on the receive thread instead of being queued and never delivered.
program ChatClientConsole;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
WebSocket4D.Core.Types,
WebSocket4D.Client,
WebSocket4D.Transport.Indy;
var
GClient: IWebSocketClient;
GNickname, LLine: string;
begin
try
if ParamCount >= 1 then
GNickname := ParamStr(1)
else
begin
Write('Nickname: ');
Readln(GNickname);
end;
GClient := TWebSocketClient4D.New
.URL('ws://127.0.0.1:8080/')
.SyncEvents(False) // console: no message loop to pump TThread.Queue
.OnMessage(procedure(AText: string) begin Writeln(AText); end)
.OnClose(procedure(ACode: Integer; AReason: string)
begin
Writeln(Format('closed (code=%d reason=%s)', [ACode, AReason]));
end)
.OnError(procedure(AError: string) begin Writeln('error: ', AError); end)
.Connect;
if not GClient.IsOpen then
begin
Writeln('failed to connect.');
Exit;
end;
GClient.Send(GNickname); // first message = nickname
while GClient.IsOpen and not Eof do
begin
Readln(LLine);
if SameText(LLine, '/quit') then
begin
GClient.Disconnect(wsccNormal);
Break;
end
else if SameText(LLine, '/ping') then
GClient.Ping
else
GClient.Send(LLine);
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.Four complete console programs ship in the repository — echo server/client and chat server/client. See samples/console/ and samples/README.md for build and run instructions.
Win32 application example (VCL)
In a GUI application, keep .SyncEvents(True) (the default) on the client — events are then marshalled to the main thread via TThread.Queue and you can touch VCL controls directly.
The server is different: its handlers run on each connection's own thread and are not marshalled for you. Wrap every UI access in TThread.Queue yourself.
unit MainForm;
interface
uses
System.SysUtils, System.Classes, Vcl.Forms, Vcl.StdCtrls, Vcl.Controls,
WebSocket4D.Core.Types,
WebSocket4D.Client,
WebSocket4D.Server,
WebSocket4D.Server.Connection,
WebSocket4D.Transport.Indy;
type
TFrmMain = class(TForm)
MmoLog: TMemo;
EdtMessage: TEdit;
BtnConnect: TButton;
BtnSend: TButton;
BtnStartServer: TButton;
procedure BtnStartServerClick(Sender: TObject);
procedure BtnConnectClick(Sender: TObject);
procedure BtnSendClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FServer: IWebSocketServer;
FClient: IWebSocketClient;
procedure Log(const AText: string);
end;
var
FrmMain: TFrmMain;
implementation
{$R *.dfm}
procedure TFrmMain.Log(const AText: string);
begin
MmoLog.Lines.Add(AText); // must already be on the main thread
end;
procedure TFrmMain.BtnStartServerClick(Sender: TObject);
begin
FServer := TWebSocketServer4D.New
.Port(8080)
.OnConnect(procedure(const AConn: IWebSocketConnection)
var
LId: string;
begin
LId := AConn.Id; // capture by value — don't touch AConn from the UI thread
TThread.Queue(nil, procedure begin Log('client #' + LId + ' connected'); end);
end)
.OnMessage(procedure(const AConn: IWebSocketConnection; const AText: string)
var
LId, LText: string;
begin
LId := AConn.Id;
LText := AText;
AConn.Send('echo: ' + AText); // reply on the connection's own thread
TThread.Queue(nil, procedure begin Log(Format('#%s -> %s', [LId, LText])); end);
end)
.Start;
Log('server listening on ws://127.0.0.1:8080/');
end;
procedure TFrmMain.BtnConnectClick(Sender: TObject);
begin
FClient := TWebSocketClient4D.New
.URL('ws://127.0.0.1:8080/')
.SyncEvents(True) // default: events arrive on the main thread
.OnMessage(procedure(AText: string)
begin
Log(AText); // safe: already marshalled
end)
.OnClose(procedure(ACode: Integer; AReason: string)
begin
Log(Format('closed (code=%d)', [ACode]));
end)
.OnError(procedure(AError: string)
begin
Log('error: ' + AError);
end)
.Connect;
if FClient.IsOpen then
Log('connected');
end;
procedure TFrmMain.BtnSendClick(Sender: TObject);
begin
if Assigned(FClient) and FClient.IsOpen then
FClient.Send(EdtMessage.Text);
end;
procedure TFrmMain.FormDestroy(Sender: TObject);
begin
// Stop the server and disconnect the client before the form's fields go away,
// otherwise a handler can fire against a half-destroyed form.
if Assigned(FServer) then
FServer.Stop;
if Assigned(FClient) and FClient.IsOpen then
FClient.Disconnect(wsccNormal);
end;
end.Shutdown order matters. Stop the server (and disconnect the client) before freeing anything its handlers reference. Tearing the form down with connections still live is a use-after-free waiting to happen.
This VCL listing is an illustrative example, not a shipped sample — the repository's GUI samples are FMX (see below), and the same code works in VCL with the units swapped.
FMX example for Android/iOS
The FMX client is identical to the VCL one — the same builder, the same units. Only the UI framework changes. Keep .SyncEvents(True) so events land on the UI thread.
unit ClientForm;
interface
uses
System.SysUtils, System.Classes, FMX.Forms, FMX.Memo, FMX.Edit,
FMX.StdCtrls, FMX.Controls,
WebSocket4D.Core.Types,
WebSocket4D.Client,
WebSocket4D.Transport.Indy;
type
TFrmClient = class(TForm)
MmoLog: TMemo;
EdtUrl: TEdit;
EdtMessage: TEdit;
BtnConnect: TButton;
BtnSend: TButton;
procedure BtnConnectClick(Sender: TObject);
procedure BtnSendClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FClient: IWebSocketClient;
procedure Log(const AText: string);
end;
var
FrmClient: TFrmClient;
implementation
{$R *.fmx}
procedure TFrmClient.Log(const AText: string);
begin
MmoLog.Lines.Add(AText);
end;
procedure TFrmClient.BtnConnectClick(Sender: TObject);
begin
if Assigned(FClient) and FClient.IsOpen then
begin
FClient.Disconnect(wsccNormal);
BtnConnect.Text := 'Connect';
Exit;
end;
FClient := TWebSocketClient4D.New
.URL(EdtUrl.Text) // e.g. ws://192.168.0.10:8080/
.SyncEvents(True) // events on the UI thread
.OnMessage(procedure(AText: string) begin Log(AText); end)
.OnClose(procedure(ACode: Integer; AReason: string)
begin
Log(Format('closed (code=%d)', [ACode]));
BtnConnect.Text := 'Connect';
end)
.OnError(procedure(AError: string) begin Log('error: ' + AError); end)
.Connect;
if FClient.IsOpen then
begin
Log('connected');
BtnConnect.Text := 'Disconnect';
end;
end;
procedure TFrmClient.BtnSendClick(Sender: TObject);
begin
if Assigned(FClient) and FClient.IsOpen then
FClient.Send(EdtMessage.Text);
end;
procedure TFrmClient.FormDestroy(Sender: TObject);
begin
if Assigned(FClient) and FClient.IsOpen then
FClient.Disconnect(wsccNormal);
end;
end.Mobile checklist
| Item | Android | iOS |
|---|---|---|
| Server role | Not supported — mobile is client-only. | Not supported. |
localhost / 127.0.0.1 |
Points at the device itself, not your dev machine. Use the machine's LAN IP (ws://192.168.x.x:8080/); on an emulator, 10.0.2.2 maps to the host. |
Use the LAN IP. The iOS Simulator shares the host network, so 127.0.0.1 does work there. |
| Permissions | INTERNET permission — enabled by default in RAD Studio's Android manifest (Project ▸ Options ▸ Application ▸ Uses Permissions). Confirm it survived any manifest customization. |
None required for outbound connections. |
Cleartext ws:// |
Android 9+ blocks cleartext HTTP by default. Plain ws:// over a raw socket is generally unaffected, but if your network stack is hardened, allow it explicitly via a custom AndroidManifest.template.xml (android:usesCleartextTraffic="true") — or just use wss://. |
ATS targets URL-loading APIs, not raw sockets. Prefer wss:// for App Store submissions regardless. |
wss:// libraries |
Ship libcrypto.so + libssl.so (see TLS). |
Ship the equivalent OpenSSL libs. |
| Backgrounding | The OS suspends the app and the socket dies. Handle OnClose/OnError and reconnect on resume. |
Same. |
Reconnecting on resume
There is no automatic reconnection in v1 — wire it to the application lifecycle yourself:
// In your TApplicationEvents / TForm.OnActivate handler:
if Assigned(FClient) and not FClient.IsOpen then
FClient := TWebSocketClient4D.New
.URL(EdtUrl.Text)
.SyncEvents(True)
.OnMessage(procedure(AText: string) begin Log(AText); end)
.Connect;Runnable FMX samples (server for desktop, client for desktop + mobile, each with Echo and Chat tabs) live in samples/fmx/. Open samples/fmx/WebSocketSampleFMX.groupproj in the IDE and press F9.
Threading model
Getting this right avoids the majority of hard-to-reproduce bugs.
| Side | Where handlers run | What you must do |
|---|---|---|
Client, SyncEvents(True) (default) |
Main/UI thread, via TThread.Queue. |
Touch UI freely. Requires a running message loop — VCL/FMX apps have one. |
Client, SyncEvents(False) |
The client's receive thread. | Marshal yourself before touching UI. Use this in console apps and services. |
| Server, all handlers | The accepting connection's own thread, one thread per connection. Never marshalled. | Wrap every UI access in TThread.Queue. Capture values (AConn.Id, the text) into locals before queueing — don't dereference AConn from the UI thread. |
Two rules worth repeating, both of which caused real bugs during development:
IWebSocketConnection.Itemshas no lock. Only touch a connection'sItemsfrom that connection's own thread. To show a connection list in a UI, maintain your own collection on the UI thread and update it from insideTThread.Queue.- Shut down before you free.
IWebSocketServer.Stopin v1 stops accepting new connections but does not force-close existing ones. Stop the server and disconnect clients before tearing down forms or freeing objects the handlers reference.
Middleware
Middleware transforms messages in both directions without modifying the library. Each middleware receives a context and a Next callback; not calling Next short-circuits the chain.
uses
WebSocket4D.Pipeline;
type
TUpperCaseMiddleware = class(TWebSocketMiddlewareBase)
protected
procedure DoInvoke(const AContext: IWebSocketContext; const ANext: TWSNext); override;
end;
procedure TUpperCaseMiddleware.DoInvoke(const AContext: IWebSocketContext;
const ANext: TWSNext);
var
LMsg: TWSMessage;
begin
LMsg := AContext.GetMessage;
if (AContext.Direction = wsdOutbound) and (LMsg.Kind = mkText) then
AContext.SetMessage(TWSMessage.CreateText(UpperCase(LMsg.AsText)));
ANext(); // omit to stop the chain and drop the message
end;Register it with .Use(...) on either side:
TWebSocketServer4D.New
.Port(8080)
.Use(TUpperCaseMiddleware.Create)
.Start;AContext.Direction is wsdInbound for received messages and wsdOutbound for sent ones. AContext.Items gives you a per-message scratch dictionary. Typical uses: logging, metrics, authentication, envelope encoding/decoding, compression.
TLS (wss://) and OpenSSL by platform
Enable TLS on the server with a PEM certificate and key:
TWebSocketServer4D.New
.Port(8443)
.TLS('cert.pem', 'key.pem')
.Start;And point the client at wss://:
TWebSocketClient4D.New
.URL('wss://example.com/')
.VerifyCertificate(True) // default is False — enable in production
.Connect;wss:// uses Indy's TIdSSLIOHandlerSocketOpenSSL with PassThrough := False (immediate TLS) and TLS 1.2. The OpenSSL 1.0.2 libraries must be available at runtime — without them the error is Could not load SSL library:
| Platform | What to ship | Where |
|---|---|---|
| Windows | libeay32.dll + ssleay32.dll |
next to the .exe |
| Android | libcrypto.so + libssl.so (ABI arm64-v8a) |
Deployment with RemoteDir = library\lib\arm64-v8a\ (a Libraries node without RemoteDir falls back to assets\internal and won't load) |
| iOS / Linux | equivalent OpenSSL libs | platform-specific location; abstracted behind ITlsOptions |
Notes: OpenSSL 1.0.2 has no TLS 1.3 support — negotiate TLS 1.2 (modern servers/proxies such as Traefik accept it). A modern alternative under evaluation behind ITlsOptions is TaurusTLS (OpenSSL 1.1/3.x).
Terminating TLS at a reverse proxy (nginx, Traefik, HAProxy) is a fully supported and often simpler deployment: the proxy handles wss:// and forwards plain ws:// to WebSocket4D, so no OpenSSL libraries are needed alongside your server binary.
API reference
IWebSocketServer
| Member | Description |
|---|---|
procedure Stop |
Stops accepting new connections. Does not force-close existing ones in v1. |
procedure Broadcast(const AText: string) |
Sends text to every open connection. |
procedure Broadcast(const AData: TBytes) |
Sends binary to every open connection. |
function Clients: TArray<IWebSocketConnection> |
Thread-safe snapshot of the current connections. |
IWebSocketConnection
| Member | Description |
|---|---|
function Id: string |
Sequential per-server connection identifier. |
procedure Send(const AText: string) |
Sends a text frame to this connection only. |
procedure Send(const AData: TBytes) |
Sends a binary frame. |
procedure Ping(const AData: TBytes = nil) |
Sends a ping frame. |
procedure Close(ACode: TWSCloseCode = wsccNormal; const AReason: string = '') |
Closes with a code and reason. |
function IsOpen: Boolean |
Whether the connection is still usable. |
function Items: TDictionary<string, TValue> |
Per-connection session state. No lock — see Threading model. |
IWebSocketClient
| Member | Description |
|---|---|
procedure Send(const AText: string) |
Sends text. Raises EWebSocketError if not open. |
procedure Send(const AData: TBytes) |
Sends binary. |
procedure Ping(const AData: TBytes = nil) |
Sends a ping; the peer's pong is handled internally. |
procedure Disconnect(ACode: TWSCloseCode = wsccNormal; const AReason: string = '') |
Clean close handshake, then teardown. |
function IsOpen: Boolean |
Whether the connection is established. |
Close codes — TWSCloseCode
wsccNormal (1000), wsccGoingAway (1001), wsccProtocolError (1002), wsccUnsupportedData (1003), wsccNoStatus (1005), wsccAbnormal (1006), wsccInvalidPayload (1007), wsccPolicyViolation (1008), wsccTooBig (1009), wsccInternalError (1011).
Exceptions
| Class | Raised when |
|---|---|
EWebSocketError |
Base class. Sending while not connected, no transport factory registered. |
EWebSocketProtocol |
An RFC 6455 violation — the connection is closed with 1002. |
EWebSocketHandshake |
The server rejected the opening handshake. |
Samples
Runnable examples live under samples/ — see samples/README.md for full build and run instructions.
| Sample | Path | Scenario |
|---|---|---|
| Console echo server/client | samples/console/echo/ |
1:1 reply |
| Console chat server/client | samples/console/chat/ |
Broadcast 1:N with nicknames |
| FMX server (desktop) | samples/fmx/server/ |
Echo + Chat tabs |
| FMX client (desktop + mobile) | samples/fmx/client/ |
Echo + Chat tabs |
The samples reference the library through a relative path to ..\src, so they build straight from a clone with no installation step.
Tests
The DUnitX suite is split into three projects under tests/, built by the scripts in build/:
build\build-core-tests.bat
build\build-client-tests.bat
build\build-server-tests.batExit code 0 means green. The suites bind to 127.0.0.1 and use ports 28091–28097 (client) and 28100–28106 (server). TLS tests are marked [Ignore] pending an OpenSSL 1.0.2 test fixture.
License
MIT — Copyright (c) 2026 Adriano Santos.
🇧🇷 Esta documentação também está disponível em português: README-pt-BR.md

