Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
Чому 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, коли інтеграції, потоки даних і подальший розвиток мають коректно взаємодіяти.
Наступний крок
Якщо тема перетворюється на реальний проєкт, архітектуру, наявну інфраструктуру та експлуатацію слід розглядати разом на ранньому етапі.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та розгортання не відкладаються на пізніші етапи.
- Ви завчасно визначаєте, який підхід є економічно та операційно життєздатним.