Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

WebSocket4D

Biblioteca cliente + servidor WebSocket em Delphi (Indy 10, RFC 6455), sem dependência do DMVCFramework. MIT.

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

WebSocket4D

🇧🇷 Leia em Português

WebSocket client + server library for Delphi (Object Pascal), built on top of Indy 10, following RFC 6455.

pascal
TWebSocketServer4D.New
  .Port(8080)
  .OnMessage(procedure(const AConn: IWebSocketConnection; const AText: string)
    begin
      AConn.Send('echo: ' + AText);
    end)
  .Start;

Table of contents


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-deflate in 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)

  1. Get the source. Clone the repository, or download a release ZIP and extract it, somewhere accessible to your project:

    bash
    git clone https://github.com/adrianosantostreina/WebSocket4D.git

    A common layout is a shared libs folder next to your projects:

    pascal
    C:\Dev\
    ?????? libs\
    ???  ?????? WebSocket4D\
    ???     ?????? src\           <-- this is the path you will add
    ?????? MyApp\
       ?????? MyApp.dproj
  2. Add src to the search path. In the RAD Studio IDE:

    Project ▸ Options ▸ Building ▸ Delphi Compiler ▸ Search path

    Add the full path to the src folder, 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

bash
boss install github.com/adrianosantostreina/WebSocket4D

Or pinned to a specific version:

bash
boss install github.com/adrianosantostreina/WebSocket4D:1.0.1

Note: the boss.json at the repository root describes the library only. The applications under samples/ are not BOSS projects — they reference ..\..\src directly (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:

pascal
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 — required

One unit matters for transport registration, and forgetting it is the most common first error:

WebSocket4D.Transport.Indy must be in your uses — for a client and for a server. Omitting it raises EWebSocketError: No ITransportFactory available on .Connect or .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.

pascal
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

pascal
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:

pascal
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:

pascal
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:

pascal
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:

pascal
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:

pascal
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's Items dictionary has no internal lock. It is safe as long as you only touch a connection's Items from that connection's own event handlers (which all run on that connection's thread). Reading another connection's Items from 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.

pascal
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.

pascal
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.

pascal
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:

pascal
// 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:

  1. IWebSocketConnection.Items has no lock. Only touch a connection's Items from 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 inside TThread.Queue.
  2. Shut down before you free. IWebSocketServer.Stop in 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.

pascal
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:

pascal
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:

pascal
TWebSocketServer4D.New
  .Port(8443)
  .TLS('cert.pem', 'key.pem')
  .Start;

And point the client at wss://:

pascal
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/:

bat
build\build-core-tests.bat
build\build-client-tests.bat
build\build-server-tests.bat

Exit 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