quicsilver 0.3.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/cibuildgem.yaml +93 -0
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +32 -0
  6. data/Gemfile.lock +20 -2
  7. data/README.md +92 -29
  8. data/Rakefile +67 -2
  9. data/benchmarks/concurrent.rb +2 -2
  10. data/benchmarks/rails.rb +3 -3
  11. data/benchmarks/throughput.rb +2 -2
  12. data/examples/README.md +44 -91
  13. data/examples/benchmark.rb +111 -0
  14. data/examples/connection_pool_demo.rb +47 -0
  15. data/examples/example_helper.rb +18 -0
  16. data/examples/falcon_middleware.rb +44 -0
  17. data/examples/feature_demo.rb +125 -0
  18. data/examples/grpc_style.rb +97 -0
  19. data/examples/minimal_http3_server.rb +6 -18
  20. data/examples/priorities.rb +60 -0
  21. data/examples/protocol_http_server.rb +31 -0
  22. data/examples/rack_http3_server.rb +8 -20
  23. data/examples/rails_feature_test.rb +260 -0
  24. data/examples/simple_client_test.rb +2 -2
  25. data/examples/streaming_sse.rb +33 -0
  26. data/examples/trailers.rb +69 -0
  27. data/ext/quicsilver/extconf.rb +14 -0
  28. data/ext/quicsilver/quicsilver.c +39 -0
  29. data/lib/quicsilver/client/client.rb +138 -39
  30. data/lib/quicsilver/client/connection_pool.rb +106 -0
  31. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  32. data/lib/quicsilver/protocol/adapter.rb +176 -0
  33. data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
  34. data/lib/quicsilver/protocol/frame_parser.rb +142 -0
  35. data/lib/quicsilver/protocol/frame_reader.rb +55 -0
  36. data/lib/quicsilver/protocol/frames.rb +18 -7
  37. data/lib/quicsilver/protocol/priority.rb +56 -0
  38. data/lib/quicsilver/protocol/qpack/encoder.rb +39 -1
  39. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
  40. data/lib/quicsilver/protocol/request_parser.rb +28 -140
  41. data/lib/quicsilver/protocol/response_encoder.rb +27 -2
  42. data/lib/quicsilver/protocol/response_parser.rb +22 -130
  43. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  44. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  45. data/lib/quicsilver/quicsilver.bundle +0 -0
  46. data/lib/quicsilver/server/request_handler.rb +96 -44
  47. data/lib/quicsilver/server/server.rb +316 -42
  48. data/lib/quicsilver/transport/configuration.rb +10 -1
  49. data/lib/quicsilver/transport/connection.rb +92 -63
  50. data/lib/quicsilver/version.rb +1 -1
  51. data/lib/quicsilver.rb +26 -3
  52. data/quicsilver.gemspec +10 -2
  53. metadata +69 -5
  54. data/examples/setup_certs.sh +0 -57
data/examples/README.md CHANGED
@@ -1,105 +1,58 @@
1
- # Quicsilver Examples
1
+ # Examples
2
2
 
3
- This directory contains examples for testing your Ruby QUIC implementation.
3
+ Self-contained scripts demonstrating quicsilver features. Each boots its own server — no external setup needed.
4
4
 
5
- ## šŸš€ Quick Start
6
-
7
- ### 1. Generate Certificates
5
+ ## Getting Started
8
6
 
9
7
  ```bash
10
- cd examples
11
- ./setup_certs.sh
12
- ```
13
-
14
- This creates the necessary certificate files in the `certs/` directory.
15
-
16
- ### 2. Run the Server
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
- # Terminal 1
20
- ruby examples/test_server.rb
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
- šŸš€ Starting QUIC server on 127.0.0.1:4433
54
- āœ… QUIC server started successfully!
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
- **Client:**
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
- Connected to 127.0.0.1:4433
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
- ## šŸ”§ Manual Certificate Setup
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
- # Generate private key
86
- openssl genrsa -out server.key 2048
51
+ ```ruby
52
+ require "falcon"
53
+ require "quicsilver"
87
54
 
88
- # Generate certificate with proper QUIC extensions
89
- openssl req -new -x509 -key server.key -out server.crt -days 365 \
90
- -subj "/CN=localhost/O=QuicsilverTest/C=US" \
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
- require "bundler/setup"
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
- # Create and start the server
10
- server = Quicsilver::Server.new(4433)
8
+ server = Quicsilver::Server.new(4433, server_configuration: EXAMPLE_TLS_CONFIG)
11
9
 
12
- puts "šŸ”§ Starting server..."
13
- server.start
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
- # Keep the server running
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"