Session lifecycle
A session can end in more ways than a dropped connection. This server keeps a small piece of state per session, asks the client to wind down after its first datagram, and closes the session with an application code and a reason after the second.
Code
#define WIRED_MAIN#include "wired.h"
/* App state kept per session, keyed on the session pointer. */typedef struct { wired_wt_session* s; /* 0 = free slot */ int datagrams;} session_state;
static session_state states[4];
static session_state* find(wired_wt_session* s) { for (usz i = 0; i < sizeof states / sizeof states[0]; i++) if (states[i].s == s) return &states[i]; return 0;}
/* Claim a free slot for the new session, starting from zeroed state. */static void on_session( void* ctx, wired_wt_session* s, wired_span path, wired_span protocol) { (void)ctx; (void)path; (void)protocol; session_state* st = find(0); if (!st) return; *st = (session_state){.s = s}; wired_log_str("session open\n");}
/* First datagram: ask the client to wind down. The session stays open, so * the second datagram still arrives; then close with a code and a reason. */static void on_datagram(void* ctx, wired_wt_session* s, wired_span data) { (void)ctx; (void)data; session_state* st = find(s); if (!st) return; if (++st->datagrams == 1) { wired_server_wt_drain_session(s); wired_log_str("drain sent\n"); return; } wired_server_wt_close_session(s, 4001, wired_span_cstr("demo done")); wired_log_str("close sent\n");}
/* Fires once per session. Release the slot: the next session may get the * same pointer, and it must not inherit this one's state. */static void on_session_close(void* ctx, wired_wt_session* s) { (void)ctx; session_state* st = find(s); if (st) st->s = 0; wired_log_str("session closed, state released\n");}
int wired_main(int argc, char** argv) { /* The same fixed demo identity as the HTTP/3 hello page. */ static wired_srvboot_demo_keys keys; wired_srvboot_id id; wired_srvboot_demo(&id, &keys, 0x50, "guide-wt"); id.max_datagram_frame_size = 65535; /* WebTransport requires DATAGRAM */
wired_srvrun_opt opt = { .incoming_cpu = -1, .wt_on_session = on_session, .wt_on_datagram = on_datagram, .wt_on_session_close = on_session_close, };
u16 port = (u16)wired_cliargs_int(argc, argv, "--port", 4433); wired_srvrun_handler h = {0}; wired_srvrun_obs obs = {0}; return wired_server_run_opt(port, &id, h, obs, &opt) ? 0 : 1;}Client
package main
import ( "context" "crypto/tls" "errors" "fmt" "net/http" "os"
"github.com/quic-go/quic-go" "github.com/quic-go/webtransport-go")
func main() { ctx := context.Background() var conn *quic.Conn // kept so main can close it before exiting d := webtransport.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // self-signed demo cert QUICConfig: &quic.Config{EnableDatagrams: true, EnableStreamResetPartialDelivery: true}, DialAddr: func(ctx context.Context, addr string, t *tls.Config, q *quic.Config) (*quic.Conn, error) { c, err := quic.DialAddrEarly(ctx, addr, t, q) conn = c return c, err }, } _, s, err := d.Dial(ctx, os.Args[1]+"/lifecycle", http.Header{}) if err != nil { panic(err) } fmt.Println("session open")
// The first datagram makes the server send WT_DRAIN_SESSION, the second // makes it close the session. for _, msg := range []string{"one", "two"} { if err := s.SendDatagram([]byte(msg)); err != nil { panic(err) } }
// Any session call returns the close once it arrives. _, err = s.AcceptUniStream(ctx) var se *webtransport.SessionError if !errors.As(err, &se) { panic(err) } fmt.Printf("closed by server: code %d, reason %q\n", se.ErrorCode, se.Message) conn.CloseWithError(0, "")}Output
$ ./wt-session-lifecycle --port 4433session opendrain sentclose sentsession closed, state releasedconn closed by peer (CONNECTION_CLOSE)$ go run . https://127.0.0.1:4433session openclosed by server: code 4001, reason "demo done"How it works
Section titled “How it works”- The app state lives in a small table keyed on the
wired_wt_sessionpointer.wt_on_sessionclaims a slot, andwt_on_session_closefrees it. The SDK reuses the session’s storage for a later connection, so a slot left behind would hand the dead session’s state to the next client that lands on the same pointer. wired_server_wt_drain_session(s)sendsWT_DRAIN_SESSION: a request to finish up soon. The session stays open, which is why the second datagram still arrives. It is sent at most once per session.- webtransport-go v0.13 has no API for drain. It skips the capsule as unknown, so the client prints nothing for it.
wired_server_wt_close_session(s, 4001, reason)sendsWT_CLOSE_SESSIONwith that code and reason, then ends the session. It is queued, not sent inline, and the reason is copied. The client sees it as a*webtransport.SessionErrorcarrying the same code and reason.- The server logs
session closedbefore the connection closes, because it ended the session itself. The client closes its QUIC connection afterwards. - The other direction exists too: register
opt.wt_on_session_drainingto hear when a client sendsWT_DRAIN_SESSION. It fires once per session, however often the client repeats it. This page does not show it, because webtransport-go v0.13 cannot send a drain.