MoQT hub
A MoQT server is a wired_moqt_hub sitting on the WebTransport callbacks. It
negotiates whichever of draft-ietf-moq-transport-18/19/22 the peer offers via
the WT subprotocol (moqt-18/moqt-19/moqt-22). Every MoQT sample server
in this guide sets opt.wt_protocols = "moqt-22" and its Go client offers
moqt-22, so the hub runs draft-22 and the section numbers cited in this
guide are draft-22’s. This page shows the hub alone: a client connects and
receives the server’s SETUP.
Code
#define WIRED_MAIN#include "wired.h"
static wired_moqt_hub g_hub;
int wired_main(int argc, char** argv) { /* The same fixed demo identity as the HTTP/3 pages, plus QUIC DATAGRAM * support, which WebTransport clients ask for. */ static wired_srvboot_demo_keys keys; wired_srvboot_id id; wired_srvboot_demo(&id, &keys, 0x50, "guide-mq"); id.max_datagram_frame_size = 65535;
wired_moqt_io io = wired_moqraw_io(); wired_moqt_init(&g_hub, io);
/* The hub owns every WebTransport session. A client that offers the * subprotocol "moqt-22" gets draft-ietf-moq-transport-22: the hub sends * SETUP on a uni control stream of its own (6.3), answers each request * on the bidi stream it arrived on (6.4.2), and forgets it on close. */ wired_srvrun_opt opt = {0}; opt.incoming_cpu = -1; opt.wt_protocols = "moqt-22"; opt.wt_on_session = wired_moqt_on_session; opt.wt_session_ctx = &g_hub; opt.wt_on_stream_data = wired_moqt_on_stream_data; opt.wt_stream_data_ctx = &g_hub; opt.wt_on_datagram = wired_moqt_on_datagram; opt.wt_datagram_ctx = &g_hub; opt.wt_on_session_close = wired_moqt_on_session_close; opt.wt_session_close_ctx = &g_hub;
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 ( "bufio" "context" "crypto/tls" "fmt" "io" "log" "math/bits" "net/http" "os" "time"
"github.com/quic-go/quic-go" "github.com/quic-go/webtransport-go")
// varint reads a MoQT variable-length integer: the count of leading 1 bits// in the first byte is the count of bytes that follow.func varint(r *bufio.Reader) uint64 { b, err := r.ReadByte() check(err) n := bits.LeadingZeros8(^b) v := uint64(b) & (0xff >> (n + 1)) for ; n > 0; n-- { c, err := r.ReadByte() check(err) v = v<<8 | uint64(c) } return v}
// readMsg reads one control message: Type (varint), 16-bit Length, body.func readMsg(r *bufio.Reader) (uint64, []byte) { typ := varint(r) var hdr [2]byte _, err := io.ReadFull(r, hdr[:]) check(err) body := make([]byte, int(hdr[0])<<8|int(hdr[1])) _, err = io.ReadFull(r, body) check(err) return typ, body}
func check(err error) { if err != nil { log.Fatal(err) }}
func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Keep the QUIC connection so main can close it before exiting. var conn *quic.Conn // Offering the WebTransport subprotocol "moqt-22" selects // draft-ietf-moq-transport-22 (SS6.2.1). d := webtransport.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, QUICConfig: &quic.Config{EnableDatagrams: true, EnableStreamResetPartialDelivery: true}, ApplicationProtocols: []string{"moqt-22"}, DialAddr: func(ctx context.Context, addr string, t *tls.Config, c *quic.Config) (*quic.Conn, error) { var err error conn, err = quic.DialAddrEarly(ctx, addr, t, c) return conn, err }, } rsp, s, err := d.Dial(ctx, os.Args[1]+"/moqt", http.Header{}) check(err) fmt.Println("status", rsp.StatusCode) if p := s.SessionState().ApplicationProtocol; p != "moqt-22" { log.Fatalf("server picked subprotocol %q", p) }
// Each side opens one uni control stream that starts with SETUP // (SS6.3). Ours: Type 0x2F00 (also the stream type), Length 0, no // Setup Options. It stays open for the whole session. ctl, err := s.OpenUniStreamSync(ctx) check(err) _, err = ctl.Write([]byte{0xAF, 0x00, 0, 0}) check(err)
// The server's control stream, and the SETUP on it. srv, err := s.AcceptUniStream(ctx) check(err) typ, body := readMsg(bufio.NewReader(srv)) fmt.Printf("recv SETUP type=%#x options=%d bytes\n", typ, len(body)) check(conn.CloseWithError(0, ""))}Output
$ ./moqt-hub --port 4433conn closed by peer (CONNECTION_CLOSE)$ go run . https://127.0.0.1:4433status 200recv SETUP type=0x2f00 options=4 bytesHow it works
Section titled “How it works”wired_moqt_init(&hub, io)sets up the hub.iois the table of WebTransport sends the hub uses;wired_moqraw_io()(fromapp/moqt/qraw/moqrawio.h) returns the SDK’s ready-made table, which puts the stream signal in front of every stream the hub opens and serves raw-QUIC MoQT sessions too.- The hub’s
wired_moqt_on_session,_on_stream_data,_on_datagramand_on_session_closego straight intowired_srvrun_opt, each with&hubas its context.wired_server_run_optruns the server. - When a session opens, the hub opens its control stream and sends SETUP
(type
0x2f00, which also serves as the stream type; draft-ietf-moq-transport-22 6.4.1, 9.1) on it. Which shape that stream takes follows the WT subprotocol. This page’s server setsopt.wt_protocols = "moqt-22"and the client offersmoqt-22(webtransport-goApplicationProtocols), so the hub opens a unidirectional control stream and the client opens its own and sends its SETUP (6.3). The hub serves requests only once both SETUPs are exchanged, and each later request (e.g. SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE) goes on its own client-opened bidirectional stream and is answered there (6.4.2). The client reads the server’s SETUP from the first incoming unidirectional stream (AcceptUniStream). - A client that offers no subprotocol (a browser without WebTransport
protocolssupport) gets draft-19 over the older single bidirectional control stream and would read SETUP withAcceptStream;examples/moqt_chatuses that as its fallback. - To let clients pick a draft, fill
opt.wt_protocolsfromwired_moqt_wt_protocols(buf, sizeof buf), which writes the hub’s list (moqt-22 moqt-19 moqt-18);examples/moqt_interopdoes this. max_datagram_frame_sizein the identity enables QUIC DATAGRAM, which WebTransport clients require.