Skip to content

Fanning a datagram out to every session

srvrun.h has three ways to fan a datagram out from inside a callback: a single-slot broadcast (last writer wins if two land in the same step), a bounded ring that queues several without overwriting, and sending to one session directly. This page picks between them by a leading tag byte.

Code

main.c
#define WIRED_MAIN
#include "wired.h"
/* The first byte of a received datagram picks which of srvrun.h's three
* datagram-fanout primitives relays the rest: 1 = the single-slot
* wired_server_broadcast_datagram (a second broadcast in the same loop
* step overwrites the first); 2 = wired_server_broadcast_datagram_ring,
* the bounded ring that queues several without overwriting; 3 =
* wired_server_wt_send_datagram_to, addressed back at the sender only --
* not a broadcast at all. */
static void on_datagram(void* ctx, wired_wt_session* s, wired_span data) {
(void)ctx;
if (data.n == 0) return;
wired_span rest = wired_span_of(data.p + 1, data.n - 1);
switch (data.p[0]) {
case 1:
wired_server_broadcast_datagram(rest);
break;
case 2:
wired_server_broadcast_datagram_ring(rest);
break;
case 3:
wired_server_wt_send_datagram_to(s, rest);
break;
}
}
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 = {0};
opt.incoming_cpu = -1;
opt.wt_on_datagram = on_datagram;
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 (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"github.com/quic-go/quic-go"
"github.com/quic-go/webtransport-go"
)
func dial(ctx context.Context, addr string) (*webtransport.Session, *quic.Conn) {
var conn *quic.Conn // the latest connection, kept so main can close it
d := webtransport.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
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, addr+"/relay", http.Header{})
if err != nil {
panic(err)
}
return s, conn
}
func main() {
ctx := context.Background()
a, connA := dial(ctx, os.Args[1])
b, connB := dial(ctx, os.Args[1])
defer connA.CloseWithError(0, "")
defer connB.CloseWithError(0, "")
// tag 1: the single-slot broadcast reaches every session, including A.
a.SendDatagram([]byte{1, 's', 'l', 'o', 't'})
for _, who := range []struct {
name string
s *webtransport.Session
}{{"A", a}, {"B", b}} {
msg, err := who.s.ReceiveDatagram(ctx)
if err != nil {
panic(err)
}
fmt.Printf("%s recv %q\n", who.name, msg)
}
// tag 2: the ring delivers all three, in order, to every session.
for _, word := range []string{"ring1", "ring2", "ring3"} {
a.SendDatagram(append([]byte{2}, word...))
}
for i := 0; i < 3; i++ {
for _, who := range []struct {
name string
s *webtransport.Session
}{{"A", a}, {"B", b}} {
msg, err := who.s.ReceiveDatagram(ctx)
if err != nil {
panic(err)
}
fmt.Printf("%s recv %q\n", who.name, msg)
}
}
// tag 3: send_datagram_to addresses only the sender, B never sees it.
a.SendDatagram([]byte{3, 'd', 'i', 'r', 'e', 'c', 't'})
msg, err := a.ReceiveDatagram(ctx)
if err != nil {
panic(err)
}
fmt.Printf("A recv %q\n", msg)
}

Output

Terminal window
$ ./wt-broadcast --port 4433
conn closed by peer (CONNECTION_CLOSE)
conn closed by peer (CONNECTION_CLOSE)
Terminal window
$ go run . https://127.0.0.1:4433
A recv "slot"
B recv "slot"
A recv "ring1"
B recv "ring1"
A recv "ring2"
B recv "ring2"
A recv "ring3"
B recv "ring3"
A recv "direct"
  • Tag 1 calls wired_server_broadcast_datagram: queues one payload per connection with an active WT session, overwritten by a second call in the same step.
  • Tag 2 calls wired_server_broadcast_datagram_ring: the same fan-out, but into the bounded ring also backing wired_server_wt_send_datagram_to – several calls in one step are all delivered, none overwritten.
  • Tag 3 calls wired_server_wt_send_datagram_to(s, ...): addressed at s only, not a broadcast – the client’s second session never sees it.
  • Neither primitive excludes the sender: a broadcast reaches every active session, including the one it came from, so the client reads its own copy back too.