Net-Base Журнал

01.06.2026

Delphi WebSocket-клієнт: надійне підключення, коректне завершення, надійне налагодження

Клієнт WebSocket Delphi швидко «якось підключається» – але в експлуатації вирішальними є відновлення з'єднання, heartbeat‑сигнали, коректне завершення та налагоджуваність. З практичною обгорткою на базі System.Net.WebSockets (з резервним механізмом) та фрагментом вихідного коду для мультипотоковості і...

01.06.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

Чому ein Delphi WebSocket Client на практиці більше, ніж «Connect»

Одним Delphi WebSocket Client збирається за хвилини: URL, Connect, SendText, і готово. У індивідуальному корпоративному програмному забезпеченні та в процесно орієнтованих рішеннях проблема зазвичай виникає в експлуатації: Reverse Proxy розриває неактивні з’єднання, мобільні або VPN-канали мають короткі NAT-таймаути, сертифікати змінюються, а при завершенні процес зависає, бо Receive-Loop ще блокує. До того ж: WebSocket — це довгоживучий, станозалежний канал, тому діють інші правила, ніж для класичного HTTP/REST (Request/Response, короткоживучий).

У цьому фрагменті коду мова не про «Hello WebSocket», а про практично придатний клієнтський обгорток з такими властивостями:

  • чистий старт/стоп (без зависань під час завершення роботи),
  • Receive-Loop з Cancellation (сигнал відміни) замість «Thread kill»,
  • Reconnect з Backoff (контрольоване повторне під’єднання),
  • Heartbeat як патерн застосування (оскільки Ping/Pong не завжди доступні),
  • Debug- і Trace-Hooks, які реально допомагають у випадках супорту.

Реалізація базується на System.Net.WebSockets (Delphi RTL; WebSocket-клієнтська API з TClientWebSocket). Там, де цей RTL-шар у старіших версіях недоступний або надто обмежений, часто має сенс fallback через бібліотеку (наприклад, ICS) — нижче наведено відповідну класифікацію/пояснення.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Поширена помилка у вирослих Delphi-додатках: UI-форми або сервісні модулі «розмовляють безпосередньо з WebSocket» і в результаті по всьому коду розкидані таймери, потоки та обробки виключень. Краще мати чіткий компонент з добре визначеними подіями і невеликою машиною станів.

Терміни коротко пояснені: Backoff означає інтервал очікування, який після помилок поступово зростає (наприклад 1s, 2s, 4s …), щоб не затоплювати сервер і мережу. CancellationToken — це сигнал відміни зі світу .NET; в Delphi немає ідентичного патерну, але його можна змоделювати за допомогою TEvent і прапорця «StopRequested». TThread.Queue планує код для виконання в головному потоці (UI), не блокуючи воркер; Synchronize блокує і в шляхах завершення часто стає причиною взаємних блокувань (deadlocks).

Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch

Наведений код свідомо побудований як «експлуатаційний компонент»: клас, який можна використовувати у VCL/FMX або в Windows- та Windows- та Linux-Services (залежно від версії/платформи Delphi). Ядро — воркер-потік, який тримає Receive-Loop і повідомляє додаток через події.

unit Net.WebSocketClientEx;

interface

uses
System.SysUtils,
System.Classes,
System.SyncObjs,
System.Generics.Collections,
System.Net.URLClient,
System.Net.WebSockets;

type
TWsLogLevel = (llDebug, llInfo, llWarn, llError);

TWsLogEvent = reference to procedure(Level: TWsLogLevel; const Msg: string);
TWsTextEvent = reference to procedure(const Text: string);
TWsStateEvent = reference to procedure(const State: string);

TDelphiWebSocketClient = class
private
FUrl: string;
FOnLog: TWsLogEvent;
FOnText: TWsTextEvent;
FOnState: TWsStateEvent;

FStopEvent: TEvent;
FWorker: TThread;

FMinBackoffMs: Integer;
FMaxBackoffMs: Integer;
FHeartbeatSec: Integer;

procedure Log(Level: TWsLogLevel; const Msg: string);
procedure State(const S: string);

procedure Run;
function NextBackoffMs(const Prev: Integer): Integer;
function NowUtcStr: string;
public
constructor Create(const AUrl: string);
destructor Destroy; override;

procedure Start;
procedure Stop(TimeoutMs: Cardinal = 5000);

property OnLog: TWsLogEvent read FOnLog write FOnLog;
property OnText: TWsTextEvent read FOnText write FOnText;
property OnState: TWsStateEvent read FOnState write FOnState;

property MinBackoffMs: Integer read FMinBackoffMs write FMinBackoffMs;
property MaxBackoffMs: Integer read FMaxBackoffMs write FMaxBackoffMs;
property HeartbeatSec: Integer read FHeartbeatSec write FHeartbeatSec;
end;

implementation

type
TBytesBuffer = record
Data: TBytes;
Len: Integer;
end;

{ TDelphiWebSocketClient }

constructor TDelphiWebSocketClient.Create(const AUrl: string);
begin
inherited Create;
FUrl := AUrl;
FStopEvent := TEvent.Create(nil, True, False, “);

FMinBackoffMs := 500;
FMaxBackoffMs := 15000;
FHeartbeatSec := 20; // App-Heartbeat: корисний для запобігання таймаутам бездіяльності за проксі
end;

destructor TDelphiWebSocketClient.Destroy;
begin
Stop;
FStopEvent.Free;
inherited;
end;

procedure TDelphiWebSocketClient.Start;
begin
if Assigned(FWorker) then
Exit;

FStopEvent.ResetEvent;
FWorker := TThread.CreateAnonymousThread(
procedure
begin
Run;
end);
FWorker.FreeOnTerminate := False;
FWorker.Start;
end;

procedure TDelphiWebSocketClient.Stop(TimeoutMs: Cardinal);
var
W: TThread;
begin
FStopEvent.SetEvent;

W := FWorker;
if Assigned(W) then
begin
if W.WaitFor(TimeoutMs) = wrTimeout then
Log(llWarn, ‚Зупинка: Worker не реагує в межах таймауту; можливе блокування в мережевому стеку‘);
FreeAndNil(FWorker);
end;
end;

procedure TDelphiWebSocketClient.Log(Level: TWsLogLevel; const Msg: string);
begin
if Assigned(FOnLog) then
TThread.Queue(nil,
procedure
begin
FOnLog(Level, NowUtcStr + ‚ ‚ + Msg);
end);
end;

procedure TDelphiWebSocketClient.State(const S: string);
begin
if Assigned(FOnState) then
TThread.Queue(nil,
procedure
begin
FOnState(S);
end);
end;

function TDelphiWebSocketClient.NowUtcStr: string;
begin
Result := FormatDateTime(‚yyyy-mm-dd“T“hh:nn:ss.zzz“Z“‚, TTimeZone.Local.ToUniversalTime(Now));
end;

function TDelphiWebSocketClient.NextBackoffMs(const Prev: Integer): Integer;
var
N: Integer;
begin
if Prev <= 0 then
Exit(FMinBackoffMs);

N := Prev * 2;
if N < FMinBackoffMs then N := FMinBackoffMs;
if N > FMaxBackoffMs then N := FMaxBackoffMs;
Result := N;
end;

procedure TDelphiWebSocketClient.Run;
var
WS: TClientWebSocket;
Backoff: Integer;
LastHeartbeat: UInt64;
Msg: string;
Buf: TBytes;
Received: TWebSocketReceiveResult;
SB: TStringBuilder;
WaitRes: TWaitResult;

function StopRequested: Boolean;
begin
Result := (FStopEvent.WaitFor(0) = wrSignaled);
end;

procedure SafeClose;
begin
try
if WS.State = TWebSocketState.Open then
WS.Close(TWebSocketCloseStatus.NormalClosure, ‚client shutdown‘);
except
on E: Exception do
Log(llDebug, ‚Close: ‚ + E.ClassName + ‚: ‚ + E.Message);
end;
end;

begin
Backoff := 0;
LastHeartbeat := 0;

while not StopRequested do
begin
WS := TClientWebSocket.Create;
try
State(‚connecting‘);
Log(llInfo, ‚Connect to ‚ + FUrl);

try
// Примітка: TClientWebSocket.Connect є синхронним і може блокуватися в залежності від DNS/TLS.
// Тому цей код виконується в робочому потоці.
WS.Connect(FUrl);
except
on E: Exception do
begin
State(‚connect_failed‘);
Log(llWarn, ‚Connect failed: ‚ + E.ClassName + ‚: ‚ + E.Message);
Backoff := NextBackoffMs(Backoff);
WaitRes := FStopEvent.WaitFor(Backoff);
if WaitRes = wrSignaled then Break;
Continue;
end;
end;

State(‚open‘);
Backoff := 0;

SetLength(Buf, 16 * 1024);
SB := TStringBuilder.Create;
try
while (WS.State = TWebSocketState.Open) and (not StopRequested) do
begin
// Heartbeat як повідомлення додатка, оскільки Ping/Pong не у всіх версіях Delphi коректно доступні.
if (FHeartbeatSec > 0) then
begin
if (LastHeartbeat = 0) or (TThread.GetTickCount64 – LastHeartbeat >= UInt64(FHeartbeatSec) * 1000) then
begin
try
WS.Send(‚ping‘);
LastHeartbeat := TThread.GetTickCount64;
Log(llDebug, ‚Heartbeat ping sent‘);
except
on E: Exception do
begin
Log(llWarn, ‚Heartbeat send failed: ‚ + E.Message);
Break;
end;
end;
end;
end;

// Отримання: на основі фреймів, тому TStringBuilder використовується для обробки фрагментації.
try
Received := WS.Receive(Buf);
except
on E: Exception do
begin
Log(llWarn, ‚Receive failed: ‚ + E.ClassName + ‚: ‚ + E.Message);
Break;
end;
end;

case Received.Kind of
TWebSocketMessageKind.Text:
begin
SB.Append(TEncoding.UTF8.GetString(Buf, 0, Received.BytesReceived));
if Received.EndOfMessage then
begin
Msg := SB.ToString;
SB.Clear;

if Assigned(FOnText) then
TThread.Queue(nil,
procedure
begin
FOnText(Msg);
end);
end;
end;

TWebSocketMessageKind.Binary:
begin
// У багатьох бізнес-протоколах стандартом є текст/JSON.
// Binary тут можна подібним чином буферизувати або передавати безпосередньо.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

TWebSocketMessageKind.Close:
begin
Log(llInfo, ‚Server requested close‘);
Break;
end;
end;

// Коротка пауза (sleep), щоб знизити навантаження на CPU при дуже швидкому циклі.
// Не надто велика, інакше погіршиться затримка.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Reconnect nach sauberem Close oder nach Fehlern
Backoff := NextBackoffMs(Backoff);
Log(llInfo, ‚Reconnect in ‚ + Backoff.ToString + ‚ ms‘);
WaitRes := FStopEvent.WaitFor(Backoff);
if WaitRes = wrSignaled then
Break;
end;

State(’stopped‘);
Log(llInfo, ‚Worker stopped‘);
end;

end.

Що код навмисно робить «інакше», ніж типові приклади

  • Stop без насильства: Замість того щоб вбивати Threads, Stop встановлює подію. Der Worker завершує цикли в визначених місцях. Це зменшує зависання при завершенні та запобігає витокам ресурсів у Socket-Stack.
  • Queue statt Synchronize: Логування та події потрапляють через TThread.Queue у головний потік. Das ist wichtig, wenn Stop/Shutdown aus dem UI oder aus Service-Control-Handlern kommt. Synchronize може блокувати, якщо головний потік в цей час очікує.
  • Fragmentierung berücksichtigt: Текст WebSocket може надходити фрагментований у фреймах. Тому використовується TStringBuilder та перевірка EndOfMessage.
  • Heartbeat als App-Protokoll: У багатьох конфігураціях з’єднання розриваються через Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Легковаговий «ping»-текст як операційний важіль часто ефективніший за надію на «TCP keepalive» або на API Ping/Pong, доступне не скрізь.

Граничні умови та підводні камені в експлуатації

1) DNS, TLS та проксі: Connect може блокуватися

TClientWebSocket.Connect виконується синхронно. Залежно від розв’язання DNS, TLS-хендшейку, перевірки сертифікатів або проксі-середовища це може займати декілька секунд. Код свідомо поміщає це в Worker. Якщо вам потрібні жорсткі таймаути, перевірте на рівні API, чи ваша Delphi-версія надає опції таймауту, або оберніть Connect у окремий потік і розривайте його через логіку процесу. Важливо: «скасувати» тут зазвичай означає «позначити з’єднання як пошкоджене та пересоздати Worker», а не «негайно вбити операцію сокета».

2) Idle-таймаути: чому Heartbeat часто є обов’язковим

У корпоративних мережах WebSocket часто терміновано за Reverse Proxy (nginx, IIS ARR) або Load Balancer. Багато таких компонентів закривають з’єднання, якщо тривалий час не надходять дані. TCP-Keepalive не завжди налаштований на достатньо короткий інтервал (і під Windows часто радше в хвилинах, ніж у секундах). Тому Heartbeat на рівні застосунку є стабільним обходом. Переконайтеся, що сервер і клієнт мають однакову практику (наприклад «ping»/«pong» як текст або JSON).

3) Threading und UI: Ereignisse müssen entkoppelt bleiben

Якщо обробка OnText важка (JSON-Parsing, доступи до БД із BDE-заміною з нативною інтеграцією, оновлення UI), вона не повинна блокувати все в головному потоці. Враппер передає лише повідомлення. Типовий патерн: OnText кладе payload у чергу (наприклад, TThreadedQueue<string>), окремий Worker обробляє її з backpressure (тобто обмеженою довжиною черги). Це запобігає тому, щоб при пікових навантаженнях UI не зависав або прийом не виходив з ритму.

Налагодження: що варто логувати, коли воно «іноді» обривається

WebSockets відомі тим, що «працюють днями, а потім — ні». Без логування це важко локалізувати. Розумні точки логування:

  • Мітки часу (UTC), URL і переходи станів (connecting/open/closed).
  • Close-Reason, якщо доступно (сервер ініціює Close проти помилки мережі).
  • Помилки відправки Heartbeat та виключення при прийомі, включно з типом Exception.
  • Опціонально: розміри отриманих повідомлень (не їх вміст), щоб виявити вибух даних.

Якщо ви термінуєте через TLS: додатково перевірте, чи корелюють помилки зі змінами сертифіката (закінчення терміну, новий Issuer) у часі. У посиленому оточенні також кандидатами на проблеми є проксі- та DPI-пристрої (Deep Packet Inspection).

Варіанти: коли System.Net.WebSockets достатній – а коли ні

System.Net.WebSockets підходить для багатьох випадків інтеграції, особливо коли йдеться про текст/JSON, помірне навантаження та чіткі стратегії повторного підключення. Межі проявляються залежно від версії Delphi та цільової платформи:

  • Відсутня/обмежена підтримка Ping/Pong: Тоді App-Heartbeat залишається надійною практикою.
  • Відсутність таймаутів/скасування при Connect/Receive: Тоді потрібно будувати архітектуру так, щоб завислий Worker залишався ізольованим, а застосунок усе одно коректно завершувався (наприклад, через Prozesswatchdog або окремі Worker-Instanzen).
  • Високе навантаження або бінарні потоки: Тоді має сенс більш потужна концепція фреймінгу/буферизації (наприклад, ring buffer, окреме Binary-Event, Message-Assembler з обмеженнями).

Для legacy-ситуацій (старіші Delphi-покоління, дуже специфічні вимоги TLS/Proxy) бібліотеки на кшталт ICS у деяких проєктах виявляються прагматичнішими. Важливіше не «яка Library», а те, щоб ви розглядали Shutdown, Reconnect і Observability (Logs/Metriken) як першорядні теми.

Висновок: Delphi WebSocket Client — елемент експлуатації з чіткими межами

WebSocket добре підходить для Push-Events, живого статусу, повідомлень від машин або процесів і як зворотний канал для порталів та сервісів. Показаний Wrapper зосереджений на моментах, які в цифрових корпоративних рішеннях часто визначають різницю: контрольований Reconnect, Heartbeat проти Idle-Timeouts, фрагментобезпечна текстова обробка і Stop-Pfad, який при розгортанні або оновленні не зависає.

Межі застосування залишаються: якщо вам потрібні жорсткі гарантії для переривання Connect/Receive у дуже стиснутих часових рамках або ви оперуєте надзвичайно високими швидкостями передачі даних, доведеться заглибитись у таймаути, особливості платформи та, за потреби, альтернативні стеки. Для більшості сценаріїв інтеграції та модернізації чисто інкапсульований, добре логований клієнт, як наведений вище, є надійною базою, яку можна інтегрувати в уже наявні Delphi-системи.

Якщо вам потрібно впровадити такий компонент у наявну архітектуру (наприклад, Layer-3 архітектуру з чіткими шаром сервісів і UI) або налагоджувати спорадичні роз’єднання в реальних умовах, ви можете цілеспрямовано узгодити це з нами: зв’яжіться з нами.

У професійному контексті також важливу роль відіграють Heartbeat Ping/Pong, коли інтеграції, потоки даних і подальший розвиток мають коректно взаємодіяти.

Обговорити проєкт або завдання модернізації з Net-Base.

Наступний крок

Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.

Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
  • Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та електронна пошта доступні негайно. Для Instagram ми готуємо посилання та короткий текст безпосередньо.

Електронна пошта

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.