Skip to content

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

main.c
#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

client.go
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

Terminal window
$ ./moqt-hub --port 4433
conn closed by peer (CONNECTION_CLOSE)
Terminal window
$ go run . https://127.0.0.1:4433
status 200
recv SETUP type=0x2f00 options=4 bytes
  • wired_moqt_init(&hub, io) sets up the hub. io is the table of WebTransport sends the hub uses; wired_moqraw_io() (from app/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_datagram and _on_session_close go straight into wired_srvrun_opt, each with &hub as its context. wired_server_run_opt runs 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 sets opt.wt_protocols = "moqt-22" and the client offers moqt-22 (webtransport-go ApplicationProtocols), 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 protocols support) gets draft-19 over the older single bidirectional control stream and would read SETUP with AcceptStream; examples/moqt_chat uses that as its fallback.
  • To let clients pick a draft, fill opt.wt_protocols from wired_moqt_wt_protocols(buf, sizeof buf), which writes the hub’s list (moqt-22 moqt-19 moqt-18); examples/moqt_interop does this.
  • max_datagram_frame_size in the identity enables QUIC DATAGRAM, which WebTransport clients require.