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
data/examples/README.md
CHANGED
|
@@ -1,105 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Examples
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Self-contained scripts demonstrating quicsilver features. Each boots its own server — no external setup needed.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
### 1. Generate Certificates
|
|
5
|
+
## Getting Started
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
# Server
|
|
9
|
+
ruby examples/minimal_http3_server.rb
|
|
10
|
+
curl --http3-only -k https://localhost:4433/
|
|
11
|
+
|
|
12
|
+
# Client
|
|
13
|
+
ruby examples/simple_client_test.rb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
|
|
18
|
+
| Script | Feature |
|
|
19
|
+
|--------|---------|
|
|
20
|
+
| `minimal_http3_server.rb` | Simplest HTTP/3 server |
|
|
21
|
+
| `rack_http3_server.rb` | Rack app with multiple routes |
|
|
22
|
+
| `protocol_http_server.rb` | Protocol-http mode |
|
|
23
|
+
| `simple_client_test.rb` | Basic client request |
|
|
24
|
+
| `connection_pool_demo.rb` | Connection reuse — 6ms first, 0.2ms reused |
|
|
25
|
+
| `feature_demo.rb` | All features in one script |
|
|
26
|
+
| `streaming_sse.rb` | Server-Sent Events over HTTP/3 |
|
|
27
|
+
| `priorities.rb` | Extensible Priorities (RFC 9218) — CSS before images |
|
|
28
|
+
| `trailers.rb` | Trailing headers after body |
|
|
29
|
+
| `grpc_style.rb` | gRPC-style request/response with JSON (no protobuf needed) |
|
|
30
|
+
| `falcon_middleware.rb` | Falcon's middleware stack over HTTP/3 (requires falcon gem) |
|
|
31
|
+
| `benchmark.rb` | Throughput benchmark |
|
|
32
|
+
| `rails_feature_test.rb` | 15 feature tests against a Rails app |
|
|
33
|
+
|
|
34
|
+
## Rails Integration
|
|
17
35
|
|
|
18
36
|
```bash
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### 3. Test the Connection
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# Terminal 2
|
|
27
|
-
ruby examples/test_connection.rb
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## 📁 Files
|
|
31
|
-
|
|
32
|
-
| File | Purpose |
|
|
33
|
-
|------|---------|
|
|
34
|
-
| `setup_certs.sh` | Generate certificates for testing |
|
|
35
|
-
| `test_server.rb` | Ruby QUIC server |
|
|
36
|
-
| `test_connection.rb` | QUIC client test |
|
|
37
|
-
|
|
38
|
-
## ✅ Expected Output
|
|
39
|
-
|
|
40
|
-
**Server:**
|
|
41
|
-
```
|
|
42
|
-
🔥 Quicsilver QUIC Server Test
|
|
43
|
-
════════════════════════════════════════
|
|
44
|
-
📋 Server Info:
|
|
45
|
-
server_id: a47271c7ae4276d5
|
|
46
|
-
address: 127.0.0.1
|
|
47
|
-
port: 4433
|
|
48
|
-
running: false
|
|
49
|
-
cert_file: /path/to/certs/server.crt
|
|
50
|
-
key_file: /path/to/certs/server.key
|
|
51
|
-
max_connections: 100
|
|
37
|
+
# 1. Add to Gemfile
|
|
38
|
+
gem "quicsilver"
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
🔗 Listening for connections...
|
|
56
|
-
🎯 Server is running! Press Ctrl+C to stop
|
|
57
|
-
```
|
|
40
|
+
# 2. Start
|
|
41
|
+
rackup -s quicsilver -p 4433
|
|
58
42
|
|
|
59
|
-
|
|
43
|
+
# 3. Test
|
|
44
|
+
curl --http3-only -k https://localhost:4433/
|
|
60
45
|
```
|
|
61
|
-
🔗 Testing QUIC Connection to 127.0.0.1:4433...
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
✅ SUCCESS! Connected to QUIC server
|
|
65
|
-
🕐 Connection time: 13.5ms
|
|
66
|
-
📊 Connected status: true
|
|
67
|
-
📋 Connection info: {"connected" => true, "failed" => false, ...}
|
|
68
|
-
|
|
69
|
-
🧪 Testing connection stability...
|
|
70
|
-
⏱️ Tick 1: Connected = true
|
|
71
|
-
⏱️ Tick 2: Connected = true
|
|
72
|
-
⏱️ Tick 3: Connected = true
|
|
73
|
-
✅ Connection test completed!
|
|
74
|
-
🔒 Connection closed cleanly
|
|
75
|
-
```
|
|
47
|
+
## Falcon Integration
|
|
76
48
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
If you prefer to create certificates manually:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
mkdir -p certs
|
|
83
|
-
cd certs
|
|
49
|
+
No extra gems or config needed — just pass Falcon's middleware:
|
|
84
50
|
|
|
85
|
-
|
|
86
|
-
|
|
51
|
+
```ruby
|
|
52
|
+
require "falcon"
|
|
53
|
+
require "quicsilver"
|
|
87
54
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
|
|
92
|
-
-addext "extendedKeyUsage=serverAuth"
|
|
55
|
+
middleware = Falcon::Server.middleware(Rails.application)
|
|
56
|
+
config = Quicsilver::Transport::Configuration.new(cert, key, mode: :falcon)
|
|
57
|
+
Quicsilver::Server.new(4433, app: middleware, server_configuration: config).start
|
|
93
58
|
```
|
|
94
|
-
|
|
95
|
-
## 🐛 Troubleshooting
|
|
96
|
-
|
|
97
|
-
**Port in use error:** The server automatically cleans up port 4433 before starting.
|
|
98
|
-
|
|
99
|
-
**Connection refused:** Make sure the server is running before testing the client.
|
|
100
|
-
|
|
101
|
-
**Certificate errors:** Run `./setup_certs.sh` to regenerate certificates with proper QUIC extensions.
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
That's it! No Docker, no external servers, just pure Ruby QUIC. 🎯
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Quicsilver benchmark — self-contained, boots its own server.
|
|
4
|
+
#
|
|
5
|
+
# ruby examples/benchmark.rb
|
|
6
|
+
|
|
7
|
+
require_relative "example_helper"
|
|
8
|
+
|
|
9
|
+
HOST = "localhost"
|
|
10
|
+
PORT = 4433
|
|
11
|
+
WARMUP = 10
|
|
12
|
+
ITERATIONS = 200
|
|
13
|
+
|
|
14
|
+
app = ->(env) {
|
|
15
|
+
case env["PATH_INFO"]
|
|
16
|
+
when "/large"
|
|
17
|
+
[200, { "content-type" => "application/octet-stream" }, ["x" * 50_000]]
|
|
18
|
+
else
|
|
19
|
+
[200, { "content-type" => "application/json" }, ['{"ok":true}']]
|
|
20
|
+
end
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
24
|
+
server_thread = Thread.new { server.start }
|
|
25
|
+
sleep 0.3
|
|
26
|
+
|
|
27
|
+
def run_benchmark(name, iterations)
|
|
28
|
+
times = iterations.times.map { yield }
|
|
29
|
+
total = times.sum
|
|
30
|
+
avg = (total / times.size).round(2)
|
|
31
|
+
p50 = times.sort[times.size / 2].round(2)
|
|
32
|
+
p99 = times.sort[(times.size * 0.99).to_i].round(2)
|
|
33
|
+
rps = (times.size / (total / 1000.0)).round(0)
|
|
34
|
+
puts " Total: #{total.round(1)}ms"
|
|
35
|
+
puts " Avg: #{avg}ms"
|
|
36
|
+
puts " p50: #{p50}ms"
|
|
37
|
+
puts " p99: #{p99}ms"
|
|
38
|
+
puts " RPS: #{rps} req/s"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts "🔨 Quicsilver Benchmark"
|
|
42
|
+
puts "=" * 60
|
|
43
|
+
|
|
44
|
+
# === 1. Sequential (single connection) ===
|
|
45
|
+
puts "\n📊 Sequential — single connection, #{ITERATIONS} requests"
|
|
46
|
+
puts "-" * 60
|
|
47
|
+
|
|
48
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
49
|
+
WARMUP.times { client.get("/ping") }
|
|
50
|
+
|
|
51
|
+
run_benchmark("Sequential", ITERATIONS) do
|
|
52
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
53
|
+
client.get("/ping")
|
|
54
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
|
|
55
|
+
end
|
|
56
|
+
client.disconnect
|
|
57
|
+
|
|
58
|
+
# === 2. Pooled ===
|
|
59
|
+
puts "\n📊 Pooled — connection pool, #{ITERATIONS} requests"
|
|
60
|
+
puts "-" * 60
|
|
61
|
+
|
|
62
|
+
WARMUP.times { Quicsilver::Client.get(HOST, PORT, "/ping", unsecure: true) }
|
|
63
|
+
|
|
64
|
+
run_benchmark("Pooled", ITERATIONS) do
|
|
65
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
Quicsilver::Client.get(HOST, PORT, "/ping", unsecure: true)
|
|
67
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
|
|
68
|
+
end
|
|
69
|
+
Quicsilver::Client.close_pool
|
|
70
|
+
|
|
71
|
+
# === 3. Large payload ===
|
|
72
|
+
puts "\n📊 Large payload — 50KB response, #{ITERATIONS} requests"
|
|
73
|
+
puts "-" * 60
|
|
74
|
+
|
|
75
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
76
|
+
WARMUP.times { client.get("/large") }
|
|
77
|
+
|
|
78
|
+
run_benchmark("Large", ITERATIONS) do
|
|
79
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
80
|
+
client.get("/large")
|
|
81
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
client.disconnect
|
|
85
|
+
|
|
86
|
+
# === 4. True multiplexing ===
|
|
87
|
+
puts "\n📊 True multiplexing — concurrent streams, single connection"
|
|
88
|
+
puts "-" * 60
|
|
89
|
+
|
|
90
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
91
|
+
WARMUP.times { client.get("/ping") }
|
|
92
|
+
|
|
93
|
+
[1, 5, 10, 20, 50].each do |concurrent|
|
|
94
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
95
|
+
requests = concurrent.times.map { client.get("/ping") { |req| req } }
|
|
96
|
+
responses = requests.map(&:response)
|
|
97
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
98
|
+
ok = responses.count { |r| r[:status] == 200 }
|
|
99
|
+
rps = (concurrent / (elapsed / 1000.0)).round(0)
|
|
100
|
+
puts " #{concurrent.to_s.rjust(3)} streams: #{elapsed.to_s.rjust(7)}ms #{ok}/#{concurrent} ok #{rps} req/s"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
client.disconnect
|
|
104
|
+
|
|
105
|
+
# Cleanup
|
|
106
|
+
Quicsilver::Client.close_pool
|
|
107
|
+
server.stop
|
|
108
|
+
server_thread.join(2)
|
|
109
|
+
|
|
110
|
+
puts "\n" + "=" * 60
|
|
111
|
+
puts "✅ Benchmark complete"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstrates connection pooling.
|
|
4
|
+
#
|
|
5
|
+
# ruby examples/connection_pool_demo.rb
|
|
6
|
+
#
|
|
7
|
+
# To compare with main (no pooling), checkout main and run:
|
|
8
|
+
#
|
|
9
|
+
# ruby examples/simple_client_test.rb
|
|
10
|
+
#
|
|
11
|
+
# Each request on main pays the full QUIC handshake cost.
|
|
12
|
+
# With pooling, only the first request pays it.
|
|
13
|
+
|
|
14
|
+
require_relative "example_helper"
|
|
15
|
+
|
|
16
|
+
PORT = 4433
|
|
17
|
+
HOST = "localhost"
|
|
18
|
+
|
|
19
|
+
# Start a simple server in-process
|
|
20
|
+
app = ->(env) { [200, { "content-type" => "text/plain" }, ["Hello from #{env['PATH_INFO']}"]] }
|
|
21
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
22
|
+
server_thread = Thread.new { server.start }
|
|
23
|
+
sleep 0.3
|
|
24
|
+
|
|
25
|
+
puts "Connection Pool Demo"
|
|
26
|
+
puts "=" * 50
|
|
27
|
+
|
|
28
|
+
puts "\n🔄 Client.get — pooling is automatic"
|
|
29
|
+
puts "-" * 50
|
|
30
|
+
|
|
31
|
+
6.times do |i|
|
|
32
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
33
|
+
response = Quicsilver::Client.get(HOST, PORT, "/request-#{i}", unsecure: true)
|
|
34
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
35
|
+
|
|
36
|
+
label = i == 0 ? "← new connection + QUIC handshake" : "← reused"
|
|
37
|
+
puts " Request #{i}: #{response[:status]} — #{elapsed}ms #{label}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
puts "\n Pool: #{Quicsilver::Client.pool.size} connection(s) ready for reuse"
|
|
41
|
+
|
|
42
|
+
# --- Cleanup ---
|
|
43
|
+
Quicsilver::Client.close_pool
|
|
44
|
+
server.stop
|
|
45
|
+
server_thread.join(2)
|
|
46
|
+
|
|
47
|
+
puts "\n✅ Done"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared helper for examples — uses the `localhost` gem to generate
|
|
4
|
+
# self-signed TLS certificates so examples work without manual setup.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# require_relative "example_helper"
|
|
8
|
+
# server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
9
|
+
|
|
10
|
+
require "bundler/setup"
|
|
11
|
+
require "quicsilver"
|
|
12
|
+
require "localhost/authority"
|
|
13
|
+
|
|
14
|
+
authority = Localhost::Authority.fetch
|
|
15
|
+
EXAMPLE_TLS_CONFIG = Quicsilver::Transport::Configuration.new(
|
|
16
|
+
authority.certificate_path,
|
|
17
|
+
authority.key_path
|
|
18
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Quicsilver with Falcon's middleware stack over HTTP/3.
|
|
4
|
+
#
|
|
5
|
+
# Falcon provides caching, content encoding, and protocol-rack.
|
|
6
|
+
# Quicsilver provides the HTTP/3 transport.
|
|
7
|
+
#
|
|
8
|
+
# Prerequisites: gem install falcon
|
|
9
|
+
#
|
|
10
|
+
# ruby examples/falcon_middleware.rb
|
|
11
|
+
# curl --http3-only -k https://localhost:4433/
|
|
12
|
+
|
|
13
|
+
require_relative "example_helper"
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
require "falcon"
|
|
17
|
+
rescue LoadError
|
|
18
|
+
puts "❌ Falcon not installed. Run: gem install falcon"
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
app = ->(env) {
|
|
23
|
+
[200, { "content-type" => "application/json" }, [
|
|
24
|
+
%({"protocol":"#{env['SERVER_PROTOCOL']}","server":"quicsilver+falcon"})
|
|
25
|
+
]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Falcon's middleware adds caching, content encoding, etc.
|
|
29
|
+
middleware = Falcon::Server.middleware(app)
|
|
30
|
+
|
|
31
|
+
config = Quicsilver::Transport::Configuration.new(
|
|
32
|
+
EXAMPLE_TLS_CONFIG.cert_file,
|
|
33
|
+
EXAMPLE_TLS_CONFIG.key_file,
|
|
34
|
+
mode: :falcon
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
server = Quicsilver::Server.new(4433, app: middleware, server_configuration: config)
|
|
38
|
+
|
|
39
|
+
puts "🦅 Quicsilver + Falcon Middleware"
|
|
40
|
+
puts " https://localhost:4433"
|
|
41
|
+
puts " curl --http3-only -k https://localhost:4433/"
|
|
42
|
+
puts
|
|
43
|
+
|
|
44
|
+
server.start
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstrates all major quicsilver features in one script.
|
|
4
|
+
#
|
|
5
|
+
# ruby examples/feature_demo.rb
|
|
6
|
+
|
|
7
|
+
require_relative "example_helper"
|
|
8
|
+
|
|
9
|
+
PORT = 4433
|
|
10
|
+
HOST = "localhost"
|
|
11
|
+
|
|
12
|
+
# === Server with multiple endpoints ===
|
|
13
|
+
app = ->(env) {
|
|
14
|
+
path = env["PATH_INFO"]
|
|
15
|
+
method = env["REQUEST_METHOD"]
|
|
16
|
+
|
|
17
|
+
case path
|
|
18
|
+
when "/"
|
|
19
|
+
[200, { "content-type" => "text/plain" }, ["Hello HTTP/3!\n"]]
|
|
20
|
+
|
|
21
|
+
when "/api/users"
|
|
22
|
+
[200, { "content-type" => "application/json" },
|
|
23
|
+
['{"users":["alice","bob","charlie"]}']]
|
|
24
|
+
|
|
25
|
+
when "/stream"
|
|
26
|
+
# Streaming body — chunks sent as they're generated
|
|
27
|
+
body = Enumerator.new do |y|
|
|
28
|
+
5.times do |i|
|
|
29
|
+
y << "chunk #{i}\n"
|
|
30
|
+
sleep 0.01
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
[200, { "content-type" => "text/plain" }, body]
|
|
34
|
+
|
|
35
|
+
when "/large"
|
|
36
|
+
# Large response for priority testing
|
|
37
|
+
[200, { "content-type" => "text/plain" }, ["x" * 10_000]]
|
|
38
|
+
|
|
39
|
+
when "/echo"
|
|
40
|
+
# Echo POST body back
|
|
41
|
+
body = env["rack.input"]&.read || ""
|
|
42
|
+
[200, { "content-type" => "text/plain", "content-length" => body.bytesize.to_s }, [body]]
|
|
43
|
+
|
|
44
|
+
else
|
|
45
|
+
[404, { "content-type" => "text/plain" }, ["Not Found"]]
|
|
46
|
+
end
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
50
|
+
server_thread = Thread.new { server.start }
|
|
51
|
+
sleep 0.3
|
|
52
|
+
|
|
53
|
+
puts "🚀 Quicsilver Feature Demo"
|
|
54
|
+
puts "=" * 60
|
|
55
|
+
|
|
56
|
+
# === 1. Connection Pooling ===
|
|
57
|
+
puts "\n1️⃣ Connection Pooling"
|
|
58
|
+
puts "-" * 60
|
|
59
|
+
|
|
60
|
+
6.times do |i|
|
|
61
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
62
|
+
response = Quicsilver::Client.get(HOST, PORT, "/", unsecure: true)
|
|
63
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
64
|
+
label = i == 0 ? "← handshake" : "← reused"
|
|
65
|
+
puts " Request #{i}: #{response[:status]} — #{elapsed}ms #{label}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# === 2. Multiple Endpoints ===
|
|
69
|
+
puts "\n2️⃣ Multiple Endpoints"
|
|
70
|
+
puts "-" * 60
|
|
71
|
+
|
|
72
|
+
["/", "/api/users", "/stream", "/nonexistent"].each do |path|
|
|
73
|
+
response = Quicsilver::Client.get(HOST, PORT, path, unsecure: true)
|
|
74
|
+
body_preview = response[:body][0..50].gsub("\n", "\\n")
|
|
75
|
+
puts " GET #{path} → #{response[:status]} | #{body_preview}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# === 3. POST with Body ===
|
|
79
|
+
puts "\n3️⃣ POST with Echo"
|
|
80
|
+
puts "-" * 60
|
|
81
|
+
|
|
82
|
+
response = Quicsilver::Client.post(HOST, PORT, "/echo",
|
|
83
|
+
body: "Hello from quicsilver client!",
|
|
84
|
+
headers: { "content-type" => "text/plain" },
|
|
85
|
+
unsecure: true)
|
|
86
|
+
puts " POST /echo → #{response[:status]} | #{response[:body]}"
|
|
87
|
+
|
|
88
|
+
# === 4. Concurrent Requests ===
|
|
89
|
+
puts "\n4️⃣ Concurrent Requests (multiplexing)"
|
|
90
|
+
puts "-" * 60
|
|
91
|
+
|
|
92
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
93
|
+
t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
94
|
+
10.times do |i|
|
|
95
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
96
|
+
response = client.get("/api/users")
|
|
97
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
|
|
98
|
+
puts " Request #{i}: #{response[:status]} — #{elapsed}ms"
|
|
99
|
+
end
|
|
100
|
+
total = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000).round(1)
|
|
101
|
+
puts " All 10 completed in #{total}ms (single connection)"
|
|
102
|
+
client.disconnect
|
|
103
|
+
|
|
104
|
+
# === 5. HTTP Methods ===
|
|
105
|
+
puts "\n5️⃣ HTTP Methods"
|
|
106
|
+
puts "-" * 60
|
|
107
|
+
|
|
108
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
109
|
+
%i[get post put patch delete head].each do |method|
|
|
110
|
+
response = client.public_send(method, "/api/users")
|
|
111
|
+
body_size = response[:body]&.bytesize || 0
|
|
112
|
+
puts " #{method.to_s.upcase.ljust(6)} /api/users → #{response[:status]} (#{body_size} bytes)"
|
|
113
|
+
end
|
|
114
|
+
client.disconnect
|
|
115
|
+
|
|
116
|
+
# === Summary ===
|
|
117
|
+
puts "\n" + "=" * 60
|
|
118
|
+
puts "✅ All features working!"
|
|
119
|
+
puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
|
|
120
|
+
|
|
121
|
+
# === Cleanup ===
|
|
122
|
+
Quicsilver::Client.close_pool
|
|
123
|
+
server.stop
|
|
124
|
+
server_thread.join(2)
|
|
125
|
+
puts "👋 Done"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# gRPC-style request/response over HTTP/3.
|
|
4
|
+
#
|
|
5
|
+
# gRPC is just HTTP with:
|
|
6
|
+
# - content-type: application/grpc (or application/grpc+json)
|
|
7
|
+
# - 5-byte frame prefix: [compressed(1)][length(4)][message]
|
|
8
|
+
# - Status in trailers: grpc-status, grpc-message
|
|
9
|
+
#
|
|
10
|
+
# This example uses JSON for simplicity. In production you'd use
|
|
11
|
+
# protobuf (google-protobuf gem) for smaller, faster serialization.
|
|
12
|
+
# Quicsilver carries the bytes — the app chooses the encoding.
|
|
13
|
+
#
|
|
14
|
+
# ruby examples/grpc_style.rb
|
|
15
|
+
|
|
16
|
+
require_relative "example_helper"
|
|
17
|
+
require "json"
|
|
18
|
+
|
|
19
|
+
PORT = 4433
|
|
20
|
+
HOST = "localhost"
|
|
21
|
+
|
|
22
|
+
# gRPC frame: [0x00][4-byte big-endian length][message]
|
|
23
|
+
def grpc_encode(message)
|
|
24
|
+
data = message.to_json
|
|
25
|
+
[0, data.bytesize].pack("CN") + data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def grpc_decode(frame)
|
|
29
|
+
_compressed, length = frame.unpack("CN")
|
|
30
|
+
JSON.parse(frame[5, length])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
app = ->(env) {
|
|
34
|
+
path = env["PATH_INFO"]
|
|
35
|
+
body = env["rack.input"]&.read || ""
|
|
36
|
+
|
|
37
|
+
case path
|
|
38
|
+
when "/grpc.UserService/GetUser"
|
|
39
|
+
request = grpc_decode(body) rescue { "error" => "bad frame" }
|
|
40
|
+
user = { "id" => request["id"], "name" => "Alice", "email" => "alice@example.com" }
|
|
41
|
+
response_frame = grpc_encode(user)
|
|
42
|
+
|
|
43
|
+
[200,
|
|
44
|
+
{ "content-type" => "application/grpc+json" },
|
|
45
|
+
[response_frame]]
|
|
46
|
+
|
|
47
|
+
when "/grpc.UserService/ListUsers"
|
|
48
|
+
users = [
|
|
49
|
+
{ "id" => 1, "name" => "Alice" },
|
|
50
|
+
{ "id" => 2, "name" => "Bob" },
|
|
51
|
+
{ "id" => 3, "name" => "Charlie" }
|
|
52
|
+
]
|
|
53
|
+
response_frame = grpc_encode(users)
|
|
54
|
+
|
|
55
|
+
[200,
|
|
56
|
+
{ "content-type" => "application/grpc+json" },
|
|
57
|
+
[response_frame]]
|
|
58
|
+
|
|
59
|
+
else
|
|
60
|
+
[404, { "content-type" => "text/plain" }, ["Unknown gRPC method: #{path}"]]
|
|
61
|
+
end
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
65
|
+
server_thread = Thread.new { server.start }
|
|
66
|
+
sleep 0.3
|
|
67
|
+
|
|
68
|
+
puts "🔌 gRPC-style over HTTP/3 (JSON, no protobuf)"
|
|
69
|
+
puts "=" * 50
|
|
70
|
+
|
|
71
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
72
|
+
|
|
73
|
+
# GetUser
|
|
74
|
+
puts "\n GetUser(id=1):"
|
|
75
|
+
request_frame = grpc_encode({ "id" => 1 })
|
|
76
|
+
response = client.post("/grpc.UserService/GetUser",
|
|
77
|
+
body: request_frame,
|
|
78
|
+
headers: { "content-type" => "application/grpc+json" })
|
|
79
|
+
user = grpc_decode(response[:body])
|
|
80
|
+
puts " → #{user}"
|
|
81
|
+
|
|
82
|
+
# ListUsers
|
|
83
|
+
puts "\n ListUsers():"
|
|
84
|
+
request_frame = grpc_encode({})
|
|
85
|
+
response = client.post("/grpc.UserService/ListUsers",
|
|
86
|
+
body: request_frame,
|
|
87
|
+
headers: { "content-type" => "application/grpc+json" })
|
|
88
|
+
users = grpc_decode(response[:body])
|
|
89
|
+
puts " → #{users}"
|
|
90
|
+
|
|
91
|
+
puts "\n gRPC is just HTTP + framing. Quicsilver carries the bytes."
|
|
92
|
+
puts " This example uses JSON — swap in protobuf for production."
|
|
93
|
+
|
|
94
|
+
client.disconnect
|
|
95
|
+
server.stop
|
|
96
|
+
server_thread.join(2)
|
|
97
|
+
puts "\n✅ Done"
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "quicsilver"
|
|
3
|
+
require_relative "example_helper"
|
|
5
4
|
|
|
6
5
|
puts "🚀 Minimal HTTP/3 Server Example"
|
|
7
6
|
puts "=" * 40
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
server = Quicsilver::Server.new(4433)
|
|
8
|
+
server = Quicsilver::Server.new(4433, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
11
9
|
|
|
12
|
-
puts "
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
puts "✅ Server is running on port 4433"
|
|
16
|
-
puts "📋 Server info: #{server.server_info}"
|
|
10
|
+
puts "✅ Listening on https://localhost:4433"
|
|
11
|
+
puts "⏳ Press Ctrl+C to stop."
|
|
12
|
+
puts
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
puts "⏳ Server is running. Press Ctrl+C to stop..."
|
|
20
|
-
begin
|
|
21
|
-
server.wait_for_connections
|
|
22
|
-
rescue Interrupt
|
|
23
|
-
puts "\n🛑 Stopping server..."
|
|
24
|
-
server.stop
|
|
25
|
-
puts "👋 Server stopped"
|
|
26
|
-
end
|
|
14
|
+
server.start
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# HTTP/3 Extensible Priorities (RFC 9218).
|
|
4
|
+
#
|
|
5
|
+
# Browsers send priority hints: CSS is urgency 0 (highest),
|
|
6
|
+
# images are urgency 5 (low). Quicsilver parses these and tells
|
|
7
|
+
# MsQuic to send high-priority data first.
|
|
8
|
+
#
|
|
9
|
+
# ruby examples/priorities.rb
|
|
10
|
+
#
|
|
11
|
+
# Then from another terminal, the client fires CSS and image
|
|
12
|
+
# requests and shows the priority was parsed.
|
|
13
|
+
|
|
14
|
+
require_relative "example_helper"
|
|
15
|
+
|
|
16
|
+
PORT = 4433
|
|
17
|
+
HOST = "localhost"
|
|
18
|
+
|
|
19
|
+
app = ->(env) {
|
|
20
|
+
path = env["PATH_INFO"]
|
|
21
|
+
|
|
22
|
+
case path
|
|
23
|
+
when "/style.css"
|
|
24
|
+
# Browsers send: priority: u=0 (highest urgency)
|
|
25
|
+
[200, { "content-type" => "text/css" }, ["body { margin: 0; }\n" * 50]]
|
|
26
|
+
when "/image.png"
|
|
27
|
+
# Browsers send: priority: u=5 (low urgency)
|
|
28
|
+
[200, { "content-type" => "image/png" }, ["x" * 10_000]]
|
|
29
|
+
else
|
|
30
|
+
[200, { "content-type" => "text/html" }, [
|
|
31
|
+
"<link rel='stylesheet' href='/style.css'>\n<img src='/image.png'>\n"
|
|
32
|
+
]]
|
|
33
|
+
end
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
|
|
37
|
+
server_thread = Thread.new { server.start }
|
|
38
|
+
sleep 0.3
|
|
39
|
+
|
|
40
|
+
puts "🎯 HTTP/3 Priorities Demo"
|
|
41
|
+
puts "=" * 50
|
|
42
|
+
|
|
43
|
+
client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
|
|
44
|
+
|
|
45
|
+
# Request with priority header (what a browser would send)
|
|
46
|
+
puts "\n CSS request (high priority):"
|
|
47
|
+
response = client.get("/style.css", headers: { "priority" => "u=0, i" })
|
|
48
|
+
puts " Status: #{response[:status]}, Size: #{response[:body].bytesize} bytes"
|
|
49
|
+
|
|
50
|
+
puts "\n Image request (low priority):"
|
|
51
|
+
response = client.get("/image.png", headers: { "priority" => "u=5" })
|
|
52
|
+
puts " Status: #{response[:status]}, Size: #{response[:body].bytesize} bytes"
|
|
53
|
+
|
|
54
|
+
puts "\n MsQuic schedules CSS data before image data"
|
|
55
|
+
puts " when both are in flight on the same connection."
|
|
56
|
+
|
|
57
|
+
client.disconnect
|
|
58
|
+
server.stop
|
|
59
|
+
server_thread.join(2)
|
|
60
|
+
puts "\n✅ Done"
|