Delphiと.NET向けのServer-Sent Eventsクライアント

· コンポーネント
Delphiと.NET向けのServer-Sent Eventsクライアント

Server-Sent Eventsは、かつてWebSocketsの物静かな親戚のような存在でした。サーバーが小さなテキストイベントを次々と書き込み続ける、終わりのない普通のHTTPレスポンスです。それが今、あらゆる場所で使われています。LLM APIのストリーミング応答はSSEとして届き、MCPサーバーはStreamable HTTP上でSSEを話し、ダッシュボードや通知、ジョブの進捗フィードも、HTTPを通すあらゆるプロキシを通過できるという理由でSSEを使っています。

sgcWebSockets 2026.10は、Delphi、C++Builder、.NET向けのEventSourceクライアントであるTsgcSSEClientを追加します。バックグラウンドスレッドでストリームを読み取り、すべてのイベントをタイプ、データ、idとともに渡し、接続が切れると自ら再接続し、Last-Event-IDで再開するため、その間に何も失われません。このコンポーネントは現行のダウンロード版には含まれていません。2026.10リリースとともに、すべてのエディションに同梱されます。

TsgcSSEClientが型付きイベントを受信し、接続を失い、Last-Event-IDで再開する様子です。YouTubeでも公開中。

なぜ今SSEが重要なのか

SSEストリームは、短いブロックで構成されたtext/event-streamレスポンスです。タイプを示す省略可能なevent:行、1行以上のdata:行、省略可能なid:、そしてイベントを終わらせる空行から成ります。サーバーはretry:を送って、再接続までどれくらい待つべきかをクライアントに伝えることもできます。フォーマット自体はシンプルですが、正しいクライアントは、どこで分割されるか分からないチャンク、複数行にまたがるデータ、CR、LF、CRLFの改行、そして止まった位置を覚えている再接続を処理しなければなりません。それこそがTsgcSSEClientが代わりにやってくれる部分です。

クイックスタート

sgcHTTP_SSE_Clientユニットを追加し、URLを設定して、OnEventを処理し、Openを呼び出します。ベアラートークンのようなリクエストヘッダーはHeadersに1行ごとにName: Valueの形式で追加し、再接続を含むすべての接続で送信されます。

procedure TForm1.FormCreate(Sender: TObject);
begin
  oSSE := TsgcSSEClient.Create(nil);
  oSSE.URL := 'https://www.example.com/events';
  oSSE.Headers.Add('Authorization: Bearer ' + FToken);
  oSSE.OnEvent := OnSSEEvent;
  oSSE.OnError := OnSSEError;
  oSSE.Open;
end;

procedure TForm1.OnSSEEvent(Sender: TObject; const aEvent: TsgcSSEEvent);
begin
  if aEvent.EventType = 'alert' then
    ShowMessage(aEvent.Data)
  else
    Memo1.Lines.Add(aEvent.EventType + ' #' + aEvent.Id + ': ' + aEvent.Data);
end;

procedure TForm1.OnSSEError(Sender: TObject; const aError: string);
begin
  Memo1.Lines.Add('error: ' + aError);
end;

すべてのイベントには型が付いています。EventTypeにはevent:の名前が入っているため、1つのハンドラでalertやtick、あるいはサーバーが独自に定義したどんなタイプでもルーティングできます。VCLやFireMonkeyのアプリケーションでは、イベントはデフォルトでメインスレッドに配信されるため、ハンドラは直接UIを操作できます。NotifyEventsでこれを変更できます。OnOpenとOnCloseはストリームの開始と終了を知らせ、ReadyStateはsseConnecting、sseOpen、sseClosedのいずれかを報告します。

クライアントは、ブラウザのEventSourceと同じ方法でサーバーに反応します。

Last-Event-IDで再開する

サーバーがイベントにid:フィールドを付けると、クライアントは最後の値をLastEventIdに保持し、再接続のたびにLast-Event-IDヘッダーで送信します。サーバーはそのヘッダーを読み取り、そのイベントの続きから再開するため、接続が切れてもメッセージが失われることも重複することもありません。同じセッション内であれば、これにコードは一切必要ありません。

アプリケーションを再起動した後に再開するには、停止時にLastEventIdを保存し、Openの前に復元します。このプロパティはCloseの後も値を保持し、Openの前に設定した値は最初のリクエストとともに送信されます。

procedure TForm1.StartStream;
begin
  if FileExists('sse_last_id.txt') then
    oSSE.LastEventId := Trim(TFile.ReadAllText('sse_last_id.txt'));
  oSSE.Open;
end;

procedure TForm1.StopStream;
begin
  oSSE.Close;
  TFile.WriteAllText('sse_last_id.txt', oSSE.LastEventId);
end;

再開にはサーバー側の協力が必要です。サーバーはイベントにid:を送信し、Last-Event-IDヘッダーを尊重しなければなりません。

自分で制御できる再接続ポリシー

再接続はデフォルトで有効になっており、遅延は3秒です。ReconnectOptionsを使えば、サーバーにふさわしいどんなポリシーにも変えられます。倍率付きの指数バックオフ、遅延の上限、何千ものクライアントが同じミリ秒に一斉に戻ってこないようにするランダムジッター、そして試行回数の上限です。

procedure TForm1.SetupReconnect;
begin
  oSSE.ReconnectOptions.Interval := 1000;
  oSSE.ReconnectOptions.Backoff := True;
  oSSE.ReconnectOptions.BackoffMultiplier := 2.0;
  oSSE.ReconnectOptions.MaxInterval := 30000;
  oSSE.ReconnectOptions.Jitter := 0.2;
  oSSE.ReconnectOptions.MaxAttempts := 10;
  oSSE.OnReconnect := OnSSEReconnect;
end;

MaxAttemptsは連続した失敗の回数を数えます。接続が開くたびにカウンターはリセットされるため、たまにネットワークの不調に見舞われながら何週間も動き続けるクライアントが、試行回数を使い果たすことはありません。サーバー側にも発言権があります。retry:フィールドは、そのセッションの残り時間についてIntervalを置き換えます。最終的な決定権はあなたにあり、それぞれの試行の前に、計算済みの遅延とともに発生するOnReconnectで下せます。

procedure TForm1.OnSSEReconnect(Sender: TObject; aAttempt: Integer;
  var aDelay: Integer; var aCancel: Boolean);
begin
  // stop after the fifth failed attempt, otherwise wait at least 2 seconds
  if aAttempt > 5 then
    aCancel := True
  else if aDelay < 2000 then
    aDelay := 2000;
end;

TLS、プロキシ、認証

ストリームはTsgcHTTP1Clientで読み取られ、OnBeforeConnectが接続を試みるたびにそれをあなたに渡します。HTTPクライアントにできることは、SSEクライアントにもできます。TLSオプション、プロキシ、基本認証、より長い読み取りタイムアウトまで、すべて可能です。

procedure TForm1.OnSSEBeforeConnect(Sender: TObject;
  const aHTTP: TsgcHTTP1Client);
begin
  aHTTP.TLSOptions.Version := tls1_2;
  aHTTP.Proxy.Enabled := True;
  aHTTP.Proxy.Host := '192.168.1.10';
  aHTTP.Proxy.Port := 8080;
  aHTTP.ReadTimeout := 60000;
end;

OnBeforeConnectとOnReconnectは常にバックグラウンドスレッドで実行されるため、UI操作はその中に入れないでください。

LLMストリームとMCPのためのパーサー

長時間維持されるEventSourceがまったく必要ないことも多くあります。stream: trueを使うLLMのチャット補完リクエストは、レスポンスがSSEであるPOSTであり、MCPのStreamable HTTPレスポンスもSSEになり得ます。そうした用途のために、クライアント内部のパーサーが公開されています。TsgcSSEParserです。ネットワークが実際に配信するとおりの、任意のサイズのチャンクで生のバイト列やテキストを渡すと、完全なイベントごとにOnEventを、有効なretry:フィールドごとにOnRetryを発生させます。チャンクは行の途中やUTF-8文字の途中で終わることもありますが、パーサーは次の呼び出しまで残りを保持します。

procedure TForm1.ParseStream;
begin
  FParser := TsgcSSEParser.Create;
  FParser.OnEvent := OnParserEvent;
  // chunks arrive as the network delivers them, split anywhere
  FParser.Feed('event: content_block_delta'#10'data: {"delta":{"text":"Hel');
  FParser.Feed('lo"}}'#10#10'event: message_stop'#10);
  FParser.Feed(TEncoding.UTF8.GetBytes('data: {}'#10#10));
  // start again for the next response
  FParser.Reset;
end;

procedure TForm1.OnParserEvent(Sender: TObject; const aEvent: TsgcSSEEvent);
begin
  Memo1.Lines.Add(aEvent.EventType + ' ' + aEvent.Data);
end;

この3つのチャンクは2つのイベントを生成します。完全なJSON {"delta":{"text":"Hello"}}を伴うcontent_block_deltaと、message_stopです。Resetは次のレスポンスの前に、途中まで読まれたイベントをすべて破棄し、LastEventIdはパーサーが最後に見たidを教えてくれます。

.NET向けのC#

sgcWebSocketsの.NET版には、同じ名前の同じコンポーネントがesegece.sgcWebSockets名前空間に用意されています。HeadersはName: Value形式の文字列のリストで、イベントは通常の.NETイベントです。

using esegece.sgcWebSockets;

string token = args.Length > 0 ? args[0] : "";

var sse = new TsgcSSEClient();
sse.URL = "https://www.example.com/events";
sse.Headers.Add("Authorization: Bearer " + token);
if (File.Exists("sse_last_id.txt"))
    sse.LastEventId = File.ReadAllText("sse_last_id.txt").Trim();
sse.ReconnectOptions.Backoff = true;
sse.ReconnectOptions.MaxInterval = 30000;
sse.OnEvent += (sender, e) => Console.WriteLine($"{e.EventType} #{e.Id}: {e.Data}");
sse.OnError += (sender, error) => Console.WriteLine("error: " + error);
sse.Open();

Console.ReadLine();
sse.Close();
File.WriteAllText("sse_last_id.txt", sse.LastEventId);

TsgcSSEParserも同様に用意されており、byte[]、そのスライス、文字列に対応するFeedのオーバーロードがあります。

デモを試す

どちらの版にも、完全にオフラインで動作するデモが付属しています。同じアプリケーション内の127.0.0.1、ポート5580で小さなSSEサーバーをホストし、id:付きの番号付きイベントをストリーミングし、message、tick、alertのタイプを順番に切り替え、retry:フィールドを送信し、受け取ったLast-Event-IDの後から再開します。接続を切ってみて、クライアントが再接続し、抜けも重複もなく次のidから続きを再開する様子を見てください。

提供状況

TsgcSSEClientとTsgcSSEParserはsgcWebSockets 2026.10で、Delphi、C++Builder、.NET向けに、すべてのエディションで提供されます。今日ダウンロードできるバージョンには含まれていません。2026.10がリリースされるとダウンロードページに掲載され、このコンポーネントはSGC HTTPパレットタブに登録されます。

すべてのプロパティ、イベント、再開ガイドを含む完全なリファレンスはTsgcSSEClientのヘルプにあります。sgcWebSocketsがServer-Sent Eventsに関して提供するその他すべての機能については、SSE製品ページをご覧ください。

次に読む

ご質問やフィードバック、移行のお手伝いが必要ですか?お問い合わせください。実際にそのコードを書いた人から返信が届きます。