Dergi konusundan proje pratiğine
İçeriğe Uygun Hizmet ve Teknik Sayfalar
Neden bir Delphi WebSocket Client pratikte sadece „Connect“ değildir
Bir Delphi WebSocket Client dakikalar içinde kurulabilir: URL, Connect, SendText, tamam. Ancak özel kurumsal yazılımlar ve süreçe yakın çözümlerde asıl sorun genellikle işletmede ortaya çıkar: Reverse Proxy boşta kalan bağlantıları keser, mobil veya VPN hatlarında kısa NAT zaman aşımı vardır, sertifikalar değişir ve kapanışta süreç, bir Receive-Loop hâlâ bloklandığı için takılabilir. Buna ek olarak: Bir WebSocket uzun ömürlü, durum bilgisi taşıyan bir kanaldır — bu nedenle klasik HTTP/REST (istek/yanıt, kısa ömürlü) ile aynı kurallar geçerli değildir.
Bu kaynak parçacığı „Hello WebSocket“ ile ilgili değil; pratik kullanım için hazırlanmış bir istemci sarmalayıcısından (client-wrapper) söz ediyor:
- temiz başlatma/durdurma (kapanışta kilitlenme olmadan),
- Receive-Loop ile Cancellation (iptal sinyali) — „Thread kill“ yerine,
- Backoff ile yeniden bağlanma (kontrollü yeniden bağlantı),
- Heartbeat uygulama deseni (çünkü Ping/Pong her ortamda mevcut değildir),
- destek vakalarında gerçekten yardımcı olan Debug ve Trace hook’ları.
Uygulama System.Net.WebSockets üzerine kuruludur (Delphi RTL; TClientWebSocket ile WebSocket istemci API’si). Bu RTL katmanı eski sürümlerde mevcut değilse veya çok kısıtlıysa, bir kütüphane üzerinden (ör. ICS) fallback genellikle mantıklıdır — aşağıda bununla ilgili bir değerlendirme bulunuyor.
Mimari taslak: dağıtılmış WebSocket çağrıları yerine bir sarmalayıcı
Olgunlaşmış Delphi uygulamalarında sık yapılan bir hata: UI formları veya servis modülleri „doğrudan WebSocket ile konuşur“ ve sonuçta her yerde zamanlayıcılar, iş parçacıkları ve istisna işlemleri dağılım gösterir. Daha iyi olan, iyi tanımlanmış Event’ler ve küçük bir durum makinesi içeren net bir bileşendir.
Terimleri kısaca sınıflandıralım: Backoff, hatalardan sonra kademeli olarak artan bir bekleme süresini ifade eder (örn. 1s, 2s, 4s …); sunucu ve ağı aşırı yüklememek için kullanılır. CancellationToken .NET dünyasından bir iptal sinyalidir; Delphi içinde birebir aynı desen yoktur, ancak bunu TEvent ve bir „StopRequested“ bayrağı ile taklit edebiliriz. TThread.Queue, Worker’ı engellemeden ana iş parçacığında (UI) kod çalıştırılmasını planlar; Synchronize bloklar ve kapanış yollarında sıkça deadlock’ların nedeni olur.
Kaynak parçacığı: Delphi WebSocket Client ile Stop, Reconnect ve Message-Dispatch
Aşağıdaki kod kasıtlı olarak bir „işletme bileşeni“ (Betriebs-Baustein) olarak yapılandırılmıştır: VCL/FMX içinde veya bir Windows ve Windows- ve Linux-servislerde (Delphi sürümüne/platforuna bağlı olarak) benzer şekilde kullanılabilecek bir sınıf. Çekirdek, Receive-Loop’u çalıştıran ve Event’ler aracılığıyla uygulamaya bildiren bir Worker-Thread’dir.
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: proxy arkasındaki boşta kalma zaman aşımına karşı faydalı
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, ‚Durdur: Worker zaman aşımı içinde yanıt vermiyor; ağ yığınında olası blokaj‘);
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
// Uyarı: TClientWebSocket.Connect senkron çalışır ve DNS/TLS’e bağlı olarak bloklayabilir.
// Bu nedenle burada Worker içinde çalıştırılıyor.
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 uygulama mesajı olarak, çünkü Ping/Pong her Delphi-sürümünde düzgün şekilde erişilebilir olmayabilir.
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;
// Alım: Frame tabanlı, bu yüzden parçalanma için StringBuilder kullanılıyor.
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
// Birçok iş protokolünde Text/JSON varsayılandır.
// Binary burada benzer şekilde tamponlanabilir veya doğrudan iletilebilir.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, ‚Server requested close‘);
Break;
end;
end;
// Kısa uyku, çok hızlı döngülerde CPU’yu korumak için.
// Çok büyük olmamalı, aksi halde gecikme artar.
TThread.Sleep(1);
end;
finally
SB.Free;
end;
SafeClose;
State(‚closed‘);
finally
WS.Free;
end;
if StopRequested then
Break;
// Temiz kapanış veya hatalardan sonra yeniden bağlanma
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.
Kodun kasıtlı olarak tipik örneklerden farklı yaptığı hususlar
- Zorla sonlandırma yok: İş parçacıklarını öldürmek yerine Stop bir Event tetikler. Der Worker döngüleri tanımlı noktalarda sonlandırır. Bu, kapatma sırasında takılmaları azaltır ve socket yığınında kaynak sızıntılarını önler.
- Synchronize yerine Queue: Loglama ve event’ler TThread.Queue ile Mainthread’e gönderilir. Bu, Stop/Shutdown UI’dan veya Service-Control handler’larından geliyorsa önemlidir. Synchronize Mainthread o anda bekliyorsa bloklayabilir.
- Fragmentasyon dikkate alındı: WebSocket metni frame’lere bölünmüş olarak gelebilir. Bu yüzden TStringBuilder ve EndOfMessage kontrolleri kullanılır.
- Heartbeat uygulama protokolü olarak: Birçok kurulum Idle-Timeouts (Load Balancer, nginx, Cloud WAF) nedeniyle bağlantıların sonlanması ile karşılaşır. Hafif bir “ping” metni, işletme açısından genellikle “TCP keepalive” umudundan veya her yerde mevcut olmayan bir Ping/Pong-API’sinden daha etkilidir.
İşletmede sınır koşulları ve dikkat edilmesi gereken tuzaklar
1) DNS, TLS und Proxy: Connect engellenebilir
TClientWebSocket.Connect eşzamanlıdır. DNS çözümlemesi, TLS el sıkışması, sertifika doğrulaması veya proxy ortamına bağlı olarak bu birkaç saniye sürebilir. Kod bunu bilerek bir Worker içine koyar. Eğer ek sert timeout’lara ihtiyacınız varsa, API düzeyinde Delphi-sürümünüzün timeout seçenekleri sunup sunmadığını kontrol etmelisiniz veya Connect’i ayrı bir iş parçacığına kapsülleyip süreç mantığı ile iptal etmelisiniz. Önemli: Buradaki “iptal” genellikle “bağlantıyı bozuk olarak işaretlemek ve Worker’ı yeniden başlatmak” anlamına gelir, “socket işlemini hemen sonlandırmak” değil.
2) Idle-Timeouts: warum Heartbeat häufig Pflicht ist
Kurumsal ağlarda bir WebSocket genellikle bir Reverse Proxy (nginx, IIS ARR) veya bir Load Balancer arkasında sonlandırılır. Bu bileşenlerin çoğu, uzun süre veri akışı olmadığında bağlantıları kapatır. TCP-Keepalive her zaman yeterince kısa yapılandırılmış değildir (ve Windows altında genellikle saniyeler yerine dakikalar düzeyindedir). Bu nedenle uygulama katmanında bir Heartbeat kararlı bir çözüm yoludur. Sunucu ve istemcinin aynı kavramı kullandığından emin olun (ör. metin veya JSON olarak “ping”/“pong”).
3) Threading und UI: Ereignisse müssen entkoppelt bleiben
Eğer OnText işlemi ağırsa (JSON ayrıştırma, DB erişimleri ile BDE-Ablosung mit nativer Anbindung, UI güncellemeleri), her şeyi Mainthread’te bloke etmemelidir. Wrapper yalnızca mesajı verir. Tipik bir desen şudur: OnText payload’u bir kuyruğa koyar (ör. TThreadedQueue<string>), ayrı bir Worker ise backpressure ile (yani kuyruk uzunluğunu sınırlayarak) işler. Bu, ani yük artışlarında UI’in donmasını veya alımın aksamasını engeller.
Debugging: was Sie loggen sollten, wenn es „manchmal“ abbricht
WebSocket’ler “günlerce çalışır, sonra aniden durur” ile ünlüdür. Loglama yoksa bunu daraltmak zordur. Mantıklı log noktaları:
- Zaman damgası (UTC), URL ve durum değişimleri (connecting/open/closed).
- Close-Reason, mevcutsa (Sunucu Close talep ediyor vs. ağ hatası).
- Heartbeat gönderme hataları ve alma sırasında oluşan istisnalar dahil Exception tipi.
- İsteğe bağlı: alınan mesajların boyutları (içerikler değil), veri patlamasını tespit etmek için.
Eğer TLS ile sonlandırma yapıyorsanız: ayrıca sertifika değişikliklerinin (süre sonu, yeni Issuer) hatalarla zamanlamasının örtüşüp örtüşmediğini kontrol edin. Sertleştirilmiş ortamlarda proxy ve DPI kutuları (Deep Packet Inspection) de şüpheli bileşenlerdir.
Varyantlar: System.Net.WebSockets ne zaman yeterli — ne zaman yetmez
System.Net.WebSockets, özellikle metin/JSON, orta düzey yük ve net yeniden-bağlanma stratejileri söz konusu olduğunda birçok entegrasyon durumu için yeterlidir. Sınırlar, Delphi-sürümüne ve hedef platforma göre belirginleşir:
- Eksik/sınırlı Ping/Pong desteği: Bu durumda uygulama heartbeat’i sağlam bir model olarak kalır.
- Connect/Receive sırasında zaman aşımı/iptal eksikliği: Mimariyi, takılı kalan bir worker’ın izole tutulacağı ve uygulamanın yine de düzgün sonlandırılacağı şekilde kurmanız gerekir (ör. proses watchdog veya ayrı worker örnekleri ile).
- Yüksek yük veya ikili akışlar: Bu durumda daha güçlü bir framing/buffering konsepti gerekir (ör. ring buffer, ayrı Binary-Event, limitli Message-Assembler).
Legacy durumlarda (eski Delphi nesilleri, çok özel TLS/Proxy gereksinimleri) bazı projelerde ICS gibi kütüphaneler pragmatik bir tercih olabilir. Önemli olan hangi kütüphane olduğundan çok, Shutdown, Reconnect ve Observability (loglar/metrikler) konularını birinci sınıf meseleler olarak ele almaktır.
Sonuç: bir Delphi WebSocket Client bir operasyon bileşenidir — net sınırları vardır
Bir WebSocket, push olayları, canlı durum, makine veya süreç bildirimleri ve portaller ile servisler için geri kanal olarak çok uygundur. Gösterilen sarmalayıcı, dijital kurumsal çözümlerde fark yaratan noktalara odaklanır: kontrollü yeniden bağlantı, boşta kalma zaman aşımlarına karşı Heartbeat, parçalanmaya dayanıklı metin işleme ve dağıtım ya da güncelleme sırasında takılmayan bir durdurma yolu.
Sınırlar devam eder: Connect/Receive iptalinin çok sıkı zaman pencerelerinde garanti edilmesi veya aşırı yüksek veri hızları gerekiyorsa, zaman aşımlarına, platform özelliklerine ve gerekirse alternatif yığına derinlemesine bakmanız gerekir. Ancak entegrasyon ve modernizasyon senaryolarının çoğu için yukarıda olduğu gibi iyi kapsüllenmiş, iyi loglanmış bir istemci, yerleşik Delphi sistemlerine entegre edilebilen sağlam bir temel sağlar.
Bu tür bir bileşeni mevcut bir mimariye (ör. Layer-3 Mimari ile net servis ve UI katmanları) uyarlamanız veya gerçek koşullarda aralıklı bağlantı kopmalarını debug etmeniz gerekiyorsa, bunu bizimle hedefe yönelik değerlendirebilirsiniz: İletişime geçin.
Teknik ortamda entegrasyonlar, veri akışları ve sürdürülebilir geliştirme temiz şekilde birlikte çalışmak zorundaysa, Heartbeat Ping/Pong da önemli bir rol oynar.
Sonraki adım
Konu gerçek bir projeye dönüştüğünde, mimari, mevcut yapı ve işletme erken aşamada birlikte ele alınmalıdır.
Bireysel sorularda destek vermekle kalmıyoruz; kaynak kodu parçacıklarından, legacy konularından veya portal fikirlerinden sağlam bir kurumsal projeye dönüşene kadar da destek veriyoruz.
- Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
- REST, veri erişimi, portallar ve Rollout sonraki işler olarak ertelenmez.
- Hangi yolun ekonomik ve işletme açısından uygulanabilir olduğunu erken görürsünüz.