TsgcWebTransportStream represents one bidirectional or unidirectional stream inside a WebTransport session. It is where all application data travels.
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:
Session.OpenStream returns a stream ready to write.aStream parameter of the session's OnStream event.
Either way, the Session owns the stream and frees it. Do not free it yourself.
Unidirectional is fixed when the stream is opened and says which kind you have:
| Kind | Header on the wire | Behaviour |
|---|---|---|
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.
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.
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.
| Name | Description |
|---|---|
| StreamId | The QUIC stream id backing this stream. |
| Unidirectional | True for a one-way stream, False for a bidirectional one. Fixed at construction. |
| Session | The session that owns this stream. |
| Name | Description |
|---|---|
| WriteData | Writes application bytes to the stream. Overloaded for TBytes and for string (UTF-8 encoded). |
| Close | Unregisters the stream from its session and raises OnClose. A local signal only. |
| Name | Description |
|---|---|
| OnData | Fires when application bytes arrive on this stream. |
| OnClose | Fires when Close is called on this stream. |
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.
sgcVer.inc, the HTTP/3 feature pack SGC_PACK_HTTP3 is declared inside the All-Access edition block (SGC_EDT_ALL) and requires SGC_INDY_LIB.{$DEFINE SGC_PACK_HTTP3} in sgcVer.inc, which defines SGC_QUIC, SGC_HTTP3 and SGC_WEBTRANSPORT. The whole class is guarded by {$IFDEF SGC_WEBTRANSPORT}.sgcWebTransport declares the class; add sgcHTTP for TsgcHTTP3Client and TsgcHTTP3Server, and sgcQUIC_Server for TsgcQUICServerConnection.qmBuiltin QUIC engine (OpenSSL's native QUIC), auto-detected at OpenSSL load and not user-selectable.esegece.sgcWebSockets.