TsgcWebTransportStream

TsgcWebTransportStream represents one bidirectional or unidirectional stream inside a WebTransport session. It is where all application data travels.

Introduction

A WebTransport session carries no payload of its own. Everything you send and receive travels on a stream, and TsgcWebTransportStream is that stream. It wraps one real QUIC stream, so streams within a session are independent: a stall or a loss on one does not hold up the others, which is the head-of-line-blocking freedom QUIC exists to provide. The class is declared in the sgcWebTransport unit and, like TsgcWebTransportSession, it is a plain class rather than a component. You never construct one. It reaches you in one of two ways:

Either way, the Session owns the stream and frees it. Do not free it yourself.

Two directions

Unidirectional is fixed when the stream is opened and says which kind you have:

KindHeader on the wireBehaviour
Bidirectional (Unidirectional = False)varint(0x41) + varint(SessionId)Both sides can write. The natural choice for request and response, or for an echo.
Unidirectional (Unidirectional = True)varint(0x54) + varint(SessionId)Only the opener can write. The receiver's copy is receive-only, so writing to it does nothing useful.

That header is written once, by OpenStream, before you get the stream. Everything you pass to WriteData is pure application payload, and everything OnData gives you is pure application payload. You never see the framing.

It is a byte stream, not a message queue

This is the single most common source of bugs. WriteData and OnData do not preserve message boundaries. QUIC delivers a stream of bytes, and one WriteData may surface as several OnData calls, or several writes may arrive coalesced into one. If your protocol has messages, frame them yourself, with a length prefix, a delimiter, or fixed-size records. The string overload of WriteData encodes to UTF-8, and short test payloads usually arrive in one piece, which makes this trap easy to miss until traffic gets real.

Closing is a local signal

Close marks the stream closed, unregisters it from its session, and raises OnClose, so StreamCount drops. It does not send a QUIC FIN and does not tell the peer. Like the session's Close, it is guarded: calling it twice raises OnClose once.

Reference

Key Properties

NameDescription
StreamIdThe QUIC stream id backing this stream.
UnidirectionalTrue for a one-way stream, False for a bidirectional one. Fixed at construction.
SessionThe session that owns this stream.

Key Methods

NameDescription
WriteDataWrites application bytes to the stream. Overloaded for TBytes and for string (UTF-8 encoded).
CloseUnregisters the stream from its session and raises OnClose. A local signal only.

Key Events

NameDescription
OnDataFires when application bytes arrive on this stream.
OnCloseFires when Close is called on this stream.

Complete Example

A WebTransport echo server: accept the session, take the streams the client opens, and echo every payload back on the same stream. This is the server side of the round-trip shown on the TsgcWebTransportSession page, so the two can be run against each other.


unit FWebTransportServer;

interface

uses
  Classes, SysUtils, Forms, StdCtrls, ExtCtrls,
  // sgc
  sgcHTTP, sgcHTTP3_Classes, sgcHTTP3_Types, sgcQUIC_Server, sgcWebTransport;

type
  TfrmServer = class(TForm)
    memoLog: TMemo;
    btnStart: TButton;
    btnStop: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnStartClick(Sender: TObject);
    procedure btnStopClick(Sender: TObject);
  private
    FServer: TsgcHTTP3Server;
    procedure OnWTSession(Sender: TObject;
      const aConnection: TsgcQUICServerConnection;
      const aSession: TsgcWebTransportSession; var aAccept: Boolean);
    procedure OnWTStream(Sender: TObject;
      const aStream: TsgcWebTransportStream);
    procedure OnWTStreamData(Sender: TObject; const aData: TBytes);
    procedure OnWTStreamClose(Sender: TObject);
  end;

implementation

{$R *.dfm}

procedure TfrmServer.FormCreate(Sender: TObject);
begin
  FServer := TsgcHTTP3Server.Create(nil);

  // WebTransport is enabled ONLY when this is greater than zero. Setting it
  // advertises SETTINGS_ENABLE_CONNECT_PROTOCOL and
  // SETTINGS_WEBTRANSPORT_MAX_SESSIONS, and switches the connection into
  // multi-stream mode.
  FServer.H3Settings.Settings.WebTransportMaxSessions := 4;

  FServer.OnWebTransportSession := OnWTSession;
end;

procedure TfrmServer.FormDestroy(Sender: TObject);
begin
  FServer.Free;   // frees every session and every stream underneath
end;

procedure TfrmServer.btnStartClick(Sender: TObject);
begin
  FServer.Host := '0.0.0.0';
  FServer.Port := 4433;
  FServer.QUICOptions.SSLOptions.CertFile := 'server.crt';
  FServer.QUICOptions.SSLOptions.KeyFile := 'server.key';
  FServer.Start;
end;

procedure TfrmServer.btnStopClick(Sender: TObject);
begin
  FServer.Stop;
end;

procedure TfrmServer.OnWTSession(Sender: TObject;
  const aConnection: TsgcQUICServerConnection;
  const aSession: TsgcWebTransportSession; var aAccept: Boolean);
begin
  // aAccept arrives True, so doing nothing accepts. Set False to reject,
  // and the server answers :status=404 and discards the session.
  aAccept := True;

  memoLog.Lines.Add(Format('session %d accepted from %s:%d',
    [aSession.SessionId, aConnection.PeerIP, aConnection.PeerPort]));

  // Assign OnStream BEFORE returning: the client may open a stream the
  // moment it sees our :status=200.
  aSession.OnStream := OnWTStream;
end;

procedure TfrmServer.OnWTStream(Sender: TObject;
  const aStream: TsgcWebTransportStream);
begin
  // Assign OnData FIRST. Bytes that arrived with the stream header are
  // delivered as soon as this handler returns, and are lost otherwise.
  aStream.OnData := OnWTStreamData;
  aStream.OnClose := OnWTStreamClose;

  memoLog.Lines.Add(Format('stream %d opened by peer (unidirectional=%s)',
    [aStream.StreamId, BoolToStr(aStream.Unidirectional, True)]));
end;

procedure TfrmServer.OnWTStreamData(Sender: TObject; const aData: TBytes);
var
  oStream: TsgcWebTransportStream;
begin
  if not (Sender is TsgcWebTransportStream) then
    Exit;
  oStream := TsgcWebTransportStream(Sender);

  memoLog.Lines.Add(Format('stream %d received %d bytes: %s',
    [oStream.StreamId, Length(aData), TEncoding.UTF8.GetString(aData)]));

  // ECHO. Only a bidirectional stream can be written back on: a peer-opened
  // unidirectional stream is receive-only at this end.
  if not oStream.Unidirectional then
  begin
    oStream.WriteData(aData);
    memoLog.Lines.Add('echoed ' + IntToStr(Length(aData)) + ' bytes');
  end;
end;

procedure TfrmServer.OnWTStreamClose(Sender: TObject);
begin
  memoLog.Lines.Add('stream closed');
end;

end.

Requirements