Skip to content

Keeping a server-opened stream open

The streams page’s wired_server_wt_open_uni sends its whole payload and closes with FIN right away. wired_server_wt_open_uni_stream opens the same kind of stream but leaves it open, so the app can append more rounds later with wired_server_wt_stream_send and decide the FIN round itself.

Code

main.c
#define WIRED_MAIN
#include "wired.h"
/* The one server-opened uni stream this single-session demo keeps open
* across several rounds, appended to from on_stream_data below. */
static i64 g_uni = -1;
/* Open it on connect, WITHOUT closing: wired_server_wt_open_uni_stream
* (unlike wired_server_wt_open_uni) leaves the stream open for further
* wired_server_wt_stream_send rounds. */
static void on_session(
void* ctx, wired_wt_session* s, wired_span path, wired_span protocol) {
(void)ctx;
(void)path;
(void)protocol;
u8 buf[16];
usz n = wired_wtwire_signal_put(buf, sizeof buf, 0, s->connect_stream_id);
memcpy(buf + n, "chunk1 ", 7);
g_uni = wired_server_wt_open_uni_stream(s, wired_span_of(buf, n + 7));
}
/* The client drives the open stream over its own bidi requests: "more"
* appends a round (fin=0, stays open), "bye" appends the closing round
* (fin=1). Each request gets a short reply once applied. */
static void on_stream_data(
void* ctx, wired_wt_session* s, u64 stream_id, wired_span data, int fin) {
(void)ctx;
if (!fin) return;
if (wired_span_eq_cstr(data, "more"))
wired_server_wt_stream_send(s, (u64)g_uni, wired_span_cstr("chunk2 "), 0);
else if (wired_span_eq_cstr(data, "bye"))
wired_server_wt_stream_send(s, (u64)g_uni, wired_span_cstr("chunk3"), 1);
wired_server_wt_stream_reply(s, stream_id, wired_span_cstr("ok"));
}
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_session = on_session;
opt.wt_on_stream_data = on_stream_data;
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"
"io"
"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},
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]+"/open", http.Header{})
if err != nil {
panic(err)
}
defer conn.CloseWithError(0, "")
uni, err := s.AcceptUniStream(ctx)
if err != nil {
panic(err)
}
done := make(chan []byte, 1)
go func() {
b, _ := io.ReadAll(uni)
done <- b
}()
// Each word drives the server-opened stream one round further, and we
// wait for its ack before sending the next -- so "bye" only reaches the
// server after "more" was fully applied.
for _, word := range []string{"more", "bye"} {
str, err := s.OpenStreamSync(ctx)
if err != nil {
panic(err)
}
str.Write([]byte(word))
str.Close()
ack, _ := io.ReadAll(str)
fmt.Printf("%s: %q\n", word, ack)
}
fmt.Printf("uni stream: %q\n", <-done)
}

Output

Terminal window
$ ./wt-open-streams --port 4433
conn closed by peer (CONNECTION_CLOSE)
Terminal window
$ go run . https://127.0.0.1:4433
more: "ok"
bye: "ok"
uni stream: "chunk1 chunk2 chunk3"
  • on_session opens the uni stream with wired_server_wt_open_uni_stream and keeps its id in g_uni – fin is never set on this call, so the stream stays open past the first round.
  • Each client request appends one round: wired_server_wt_stream_send(s, g_uni, payload, fin) with fin=0 keeps it open for more, fin=1 ends it after this round’s bytes.
  • The client’s uni-stream read (io.ReadAll) only returns once the server sends that fin=1 round – until then it blocks, same as any stream read waiting on data.