Skip to content

Commit 7795845

Browse files
committed
Generalize event_loop.Client to support H3 and raw QUIC protocols
Previously Client only supported .webtransport. Now all three application protocols work through the same handler-callback pattern: - .h3: onHeaders, onData, onFinished, onSettings, onGoaway callbacks; ClientSession gains sendRequest/sendResponse/recvBody methods - .quic: onStreamData, onStreamFinished callbacks; ClientSession gains openStream/writeStream/closeQuicStream/readStream methods - .webtransport: unchanged Server also gains pollQuicEvents and raw QUIC Session methods (writeStream, closeQuicStream, readStream, openStream). ALPN is now protocol-aware (configurable via ClientConfig.alpn). New apps: h3_client, quic_client, quic_server. All 6 interop directions verified (Zig↔Zig, Zig↔Go for both H3 and QUIC).
1 parent 220abfa commit 7795845

7 files changed

Lines changed: 646 additions & 13 deletions

File tree

‎apps/h3_client.zig‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const std = @import("std");
2+
const quic = @import("quic");
3+
const event_loop = quic.event_loop;
4+
const qpack = quic.qpack;
5+
6+
const H3Client = struct {
7+
pub const protocol: event_loop.Protocol = .h3;
8+
9+
alloc: std.mem.Allocator,
10+
got_response: bool = false,
11+
request_sent: bool = false,
12+
path: []const u8 = "/",
13+
14+
pub fn onConnected(self: *H3Client, session: *event_loop.ClientSession) void {
15+
std.debug.print("H3 connection established\n", .{});
16+
17+
const req_headers = [_]qpack.Header{
18+
.{ .name = ":method", .value = "GET" },
19+
.{ .name = ":scheme", .value = "https" },
20+
.{ .name = ":authority", .value = "localhost" },
21+
.{ .name = ":path", .value = self.path },
22+
.{ .name = "user-agent", .value = "quic-zig/1.0" },
23+
};
24+
const stream_id = session.sendRequest(&req_headers, null) catch |err| {
25+
std.debug.print("sendRequest error: {any}\n", .{err});
26+
return;
27+
};
28+
self.request_sent = true;
29+
std.debug.print("H3: sent GET {s} on stream {d}\n", .{ self.path, stream_id });
30+
}
31+
32+
pub fn onHeaders(_: *H3Client, _: *event_loop.ClientSession, stream_id: u64, headers: []const qpack.Header) void {
33+
std.debug.print("H3: response headers on stream {d}:\n", .{stream_id});
34+
for (headers) |h| {
35+
std.debug.print(" {s}: {s}\n", .{ h.name, h.value });
36+
}
37+
}
38+
39+
pub fn onData(self: *H3Client, session: *event_loop.ClientSession, stream_id: u64, len: usize) void {
40+
_ = stream_id;
41+
_ = len;
42+
var body_buf: [8192]u8 = undefined;
43+
while (true) {
44+
const n = session.recvBody(&body_buf);
45+
if (n == 0) break;
46+
std.debug.print("Response: {s}", .{body_buf[0..n]});
47+
}
48+
self.got_response = true;
49+
}
50+
51+
pub fn onFinished(self: *H3Client, session: *event_loop.ClientSession, stream_id: u64) void {
52+
std.debug.print("H3: stream {d} finished\n", .{stream_id});
53+
if (self.got_response) {
54+
session.closeConnection();
55+
}
56+
}
57+
};
58+
59+
pub fn main() !void {
60+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
61+
defer arena.deinit();
62+
const alloc = arena.allocator();
63+
64+
var port: u16 = 4434;
65+
var path: []const u8 = "/";
66+
var insecure = false;
67+
var args = std.process.args();
68+
_ = args.next();
69+
while (args.next()) |arg| {
70+
if (std.mem.eql(u8, arg, "--port")) {
71+
if (args.next()) |v| port = std.fmt.parseInt(u16, v, 10) catch 4434;
72+
} else if (std.mem.eql(u8, arg, "--path")) {
73+
if (args.next()) |v| path = v;
74+
} else if (std.mem.eql(u8, arg, "--insecure")) {
75+
insecure = true;
76+
}
77+
}
78+
79+
std.debug.print("H3 client connecting to 127.0.0.1:{d}\n", .{port});
80+
81+
var handler = H3Client{ .alloc = alloc, .path = path };
82+
var client = try event_loop.Client(H3Client).init(alloc, &handler, .{
83+
.port = port,
84+
.ca_cert_path = if (insecure) null else "interop/certs/ca.crt",
85+
.skip_cert_verify = insecure,
86+
});
87+
defer client.deinit();
88+
89+
try client.run();
90+
}

‎apps/quic_client.zig‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const std = @import("std");
2+
const quic = @import("quic");
3+
const event_loop = quic.event_loop;
4+
5+
const QuicEchoClient = struct {
6+
pub const protocol: event_loop.Protocol = .quic;
7+
8+
got_response: bool = false,
9+
stream_id: ?u64 = null,
10+
message: []const u8 = "Hello from Zig QUIC client!",
11+
12+
pub fn onConnected(self: *QuicEchoClient, session: *event_loop.ClientSession) void {
13+
std.debug.print("QUIC connection established\n", .{});
14+
15+
const sid = session.openStream() catch |err| {
16+
std.debug.print("openStream error: {any}\n", .{err});
17+
return;
18+
};
19+
self.stream_id = sid;
20+
21+
session.writeStream(sid, self.message) catch |err| {
22+
std.debug.print("writeStream error: {any}\n", .{err});
23+
return;
24+
};
25+
session.closeQuicStream(sid);
26+
std.debug.print("Sent on stream {d}: {s}\n", .{ sid, self.message });
27+
}
28+
29+
pub fn onStreamData(self: *QuicEchoClient, _: *event_loop.ClientSession, stream_id: u64, data: []const u8) void {
30+
std.debug.print("Response on stream {d}: {s}\n", .{ stream_id, data });
31+
self.got_response = true;
32+
}
33+
34+
pub fn onStreamFinished(self: *QuicEchoClient, session: *event_loop.ClientSession, stream_id: u64) void {
35+
std.debug.print("Stream {d} finished\n", .{stream_id});
36+
if (self.got_response) {
37+
session.closeConnection();
38+
}
39+
}
40+
};
41+
42+
pub fn main() !void {
43+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
44+
defer arena.deinit();
45+
const alloc = arena.allocator();
46+
47+
var port: u16 = 4434;
48+
var insecure = false;
49+
var args = std.process.args();
50+
_ = args.next();
51+
while (args.next()) |arg| {
52+
if (std.mem.eql(u8, arg, "--port")) {
53+
if (args.next()) |v| port = std.fmt.parseInt(u16, v, 10) catch 4434;
54+
} else if (std.mem.eql(u8, arg, "--insecure")) {
55+
insecure = true;
56+
}
57+
}
58+
59+
std.debug.print("QUIC echo client connecting to 127.0.0.1:{d}\n", .{port});
60+
61+
var handler = QuicEchoClient{};
62+
var client = try event_loop.Client(QuicEchoClient).init(alloc, &handler, .{
63+
.port = port,
64+
.ca_cert_path = if (insecure) null else "interop/certs/ca.crt",
65+
.skip_cert_verify = insecure,
66+
});
67+
defer client.deinit();
68+
69+
try client.run();
70+
}

‎apps/quic_server.zig‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const std = @import("std");
2+
const quic = @import("quic");
3+
const event_loop = quic.event_loop;
4+
5+
const QuicEchoHandler = struct {
6+
pub const protocol: event_loop.Protocol = .quic;
7+
8+
pub fn onStreamData(_: *QuicEchoHandler, session: *event_loop.Session, stream_id: u64, data: []const u8) void {
9+
std.log.info("stream {d} received: {s}", .{ stream_id, data });
10+
// Echo back
11+
session.writeStream(stream_id, data) catch |err| {
12+
std.log.err("writeStream error: {any}", .{err});
13+
return;
14+
};
15+
session.closeQuicStream(stream_id);
16+
std.log.info("stream {d} echoed {d} bytes", .{ stream_id, data.len });
17+
}
18+
19+
pub fn onStreamFinished(_: *QuicEchoHandler, _: *event_loop.Session, stream_id: u64) void {
20+
std.log.info("stream {d} finished", .{stream_id});
21+
}
22+
};
23+
24+
pub fn main() !void {
25+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
26+
defer arena.deinit();
27+
const alloc = arena.allocator();
28+
29+
var port: u16 = 4434;
30+
var args = std.process.args();
31+
_ = args.next();
32+
while (args.next()) |arg| {
33+
if (std.mem.eql(u8, arg, "--port")) {
34+
if (args.next()) |v| port = std.fmt.parseInt(u16, v, 10) catch 4434;
35+
}
36+
}
37+
38+
var handler = QuicEchoHandler{};
39+
var server = try event_loop.Server(QuicEchoHandler).init(alloc, &handler, .{
40+
.port = port,
41+
.cert_path = "interop/certs/server.crt",
42+
.key_path = "interop/certs/server.key",
43+
});
44+
defer server.deinit();
45+
46+
std.log.info("QUIC echo server listening on 127.0.0.1:{d}", .{port});
47+
try server.run();
48+
}

‎build.zig‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ pub fn build(b: *std.Build) void {
7373
if (b.args) |args| run_client.addArgs(args);
7474
b.step("run-client", "Run QUIC client").dependOn(&run_client.step);
7575

76+
// H3 client (event_loop)
77+
const exe_h3_client = App.add(b, "h3-client", "apps/h3_client.zig", target, optimize, need_libc, lib_mod);
78+
b.installArtifact(exe_h3_client);
79+
const run_h3_client = b.addRunArtifact(exe_h3_client);
80+
run_h3_client.step.dependOn(b.getInstallStep());
81+
if (b.args) |args| run_h3_client.addArgs(args);
82+
b.step("run-h3-client", "Run H3 client").dependOn(&run_h3_client.step);
83+
84+
// Raw QUIC server (event_loop)
85+
const exe_quic_server = App.add(b, "quic-server", "apps/quic_server.zig", target, optimize, need_libc, lib_mod);
86+
b.installArtifact(exe_quic_server);
87+
const run_quic_server = b.addRunArtifact(exe_quic_server);
88+
run_quic_server.step.dependOn(b.getInstallStep());
89+
b.step("run-quic-server", "Run raw QUIC echo server").dependOn(&run_quic_server.step);
90+
91+
// Raw QUIC client (event_loop)
92+
const exe_quic_client = App.add(b, "quic-client", "apps/quic_client.zig", target, optimize, need_libc, lib_mod);
93+
b.installArtifact(exe_quic_client);
94+
const run_quic_client = b.addRunArtifact(exe_quic_client);
95+
run_quic_client.step.dependOn(b.getInstallStep());
96+
if (b.args) |args| run_quic_client.addArgs(args);
97+
b.step("run-quic-client", "Run raw QUIC echo client").dependOn(&run_quic_client.step);
98+
7699
// WebTransport server
77100
const exe_wt_server = App.add(b, "wt-server", "apps/wt_server.zig", target, optimize, need_libc, lib_mod);
78101
b.installArtifact(exe_wt_server);

0 commit comments

Comments
 (0)