מהנושא במגזין ליישום בפרויקט
דפי שירות וטכניים רלוונטיים למאמר
מדוע לקוח WebSocket של Delphi במציאות הוא יותר מ„Connect“
לקוח Delphi WebSocket Client מורכב בדקות: URL, Connect, SendText — מוכן. בתוכנה ארגונית מותאמת ובפתרונות קרובים לתהליכים הבעיות מתגלות בדרך‑כלל רק בזמן runtime: ה‑Reverse Proxy סוגר חיבורים אי‑פעילים, קווים סלולריים או VPN קצרים מבחינת timeout של NAT, תעודות (certificates) מתחלפות, ובסגירה התהליך יכול להיתקע כי Receive-Loop עדיין חסום. בנוסף: WebSocket הוא ערוץ ארוך־חיים ובעל מצב (stateful) — לכן חלים כללים שונים מאלו של HTTP/REST (Request/Response, קצר‑זמני).
בקטע קוד זה הכוונה אינה ל“Hello WebSocket“, אלא ל‑Client‑Wrapper מתאים לשימוש פרקטי הכולל:
- הפעלה/כיבוי נקיים (ללא תלישות בתהליך בעת Shutdown),
- Receive-Loop עם Cancellation (אות ביטול) במקום „Thread kill“,
- Reconnect עם Backoff (חיבור מחדש מבוקר),
- Heartbeat כדפוס יישומי (כי Ping/Pong לא תמיד זמין),
- Debug‑ ו‑Trace‑Hooks שמסייעים באמת במקרי תמיכה.
היישום מבוסס על System.Net.WebSockets (Delphi RTL; ממשק WebSocket‑Client עם TClientWebSocket). במקומות שבהם שכבת ה‑RTL הזו חסרה בגרסאות ישנות או מוגבלת מדי, פיינבק דרך ספרייה חיצונית (למשל ICS) הוא ברוב המקרים הגיוני — להלן מיקום הדבר בהקשר.
סקיצה ארכיטקטונית: Wrapper במקום קריאות WebSocket מפוזרות
טעויות נפוצות במערכות Delphi שהתפתחו עם הזמן: טפסי UI או מודולי שירות „מדברים ישירות עם WebSocket“ ואז יש בכל מקום טיימרים, ת’רים וטיפולי חריגות מפוזרים. עדיף לבנות רכיב ברור עם Events מוגדרים היטב ומכונה ממדית (state machine) קטנה.
מונחים בקצרה: Backoff פירושו זמני המתנה שגדלים בשלבים לאחר שגיאות (למשל 1s, 2s, 4s …), כדי לא להציף את השרת או הרשת. CancellationToken הוא אות ביטול מעולם .NET; ב‑Delphi אין דפוס זהה, אך ניתן לדמותו עם TEvent ודגל „StopRequested“. TThread.Queue מתזמן קוד לביצוע ב‑main thread (UI) מבלי לחסום את ה‑worker; Synchronize חוסם ועל נתיבי Shutdown הוא לעתים קרובות הגורם ל‑deadlocks.
Source‑Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message‑Dispatch
הקוד שלהלן בנוי במודע כרכיב תפעולי: מחלקה שניתן להשתמש בה ב‑VCL/FMX או בשירותים של Windows ו‑Windows‑ und Linux‑Services (תלות בגרסת/פלטפורמת Delphi). הליבה היא Worker‑Thread שמחזיק את Receive‑Loop ומדווח לאפליקציה דרך Events.
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; // Heartbeat של היישום: מועיל נגד ניתוקי Idle שנוצרים מאחורי פרוקסים
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.
// לכן זה רץ כאן בתוך ה-Worker.
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;
// קבלה: מבוססת פריימים, לכן משתמשים ב-StringBuilder לטיפול בפרגמנטציה.
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 הוא ברירת המחדל.
// נתונים בינאריים ניתן כאן לאחסן בבופר באופן דומה או להעביר ישירות.
Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, 'Server requested close');
Break;
end;
end;
// שינה קצרה, כדי לחסוך 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 מרים Event. ה-Worker מסיים לולאות בנקודות מוגדרות. זה מקטין תלויות בעת סגירה ומונע דליפות משאבים ב‑socket‑stack.
- Queue במקום Synchronize: Logging ואירועים נשלחים דרך TThread.Queue ל‑Mainthread. זה חשוב אם Stop/Shutdown מגיע מ‑UI או מ‑Service‑Control‑Handlern. Synchronize עלול לחסום אם ה‑Mainthread נמצא במצב המתנה.
- מתחשב בפראגמנטציה: טקסט WebSocket יכול להגיע מפוצל ב‑frames. לכן משתמשים ב‑TStringBuilder ובבדיקה של EndOfMessage.
- Heartbeat כפרוטוקול אפליקטיבי: מערכות רבות „מתות“ בגלל Idle‑Timeouts (Load Balancer, nginx, Cloud WAF). טקסט „ping“ קל משקל כתמרור פעולה לעתים יעיל יותר מאשר ההסתמכות על „TCP keepalive“ או על ממשק Ping/Pong שלא קיים בכל מקום.
תנאים ופתקי טיפוס בתפעול
1) DNS, TLS und Proxy: Connect עלול להיחסם
TClientWebSocket.Connect הוא סינכרוני. בהתאם ליישוב DNS, TLS‑Handshake, בדיקת תעודה או סביבת Proxy זה יכול לארוך מספר שניות. הקוד מניח זאת במכוון ומריץ את הבקשה בתוך Worker. אם אתם צריכים Timeouts קשיחים נוספים, יש לבדוק ברמת ה‑API האם גרסת Delphi שלכם מספקת אפשרויות Timeout, או לעטוף את Connect ב‑Thread נפרד ולנתק באמצעות לוגיקת תהליך. חשוב: „לבטל“ כאן בדרך כלל אומר „לסמן את החיבור כפגום ולהרים Worker חדש“, לא „להרוג מיד פעולה על Socket“.
2) Idle‑Timeouts: למה Heartbeat לעתים קריטי
ברשתות ארגוניות WebSocket לעתים מסתיים מאחורי Reverse Proxy (nginx, IIS ARR) או Load Balancer. רכיבים רבים סוגרים חיבורים אם לא זורמים נתונים לתקופה ארוכה. TCP‑Keepalive לא תמיד מוגדר לפרקי זמן קצרים מספיק (ובתנאי Windows לעתים זה דקות ולא שניות). לכן Heartbeat ברמת היישום מהווה פתרון יציב. שימו לב שלשרת וללקוח צריך להיות אותו מושג (למשל „ping“/“pong“ כטקסט או JSON).
3) Threading und UI: אירועים חייבים להישאר מופרדים
אם עיבוד OnText כבד (JSON‑Parsing, גישות DB עם BDE-Ablosung mit nativer Anbindung, עדכוני UI), אין להריץ אותו כך שיחסום את ה‑Mainthread. ה‑Wrapper מעביר רק את ההודעה. תבנית טיפוסית היא: OnText שמניח את ה‑Payload ל‑Queue (למשל TThreadedQueue<string>), Worker נפרד מעבד עם Backpressure (כלומר אורך Queue מוגבל). זה מונע שה‑UI יקפא או שהקבלה תתערער בזמן עומס פתאומי.
Debugging: מה כדאי לוג כשזה „לפעמים“ נופל
WebSockets ידועים ב‑“רצים ימים ואז מפסיקים“. בלי Logging קשה מאוד לאתר. נקודות לוג משמעותיות:
- חותמות זמן (UTC), URL, ושינויים במצב (connecting/open/closed).
- Close‑Reason, אם זמין (Server מבקש Close לעומת תקלה ברשת).
- שגיאות שליחת Heartbeat וחריגות קבלה כולל סוג ה‑Exception.
- אופציונלי: גדלים של הודעות שהתקבלו (לא התוכן), כדי לזהות התפוצצות נתונים.
אם אתם מבצעים Terminierung על TLS: בדקו גם האם שינויים בתעודה (פגות, Issuer חדש) מתזמנים עם שגיאות. בסביבות מחוזקות גם פרוקסי ו‑DPI‑Boxen (Deep Packet Inspection) הם מועמדים לבעיה.
מגוון אפשרויות: מתי System.Net.WebSockets מספיק – ומתי לא
System.Net.WebSockets מספק ברוב מקרים של אינטגרציה, במיוחד כשמדובר בטקסט/JSON, בעומס בינוני ובאסטרטגיות חיבור מחדש ברורות. גבולותיו ניכרים בהתאם לגרסת Delphi וליעד הפלטפורמה:
- חוסר/תמיכה מוגבלת ב‑Ping/Pong: במקרה כזה, דפוס יציב נשאר בדיקת חיים של האפליקציה.
- חוסר Timeouts/ביטול בתהליכי Connect/Receive: במקרה כזה עליכם לבנות את הארכיטקטורה כך שעובד תקוע יישאר מבודד והיישום ייסגר בצורה נקייה (למשל בעזרת watchdog לתהליך או מופעי Worker נפרדים).
- עומס גבוה או זרמי בינארי: במקרה כזה כדאי לממש קונספט חזק יותר של framing/buffering (למשל ring buffer, אירוע בינארי נפרד, Message-Assembler עם מגבלות).
למצבים ישנים (דורות Delphi ישנים יותר, דרישות TLS/Proxy מאוד ספציפיות) ספריות כמו ICS פרגמטיות בפרויקטים מסוימים. החשוב פחות איזו ספריה, ויותר שתטפלו ב‑Shutdown, Reconnect ו‑Observability (לוגים/מדדים) כנושאים ברמת חשיבות ראשונה.
מסקנה: לקוח WebSocket של Delphi הוא רכיב תפעולי – עם גבולות ברורים
WebSocket מתאים היטב לאירועי push, סטטוס בזמן אמת, דיווחי מכונות או תהליכים ולערוץ חזרה לפורטלים ושירותים. ה‑wrapper המוצג מתמקד בנקודות שלעיתים מהוות את ההבדל בפתרונות ארגוניים דיגיטליים: חיבור מחדש מבוקר, בדיקת חיים כנגד Idle‑Timeouts, עיבוד טקסט עמיד לפירגמנטים ונתיב עצירה שאינו נתקע בעת פריסה או עדכון.
מגבילים בשימוש נשארים: אם אתם זקוקים לערבויות קשיחות לניתוק Connect/Receive במסגרת חלונות זמן קצרים מאוד או מפעילים קצבי נתונים גבוהים במיוחד, תצטרכו להעמיק ב‑Timeouts, בפרטיות הפלטפורמה ובמקרים מסוימים בסטקים חלופיים. עבור רוב תרחישי אינטגרציה ומודרניזציה, לקוח מבודר היטב ומלווה בלוגים מהווה בסיס איתן שניתן לשלב במערכות Delphi קיימות.
אם אתם רוצים להתאים רכיב כזה לארכיטקטורה קיימת (למשל ארכיטקטורת Layer-3 עם שכבות שירות ו‑UI ברורות) או לדבג ניתוקים ספורים בתנאי אמת, נוכל למקד את המיון יחד אתכם: פנו אלינו.
בהקשר מקצועי גם Heartbeat ו‑Ping/Pong ממלאים תפקיד משמעותי כאשר אינטגרציות, זרמי נתונים ופיתוח המשכי צריכים לפעול בהרמוניה.
השלב הבא
כאשר הנושא הופך לפרויקט ממשי, יש לבחון כבר בשלב מוקדם את הארכיטקטורה, המצב הקיים והתפעול יחד.
אנו תומכים לא רק בשאלות נקודתיות, אלא גם כשמקטעי קוד מקור, נושאי Legacy או רעיונות פורטל אמורים להפוך לפרויקט ארגוני מהימן ועמיד.
- המצב הקיים, תמונת היעד והסיכונים הטכניים מוערכים יחד.
- REST, גישה לנתונים, פורטלים ו-Rollout לא יידחו כתוצאות מאוחרות.
- אתם מזהים מוקדם איזה נתיב בר-קיימא מבחינה כלכלית ותפעולית.