quicsilver 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +4 -5
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +81 -0
- data/Gemfile.lock +26 -4
- data/README.md +95 -31
- data/Rakefile +95 -3
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +1 -1
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- data/examples/README.md +44 -91
- data/examples/benchmark.rb +111 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +18 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/minimal_http3_server.rb +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +260 -0
- data/examples/simple_client_test.rb +2 -2
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/ext/quicsilver/extconf.rb +14 -0
- data/ext/quicsilver/quicsilver.c +568 -181
- data/lib/quicsilver/client/client.rb +349 -0
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/libmsquic.2.dylib +0 -0
- data/lib/quicsilver/protocol/adapter.rb +176 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
- data/lib/quicsilver/protocol/frame_parser.rb +142 -0
- data/lib/quicsilver/protocol/frame_reader.rb +55 -0
- data/lib/quicsilver/{http3.rb → protocol/frames.rb} +146 -30
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +275 -0
- data/lib/quicsilver/protocol/response_encoder.rb +97 -0
- data/lib/quicsilver/protocol/response_parser.rb +141 -0
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/quicsilver.bundle +0 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +138 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +610 -0
- data/lib/quicsilver/transport/configuration.rb +141 -0
- data/lib/quicsilver/transport/connection.rb +379 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +55 -14
- data/lib/rackup/handler/quicsilver.rb +1 -2
- data/quicsilver.gemspec +13 -3
- metadata +125 -21
- data/benchmarks/benchmark.rb +0 -68
- data/examples/setup_certs.sh +0 -57
- data/lib/quicsilver/client.rb +0 -261
- data/lib/quicsilver/connection.rb +0 -42
- data/lib/quicsilver/event_loop.rb +0 -38
- data/lib/quicsilver/http3/request_encoder.rb +0 -133
- data/lib/quicsilver/http3/request_parser.rb +0 -176
- data/lib/quicsilver/http3/response_encoder.rb +0 -186
- data/lib/quicsilver/http3/response_parser.rb +0 -160
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/quic_stream.rb +0 -36
- data/lib/quicsilver/request_registry.rb +0 -48
- data/lib/quicsilver/server.rb +0 -355
- data/lib/quicsilver/server_configuration.rb +0 -78
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example: Running a Rack app with protocol-http as the internal layer
|
|
5
|
+
#
|
|
6
|
+
# quicsilver uses protocol-http Request/Response internally.
|
|
7
|
+
# Rack apps are automatically wrapped — no code changes needed.
|
|
8
|
+
#
|
|
9
|
+
# Modes:
|
|
10
|
+
# :rack (default) — Rack app, wrapped with Protocol::Rack::Adapter
|
|
11
|
+
# :falcon — native protocol-http app, no wrapping
|
|
12
|
+
|
|
13
|
+
require_relative "example_helper"
|
|
14
|
+
|
|
15
|
+
# Any standard Rack app works
|
|
16
|
+
app = ->(env) {
|
|
17
|
+
puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{env['SERVER_PROTOCOL']}"
|
|
18
|
+
|
|
19
|
+
body = "Hello from Quicsilver!\n" \
|
|
20
|
+
"Method: #{env['REQUEST_METHOD']}\n" \
|
|
21
|
+
"Path: #{env['PATH_INFO']}\n" \
|
|
22
|
+
"Protocol: #{env['SERVER_PROTOCOL']}\n"
|
|
23
|
+
|
|
24
|
+
[200, { "content-type" => "text/plain" }, [body]]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
28
|
+
|
|
29
|
+
puts "Starting Quicsilver on https://localhost:4433"
|
|
30
|
+
puts "Test with: curl --http3 -k https://localhost:4433/"
|
|
31
|
+
server.start
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "quicsilver"
|
|
3
|
+
require_relative "example_helper"
|
|
5
4
|
|
|
6
5
|
puts "🚀 Rack HTTP/3 Server Example"
|
|
7
6
|
puts "=" * 40
|
|
@@ -31,24 +30,13 @@ app = ->(env) {
|
|
|
31
30
|
end
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
server = Quicsilver::Server.new(4433, app: app)
|
|
33
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
36
34
|
|
|
37
|
-
puts "🔧 Starting server..."
|
|
38
|
-
server.start
|
|
39
|
-
|
|
40
|
-
puts "✅ Server is running on port 4433"
|
|
41
35
|
puts "📋 Try these requests:"
|
|
42
|
-
puts " curl --http3 -k https://
|
|
43
|
-
puts " curl --http3 -k https://
|
|
44
|
-
puts " curl --http3 -k https://
|
|
36
|
+
puts " curl --http3 -k https://localhost:4433/"
|
|
37
|
+
puts " curl --http3 -k https://localhost:4433/api/users"
|
|
38
|
+
puts " curl --http3 -k https://localhost:4433/api/status"
|
|
39
|
+
puts "⏳ Press Ctrl+C to stop."
|
|
40
|
+
puts
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
puts "⏳ Server is running. Press Ctrl+C to stop..."
|
|
48
|
-
begin
|
|
49
|
-
server.wait_for_connections
|
|
50
|
-
rescue Interrupt
|
|
51
|
-
puts "\n🛑 Stopping server..."
|
|
52
|
-
server.stop
|
|
53
|
-
puts "👋 Server stopped"
|
|
54
|
-
end
|
|
42
|
+
server.start
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Tests all major quicsilver features against a running Rails app.
|
|
4
|
+
#
|
|
5
|
+
# Prerequisites:
|
|
6
|
+
# 1. Start the Rails app: cd blogz && bundle exec rackup -s quicsilver -p 4433
|
|
7
|
+
# 2. Run this script: cd quicsilver && bundle exec ruby examples/rails_feature_test.rb
|
|
8
|
+
#
|
|
9
|
+
# Tests: ping, streaming, priorities, echo, HEAD, multiplexing, connection pooling
|
|
10
|
+
|
|
11
|
+
require "bundler/setup"
|
|
12
|
+
require "quicsilver"
|
|
13
|
+
|
|
14
|
+
HOST = "localhost"
|
|
15
|
+
PORT = 4433
|
|
16
|
+
PASS = "✅"
|
|
17
|
+
FAIL = "❌"
|
|
18
|
+
|
|
19
|
+
def test(name)
|
|
20
|
+
result = yield
|
|
21
|
+
puts " #{PASS} #{name}"
|
|
22
|
+
result
|
|
23
|
+
rescue => e
|
|
24
|
+
puts " #{FAIL} #{name}: #{e.message}"
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "🚀 Quicsilver Rails Feature Test"
|
|
29
|
+
puts " Target: https://#{HOST}:#{PORT}"
|
|
30
|
+
puts "=" * 60
|
|
31
|
+
|
|
32
|
+
# === 1. Basic connectivity ===
|
|
33
|
+
puts "\n1️⃣ Basic Connectivity"
|
|
34
|
+
puts "-" * 60
|
|
35
|
+
|
|
36
|
+
test("GET /h3/ping returns JSON over HTTP/3") do
|
|
37
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
38
|
+
raise "Expected 200, got #{response[:status]}" unless response[:status] == 200
|
|
39
|
+
raise "Expected JSON" unless response[:body].include?('"status":"ok"')
|
|
40
|
+
raise "Expected HTTP/3" unless response[:body].include?("HTTP/3")
|
|
41
|
+
puts " Response: #{response[:body]}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# === 2. Connection Pooling ===
|
|
45
|
+
puts "\n2️⃣ Connection Pooling"
|
|
46
|
+
puts "-" * 60
|
|
47
|
+
|
|
48
|
+
test("Multiple requests reuse the same connection") do
|
|
49
|
+
5.times do |i|
|
|
50
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
|
+
Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
52
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
53
|
+
puts " Request #{i}: #{elapsed}ms (#{i == 0 ? 'new connection' : 'same connection'})"
|
|
54
|
+
end
|
|
55
|
+
puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
|
|
56
|
+
raise "Pool should have 1 connection" unless Quicsilver::Client.pool.size == 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# === 3. Streaming ===
|
|
60
|
+
puts "\n3️⃣ Streaming Response"
|
|
61
|
+
puts "-" * 60
|
|
62
|
+
|
|
63
|
+
test("GET /h3/stream returns chunked SSE data") do
|
|
64
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/stream", unsecure: true)
|
|
65
|
+
raise "Expected 200" unless response[:status] == 200
|
|
66
|
+
chunks = response[:body].scan(/data: chunk \d+/)
|
|
67
|
+
raise "Expected 5 chunks, got #{chunks.size}" unless chunks.size == 5
|
|
68
|
+
puts " Received #{chunks.size} streamed chunks"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# === 4. POST with Body (Echo) ===
|
|
72
|
+
puts "\n4️⃣ POST with Body"
|
|
73
|
+
puts "-" * 60
|
|
74
|
+
|
|
75
|
+
test("POST /h3/echo returns the request body") do
|
|
76
|
+
body = '{"message": "Hello HTTP/3!"}'
|
|
77
|
+
response = Quicsilver::Client.post(HOST, PORT, "/h3/echo",
|
|
78
|
+
body: body,
|
|
79
|
+
headers: { "content-type" => "application/json" },
|
|
80
|
+
unsecure: true)
|
|
81
|
+
raise "Expected 200" unless response[:status] == 200
|
|
82
|
+
raise "Body not echoed" unless response[:body].include?("Hello HTTP/3!")
|
|
83
|
+
puts " Echoed: #{response[:body][0..80]}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# === 5. HEAD Request ===
|
|
87
|
+
puts "\n5️⃣ HEAD Request"
|
|
88
|
+
puts "-" * 60
|
|
89
|
+
|
|
90
|
+
test("HEAD /h3/head returns headers but no body") do
|
|
91
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
92
|
+
response = client.head("/h3/head")
|
|
93
|
+
raise "Expected 200" unless response[:status] == 200
|
|
94
|
+
raise "HEAD should have empty body" unless response[:body].nil? || response[:body].empty?
|
|
95
|
+
puts " Status: #{response[:status]}, body size: #{response[:body]&.bytesize || 0}"
|
|
96
|
+
client.disconnect
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# === 6. Multiplexing (Sequential on single connection) ===
|
|
100
|
+
puts "\n6️⃣ Multiplexing (10 requests, single connection)"
|
|
101
|
+
puts "-" * 60
|
|
102
|
+
|
|
103
|
+
test("10 sequential requests on one connection") do
|
|
104
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
105
|
+
t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
106
|
+
10.times { client.get("/h3/ping") }
|
|
107
|
+
total = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000).round(1)
|
|
108
|
+
puts " 10 requests in #{total}ms (single connection)"
|
|
109
|
+
client.disconnect
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# === 7. Priority Endpoints (CSS vs Image) ===
|
|
113
|
+
puts "\n7️⃣ Priority Endpoints"
|
|
114
|
+
puts "-" * 60
|
|
115
|
+
|
|
116
|
+
test("GET /h3/css returns CSS content") do
|
|
117
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/css", unsecure: true)
|
|
118
|
+
raise "Expected 200" unless response[:status] == 200
|
|
119
|
+
puts " CSS: #{response[:body].bytesize} bytes"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
test("GET /h3/image returns large binary payload") do
|
|
123
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/image", unsecure: true)
|
|
124
|
+
raise "Expected 200" unless response[:status] == 200
|
|
125
|
+
raise "Expected 50KB" unless response[:body].bytesize == 50_000
|
|
126
|
+
puts " Image: #{response[:body].bytesize} bytes"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# === 8. Slow Response (Delay) ===
|
|
130
|
+
puts "\n8️⃣ Slow Response"
|
|
131
|
+
puts "-" * 60
|
|
132
|
+
|
|
133
|
+
test("GET /h3/slow?delay=0.2 respects delay") do
|
|
134
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
135
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/slow?delay=0.2", unsecure: true)
|
|
136
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
137
|
+
raise "Expected 200" unless response[:status] == 200
|
|
138
|
+
raise "Should take ~200ms, took #{elapsed}ms" unless elapsed > 150
|
|
139
|
+
puts " Delayed response in #{elapsed}ms"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# === 9. Concurrent Multiplexing ===
|
|
143
|
+
puts "\n9️⃣ Concurrent Multiplexing"
|
|
144
|
+
puts "-" * 60
|
|
145
|
+
|
|
146
|
+
test("4 concurrent requests on separate connections") do
|
|
147
|
+
threads = 4.times.map do |i|
|
|
148
|
+
Thread.new do
|
|
149
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
150
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/slow?delay=0.2", unsecure: true)
|
|
151
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
152
|
+
[i, response[:status], elapsed]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
results = threads.map(&:value).sort_by(&:first)
|
|
156
|
+
results.each { |i, status, elapsed| puts " Stream #{i}: #{status} — #{elapsed}ms" }
|
|
157
|
+
total = results.map { |_, _, e| e }.max
|
|
158
|
+
# All 4 should complete in roughly the same time (~200ms) not 4x (800ms)
|
|
159
|
+
raise "Expected concurrent, took #{total}ms" if total > 600
|
|
160
|
+
puts " All 4 concurrent in #{total}ms (not #{200 * 4}ms sequential)"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# === 10. Large Upload ===
|
|
164
|
+
puts "\n🔟 Large Upload"
|
|
165
|
+
puts "-" * 60
|
|
166
|
+
|
|
167
|
+
test("POST 100KB body") do
|
|
168
|
+
big_body = "x" * 100_000
|
|
169
|
+
response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
|
|
170
|
+
body: big_body,
|
|
171
|
+
headers: { "content-type" => "application/octet-stream" },
|
|
172
|
+
unsecure: true)
|
|
173
|
+
raise "Expected 200" unless response[:status] == 200
|
|
174
|
+
raise "Size mismatch" unless response[:body].include?('"received_bytes":100000')
|
|
175
|
+
puts " Uploaded 100KB, server received 100,000 bytes"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# === 11. Content-Length Validation ===
|
|
179
|
+
puts "\n1️⃣1️⃣ Content-Length Validation"
|
|
180
|
+
puts "-" * 60
|
|
181
|
+
|
|
182
|
+
test("POST with content-length header") do
|
|
183
|
+
body = "hello"
|
|
184
|
+
response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
|
|
185
|
+
body: body,
|
|
186
|
+
headers: { "content-type" => "text/plain" },
|
|
187
|
+
unsecure: true)
|
|
188
|
+
raise "Expected 200, got #{response[:status]}" unless response[:status] == 200
|
|
189
|
+
raise "Size mismatch" unless response[:body].include?('"received_bytes":5')
|
|
190
|
+
puts " Sent 5 bytes, server received 5 bytes"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# === 12. 0-RTT Reconnection ===
|
|
194
|
+
puts "\n1️⃣2️⃣ 0-RTT Reconnection"
|
|
195
|
+
puts "-" * 60
|
|
196
|
+
|
|
197
|
+
test("Two separate connections show handshake cost") do
|
|
198
|
+
Quicsilver::Client.close_pool
|
|
199
|
+
|
|
200
|
+
# First connection — full handshake
|
|
201
|
+
client1 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
202
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
203
|
+
client1.get("/h3/ping")
|
|
204
|
+
first = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t1) * 1000).round(1)
|
|
205
|
+
client1.disconnect
|
|
206
|
+
|
|
207
|
+
sleep 0.1
|
|
208
|
+
|
|
209
|
+
# Second connection — new handshake (0-RTT depends on MsQuic session cache)
|
|
210
|
+
client2 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
211
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
212
|
+
client2.get("/h3/ping")
|
|
213
|
+
second = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t2) * 1000).round(1)
|
|
214
|
+
client2.disconnect
|
|
215
|
+
|
|
216
|
+
puts " First connection: #{first}ms"
|
|
217
|
+
puts " Second connection: #{second}ms"
|
|
218
|
+
puts " Pooling saves: ~#{first}ms per request by avoiding handshakes"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# === 13. GOAWAY Graceful Shutdown ===
|
|
222
|
+
puts "\n1️⃣3️⃣ GOAWAY (server keeps serving during test)"
|
|
223
|
+
puts "-" * 60
|
|
224
|
+
|
|
225
|
+
test("Server responds after many requests (no GOAWAY yet)") do
|
|
226
|
+
response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
|
|
227
|
+
raise "Expected 200" unless response[:status] == 200
|
|
228
|
+
puts " Server still healthy after all tests"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# === 14. Rails CRUD ===
|
|
232
|
+
puts "\n1️⃣4️⃣ Rails CRUD"
|
|
233
|
+
puts "-" * 60
|
|
234
|
+
|
|
235
|
+
test("GET /posts.json returns posts array") do
|
|
236
|
+
response = Quicsilver::Client.get(HOST, PORT, "/posts.json", unsecure: true)
|
|
237
|
+
raise "Expected 200" unless response[:status] == 200
|
|
238
|
+
puts " Posts: #{response[:body][0..40]}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# === 15. HTTP Methods ===
|
|
242
|
+
puts "\n1️⃣5️⃣ HTTP Methods"
|
|
243
|
+
puts "-" * 60
|
|
244
|
+
|
|
245
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
246
|
+
%i[get post put patch delete head].each do |method|
|
|
247
|
+
test("#{method.to_s.upcase} /h3/ping") do
|
|
248
|
+
response = client.public_send(method, "/h3/ping")
|
|
249
|
+
body_size = response[:body]&.bytesize || 0
|
|
250
|
+
puts " #{response[:status]} (#{body_size} bytes)"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
client.disconnect
|
|
254
|
+
|
|
255
|
+
# === Summary ===
|
|
256
|
+
puts "\n" + "=" * 60
|
|
257
|
+
puts "🏁 Feature test complete!"
|
|
258
|
+
puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
|
|
259
|
+
Quicsilver::Client.close_pool
|
|
260
|
+
puts "👋 Done"
|
|
@@ -7,11 +7,11 @@ puts "🔌 Simple HTTP/3 Client Test"
|
|
|
7
7
|
puts "=" * 40
|
|
8
8
|
|
|
9
9
|
begin
|
|
10
|
-
client = Quicsilver::Client.new("
|
|
10
|
+
client = Quicsilver::Client.new("localhost", 4433, unsecure: true)
|
|
11
11
|
|
|
12
12
|
client.connect
|
|
13
13
|
|
|
14
|
-
response = client.get("/
|
|
14
|
+
response = client.get("/")
|
|
15
15
|
|
|
16
16
|
puts "Status: #{response[:status]}"
|
|
17
17
|
puts "Headers: #{response[:headers].inspect}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Server-Sent Events over HTTP/3.
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates streaming responses — data arrives chunk by chunk,
|
|
6
|
+
# not buffered. Each chunk is sent as a separate HTTP/3 DATA frame.
|
|
7
|
+
#
|
|
8
|
+
# ruby examples/streaming_sse.rb
|
|
9
|
+
# curl --http3-only -k https://localhost:4433/
|
|
10
|
+
|
|
11
|
+
require_relative "example_helper"
|
|
12
|
+
|
|
13
|
+
app = ->(env) {
|
|
14
|
+
body = Enumerator.new do |y|
|
|
15
|
+
10.times do |i|
|
|
16
|
+
y << "data: {\"count\":#{i},\"time\":\"#{Time.now.iso8601}\"}\n\n"
|
|
17
|
+
sleep 0.2
|
|
18
|
+
end
|
|
19
|
+
y << "data: {\"done\":true}\n\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
[200, { "content-type" => "text/event-stream", "cache-control" => "no-cache" }, body]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
26
|
+
|
|
27
|
+
puts "📡 Streaming SSE over HTTP/3"
|
|
28
|
+
puts " https://localhost:4433"
|
|
29
|
+
puts " curl --http3-only -k https://localhost:4433/"
|
|
30
|
+
puts " Events arrive every 200ms — no buffering."
|
|
31
|
+
puts
|
|
32
|
+
|
|
33
|
+
server.start
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# HTTP/3 Trailers — send headers after the body.
|
|
4
|
+
#
|
|
5
|
+
# Trailers let the server report status after streaming is complete.
|
|
6
|
+
# Useful for checksums, streaming error status, and gRPC.
|
|
7
|
+
#
|
|
8
|
+
# ruby examples/trailers.rb
|
|
9
|
+
|
|
10
|
+
require_relative "example_helper"
|
|
11
|
+
|
|
12
|
+
PORT = 4433
|
|
13
|
+
HOST = "localhost"
|
|
14
|
+
|
|
15
|
+
app = ->(env) {
|
|
16
|
+
path = env["PATH_INFO"]
|
|
17
|
+
|
|
18
|
+
case path
|
|
19
|
+
when "/stream-with-checksum"
|
|
20
|
+
# Stream data, then send a checksum trailer
|
|
21
|
+
body = ["chunk1\n", "chunk2\n", "chunk3\n"]
|
|
22
|
+
checksum = Digest::SHA256.hexdigest(body.join)[0..7]
|
|
23
|
+
|
|
24
|
+
[200,
|
|
25
|
+
{ "content-type" => "text/plain", "trailer" => "x-checksum" },
|
|
26
|
+
body]
|
|
27
|
+
# Note: trailers via Rack need the protocol-rack convention
|
|
28
|
+
# (see autoresearch.ideas.md for the Samuel Williams discussion)
|
|
29
|
+
|
|
30
|
+
when "/grpc-style"
|
|
31
|
+
# gRPC-style response: status comes in trailers, not headers
|
|
32
|
+
body = ['{"result":"processed"}']
|
|
33
|
+
|
|
34
|
+
[200,
|
|
35
|
+
{ "content-type" => "application/json" },
|
|
36
|
+
body]
|
|
37
|
+
|
|
38
|
+
else
|
|
39
|
+
[200, { "content-type" => "text/plain" }, ["Try /stream-with-checksum or /grpc-style"]]
|
|
40
|
+
end
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
44
|
+
server_thread = Thread.new { server.start }
|
|
45
|
+
sleep 0.3
|
|
46
|
+
|
|
47
|
+
puts "📎 HTTP/3 Trailers Demo"
|
|
48
|
+
puts "=" * 50
|
|
49
|
+
|
|
50
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
51
|
+
|
|
52
|
+
puts "\n Streaming with checksum trailer:"
|
|
53
|
+
response = client.get("/stream-with-checksum")
|
|
54
|
+
puts " Status: #{response[:status]}"
|
|
55
|
+
puts " Body: #{response[:body].inspect}"
|
|
56
|
+
|
|
57
|
+
puts "\n gRPC-style response:"
|
|
58
|
+
response = client.get("/grpc-style")
|
|
59
|
+
puts " Status: #{response[:status]}"
|
|
60
|
+
puts " Body: #{response[:body]}"
|
|
61
|
+
|
|
62
|
+
puts "\n Trailer support is built into the protocol layer."
|
|
63
|
+
puts " ResponseEncoder can send trailers after DATA frames."
|
|
64
|
+
puts " Full Rack integration needs protocol-rack update."
|
|
65
|
+
|
|
66
|
+
client.disconnect
|
|
67
|
+
server.stop
|
|
68
|
+
server_thread.join(2)
|
|
69
|
+
puts "\n✅ Done"
|
data/ext/quicsilver/extconf.rb
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
require 'mkmf'
|
|
2
2
|
|
|
3
|
+
# Skip compilation if precompiled binary is already present
|
|
4
|
+
ext_dir = File.expand_path('../../lib/quicsilver', __dir__)
|
|
5
|
+
if File.exist?(File.join(ext_dir, 'quicsilver.bundle')) || File.exist?(File.join(ext_dir, 'quicsilver.so'))
|
|
6
|
+
File.write('Makefile', "install:\n\t@echo 'Using precompiled binary'\n\nall:\n\t@echo 'Using precompiled binary'\n")
|
|
7
|
+
exit
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# On macOS, use Apple clang if available. Homebrew clang can't find
|
|
11
|
+
# system headers and produces broken binaries with MsQuic.
|
|
12
|
+
if RUBY_PLATFORM =~ /darwin/ && File.exist?("/usr/bin/clang")
|
|
13
|
+
RbConfig::CONFIG["CC"] = "/usr/bin/clang"
|
|
14
|
+
RbConfig::MAKEFILE_CONFIG["CC"] = "/usr/bin/clang"
|
|
15
|
+
end
|
|
16
|
+
|
|
3
17
|
# Find MSQUIC in the submodule
|
|
4
18
|
msquic_dir = File.expand_path('../../../vendor/msquic', __FILE__)
|
|
5
19
|
|