async-websocket 0.13.1 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 237b59048febd538e6112ea06b520c154386d057bd3b58a22bb153d45cd3209f
4
- data.tar.gz: '0966a1666e31463adb1c2975f7e72534c7a8003794825ab94c24c3d5d02223e7'
3
+ metadata.gz: cc7cbd8769e7a5f2c98114c19e3e5b17b8e65b1d172e961b2efaca084b108dda
4
+ data.tar.gz: 25f69580b9e2f8839c2e3408bb926430eb720e9ec98ff483b4e69d9c3202f207
5
5
  SHA512:
6
- metadata.gz: 7e25c38a3d25b98209d3999e803f031cb216dd1ccd47f1c2af92fd75cab7859d888ee9fd79873aab8d41d9d6e1ff4bd3bf64addcba74bad619613c948ec17e89
7
- data.tar.gz: a7284a36f8525415327177da55e83d35001f329d50b4b1843e4cd0b4d2488b17da26559df0248e493bea86ac163e0f3434113a6b76e4d1c9a16978e3c4118a41
6
+ metadata.gz: dee41300f15a14eab436499d4ffaf3ec6906bd23000b4f2d1d5db996d70d43aec64f63d4c444731888eed71ecb6258a3361db643b782c815b85e74a062e45e7e
7
+ data.tar.gz: 68228e3e5abb6ac3eab178ecaf4d7a2e4556d6baa99c088b4b0bab6788cef669d1fdd1dae54a512b042b586fe35f94b2031d0540aa7261d3c2af26cffc5eaad5
@@ -2,11 +2,13 @@ language: ruby
2
2
  dist: xenial
3
3
  cache: bundler
4
4
 
5
+ script: bundle exec rspec
6
+
5
7
  matrix:
6
8
  include:
7
- - rvm: 2.4
8
9
  - rvm: 2.5
9
10
  - rvm: 2.6
11
+ - rvm: 2.7
10
12
  - rvm: 2.6
11
13
  env: COVERAGE=PartialSummary,Coveralls
12
14
  - rvm: truffleruby
@@ -16,3 +18,4 @@ matrix:
16
18
  allow_failures:
17
19
  - rvm: ruby-head
18
20
  - rvm: jruby-head
21
+ - rvm: truffleruby
data/Gemfile CHANGED
@@ -4,9 +4,4 @@ gemspec
4
4
 
5
5
  group :test do
6
6
  gem 'rack-test'
7
- gem 'pry'
8
- end
9
-
10
- group :development do
11
- gem 'tty-progressbar'
12
7
  end
data/README.md CHANGED
@@ -6,6 +6,8 @@ A simple asynchronous websocket client/server implementation for [HTTP/1](https:
6
6
  [![Code Climate](https://codeclimate.com/github/socketry/async-websocket.svg)](https://codeclimate.com/github/socketry/async-websocket)
7
7
  [![Coverage Status](https://coveralls.io/repos/socketry/async-websocket/badge.svg)](https://coveralls.io/r/socketry/async-websocket)
8
8
 
9
+ [![WebSocket Client & Server for Ruby](https://img.youtube.com/vi/aHop4Yyjs_o/0.jpg)](https://www.youtube.com/watch?v=aHop4Yyjs_o)
10
+
9
11
  ## Installation
10
12
 
11
13
  Add this line to your application's Gemfile:
@@ -48,7 +50,7 @@ Async do |task|
48
50
 
49
51
  endpoint = Async::HTTP::Endpoint.parse(URL)
50
52
 
51
- Async::WebSocket::Client.open(endpoint) do |connection|
53
+ Async::WebSocket::Client.connect(endpoint) do |connection|
52
54
  input_task = task.async do
53
55
  while line = stdin.read_until("\n")
54
56
  connection.write({user: USER, text: line})
@@ -96,6 +98,18 @@ run lambda {|env|
96
98
  }
97
99
  ```
98
100
 
101
+ ### Force HTTP/1 Connection
102
+
103
+ This forces the endpoint to connect using `HTTP/1.1`.
104
+
105
+ ```ruby
106
+ endpoint = Async::HTTP::Endpoint.parse("https://remote-server.com", alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
107
+
108
+ Async::WebSocket::Client.connect(endpoint) do ...
109
+ ```
110
+
111
+ You may want to use this if the server advertises `HTTP/2` but doesn't support `HTTP/2` for WebSocket connections.
112
+
99
113
  ## Contributing
100
114
 
101
115
  1. Fork it
@@ -9,21 +9,21 @@ Gem::Specification.new do |spec|
9
9
  spec.summary = %q{An async websocket library on top of websocket-driver.}
10
10
  spec.homepage = ""
11
11
  spec.license = "MIT"
12
-
12
+
13
13
  spec.files = `git ls-files -z`.split("\x0")
14
14
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
15
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
16
  spec.require_paths = ["lib"]
17
17
 
18
18
  spec.add_dependency "async-io", "~> 1.23"
19
- spec.add_dependency "async-http", "~> 0.43"
19
+ spec.add_dependency "async-http", "~> 0.51"
20
20
  spec.add_dependency "protocol-websocket", "~> 0.7.0"
21
21
 
22
22
  spec.add_development_dependency "async-rspec"
23
- spec.add_development_dependency "falcon", "~> 0.32"
23
+ spec.add_development_dependency "falcon", "~> 0.34"
24
24
 
25
25
  spec.add_development_dependency "covered"
26
26
  spec.add_development_dependency "bundler"
27
27
  spec.add_development_dependency "rspec", "~> 3.6"
28
- spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "bake-bundler"
29
29
  end
@@ -0,0 +1,3 @@
1
+ export RUBY_FIBER_VM_STACK_SIZE=0
2
+ export RUBY_FIBER_MACHINE_STACK_SIZE=0
3
+ export RUBY_SHARED_FIBER_POOL_FREE_STACKS=0
@@ -1,3 +1,147 @@
1
1
  # The Journey to One Million
2
2
 
3
+ ## Allocations per Connection
3
4
 
5
+ ```
6
+ Array: 188498 allocations
7
+ Hash: 137041 allocations
8
+ String: 91387 allocations
9
+ Proc: 81242 allocations
10
+ Fiber: 30169 allocations
11
+ Async::Task: 30168 allocations
12
+ Async::IO::Buffer: 20904 allocations
13
+ Protocol::HTTP2::Window: 20162 allocations
14
+ Set: 20091 allocations
15
+ Async::Queue: 20082 allocations
16
+ Method: 20006 allocations
17
+ Protocol::HTTP::Headers::Merged: 10100 allocations
18
+ Protocol::HTTP::Headers: 10100 allocations
19
+ Async::Condition: 10002 allocations
20
+ Protocol::WebSocket::Framer: 10001 allocations
21
+ Async::HTTP::Body::Stream: 10001 allocations
22
+ Async::HTTP::Body::Hijack: 10001 allocations
23
+ Async::WebSocket::ConnectResponse: 10001 allocations
24
+ Async::WebSocket::Connection: 10001 allocations
25
+ Async::HTTP::Body::Writable: 10001 allocations
26
+ Async::HTTP::Protocol::HTTP2::Request::Stream: 10001 allocations
27
+ Async::HTTP::Protocol::HTTP2::Request: 10001 allocations
28
+ Falcon::Adapters::Input: 10001 allocations
29
+ Protocol::HTTP::Headers::Split: 10001 allocations
30
+ Async::HTTP::Protocol::HTTP2::Stream::Input: 10001 allocations
31
+ Async::HTTP::Protocol::HTTP2::Stream::Output: 10001 allocations
32
+ ** 80.98830116988302 objects per connection.
33
+ ```
34
+
35
+ ## System Limits
36
+
37
+ ### Fiber Performance
38
+
39
+ To improve fiber performance:
40
+
41
+ export RUBY_FIBER_VM_STACK_SIZE=0
42
+ export RUBY_FIBER_MACHINE_STACK_SIZE=0
43
+ export RUBY_SHARED_FIBER_POOL_FREE_STACKS=0
44
+
45
+ `RUBY_SHARED_FIBER_POOL_FREE_STACKS` is an experimental feature on `ruby-head`.
46
+
47
+ ### FiberError: can't set a guard page: Cannot allocate memory
48
+
49
+ This error occurs because the operating system has limited resources for allocating fiber stacks.
50
+
51
+ You can find the current limit:
52
+
53
+ % sysctl vm.max_map_count
54
+ vm.max_map_count = 65530
55
+
56
+ You can increase it:
57
+
58
+ % sysctl -w vm.max_map_count=2500000
59
+
60
+ ## Logs
61
+
62
+ ### 2020
63
+
64
+ ```
65
+ koyoko% ./multi-client.rb -c 100000
66
+ 0.15s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:11:39 +1300]
67
+ | Made 1 connections: 202.88 connections/second...
68
+ 0.15s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:11:39 +1300]
69
+ | GC.start duration=0.0s GC.count=27
70
+ 8.82s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:11:47 +1300]
71
+ | Made 10001 connections: 1152.85 connections/second...
72
+ 8.89s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:11:47 +1300]
73
+ | GC.start duration=0.06s GC.count=28
74
+ 17.7s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:11:56 +1300]
75
+ | Made 20001 connections: 1139.39 connections/second...
76
+ 17.84s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:11:56 +1300]
77
+ | GC.start duration=0.14s GC.count=29
78
+ 26.71s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:05 +1300]
79
+ | Made 30001 connections: 1129.44 connections/second...
80
+ 26.9s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:05 +1300]
81
+ | GC.start duration=0.19s GC.count=30
82
+ 35.97s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:14 +1300]
83
+ | Made 40001 connections: 1116.59 connections/second...
84
+ 36.22s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:15 +1300]
85
+ | GC.start duration=0.25s GC.count=31
86
+ 45.41s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:24 +1300]
87
+ | Made 50001 connections: 1104.74 connections/second...
88
+ 45.65s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:24 +1300]
89
+ | GC.start duration=0.24s GC.count=32
90
+ 54.94s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:33 +1300]
91
+ | Made 60001 connections: 1095.18 connections/second...
92
+ 55.3s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:34 +1300]
93
+ | GC.start duration=0.37s GC.count=33
94
+ 1m4s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:43 +1300]
95
+ | Made 70001 connections: 1085.98 connections/second...
96
+ 1m5s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:43 +1300]
97
+ | GC.start duration=0.44s GC.count=34
98
+ 1m14s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:53 +1300]
99
+ | Made 80001 connections: 1074.6 connections/second...
100
+ 1m14s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:12:53 +1300]
101
+ | GC.start duration=0.36s GC.count=35
102
+ 1m24s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:13:03 +1300]
103
+ | Made 90001 connections: 1066.37 connections/second...
104
+ 1m24s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:13:03 +1300]
105
+ | GC.start duration=0.35s GC.count=36
106
+ 1m34s info: Command [oid=0x2f8] [pid=820099] [2020-02-04 23:13:13 +1300]
107
+ | Finished top level connection loop...
108
+ ```
109
+
110
+ ### 2019
111
+
112
+ This report is affected by `tty-progressbar` bugs.
113
+
114
+ ```
115
+ koyoko% bundle exec ./multi-client.rb --count 100000
116
+ 145.94 connection/s [ ] 1/100000 ( 6s/ 0s)
117
+ 0.11s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:29:58 +1200]
118
+ | GC.start -> 0.01s
119
+ 591.10 connection/s [========== ] 10001/100000 ( 1m52s/12s)
120
+ 12.92s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:30:11 +1200]
121
+ | GC.start -> 0.3s
122
+ 455.06 connection/s [==================== ] 20001/100000 ( 1m42s/25s)
123
+ 26.17s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:30:24 +1200]
124
+ | GC.start -> 0.45s
125
+ 294.11 connection/s [============================= ] 30001/100000 ( 1m31s/39s)
126
+ 39.95s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:30:38 +1200]
127
+ | GC.start -> 0.68s
128
+ 153.08 connection/s [======================================= ] 40001/100000 ( 1m19s/53s)
129
+ 53.9s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:30:52 +1200]
130
+ | GC.start -> 0.8s
131
+ 23.03 connection/s [================================================ ] 50001/100000 ( 1m 7s/ 1m 7s)
132
+ 1m8s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:31:07 +1200]
133
+ | GC.start -> 0.95s
134
+ 0.87552 connection/s [========================================================== ] 60001/100000 (55s/ 1m23s)
135
+ 1m24s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:31:23 +1200]
136
+ | GC.start -> 1.04s
137
+ 0.74375 connection/s [==================================================================== ] 70001/100000 (43s/ 1m42s)
138
+ 1m43s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:31:42 +1200]
139
+ | GC.start -> 1.17s
140
+ 0.64832 connection/s [============================================================================== ] 80001/100000 (30s/ 2m 2s)
141
+ 2m4s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:32:02 +1200]
142
+ | GC.start -> 1.29s
143
+ 0.57842 connection/s [======================================================================================= ] 90001/100000 (16s/ 2m26s)
144
+ 2m27s info: #<Command:0x000055de596579e8> [pid=14815] [2019-07-08 00:32:26 +1200]
145
+ | GC.start -> 1.55s
146
+ 435.05 connection/s [=================================================================================================] 100000/100000 ( 0s/ 2m50s)
147
+ ```
@@ -13,10 +13,21 @@ class Room
13
13
  def initialize
14
14
  @connections = Set.new
15
15
  @semaphore = Async::Semaphore.new(512)
16
+
17
+ @count = 0
18
+ @profile = nil
16
19
  end
17
20
 
18
21
  def connect connection
19
22
  @connections << connection
23
+
24
+ @count += 1
25
+
26
+ if (@count % 10000).zero?
27
+ # (full_mark: false, immediate_sweep: false)
28
+ duration = Async::Clock.measure{GC.start}
29
+ Async.logger.info(self) {"GC.start duration=#{duration.round(2)}s GC.count=#{GC.count} @connections.count=#{@connections.count}"}
30
+ end
20
31
  end
21
32
 
22
33
  def disconnect connection
@@ -58,6 +69,28 @@ class Room
58
69
  end
59
70
  end
60
71
 
72
+ def start_profile
73
+ require 'ruby-prof' unless defined?(RubyProf)
74
+
75
+ return false if @profile
76
+
77
+ @profile = RubyProf::Profile.new(merge_fibers: true)
78
+ @profile.start
79
+ end
80
+
81
+ def stop_profile
82
+ return false unless @profile
83
+
84
+ result = @profile.stop
85
+ printer = RubyProf::FlatPrinter.new(result)
86
+ printer.print(STDOUT, min_percent: 0.5)
87
+
88
+ # printer = RubyProf::GraphPrinter.new(result)
89
+ # printer.print(STDOUT, min_percent: 0.5)
90
+
91
+ @profile = nil
92
+ end
93
+
61
94
  def command(code)
62
95
  Async.logger.warn self, "eval(#{code})"
63
96
 
@@ -82,6 +115,10 @@ class Room
82
115
  def open(connection)
83
116
  self.connect(connection)
84
117
 
118
+ if @connections.size == 1_000_000
119
+ connection.write("Congratulations, you have completed the journey to one million! 🥳 👏👏👏🏼")
120
+ end
121
+
85
122
  while message = connection.read
86
123
  if message[:text] =~ /^\/(.*?)$/
87
124
  begin
@@ -8,9 +8,6 @@ require 'async/http/endpoint'
8
8
  require_relative '../../lib/async/websocket/client'
9
9
 
10
10
  require 'samovar'
11
- require 'ruby-prof'
12
-
13
- require 'tty/progressbar'
14
11
 
15
12
  GC.disable
16
13
 
@@ -30,14 +27,12 @@ class Command < Samovar::Command
30
27
  end
31
28
 
32
29
  def call
33
- endpoint = Async::HTTP::Endpoint.parse(@options[:connect], local_address: self.local_address)
30
+ endpoint = Async::HTTP::Endpoint.parse(@options[:connect])
31
+ # endpoint = endpoint.each.first
32
+
34
33
  count = @options[:count]
35
34
 
36
35
  connections = Async::Queue.new
37
- progress = TTY::ProgressBar.new(":rate connection/s [:bar] :current/:total (:eta/:elapsed)", total: count)
38
-
39
- # profile = RubyProf::Profile.new(merge_fibers: true)
40
- # profile.start
41
36
 
42
37
  Async do |task|
43
38
  task.logger.info!
@@ -45,36 +40,39 @@ class Command < Samovar::Command
45
40
  task.async do |subtask|
46
41
  while connection = connections.dequeue
47
42
  subtask.async(connection) do |subtask, connection|
48
- pp connection.read
49
-
50
43
  while message = connection.read
51
- pp message
44
+ puts "> #{message.inspect}"
52
45
  end
53
46
  ensure
54
47
  connection.close
55
48
  end
56
49
  end
57
50
 
58
- # subtask.children.each(&:stop)
51
+ GC.start
59
52
  end
60
53
 
61
54
  client = Async::WebSocket::Client.open(endpoint)
55
+ start_time = Async::Clock.now
62
56
 
63
57
  count.times do |i|
64
58
  connections.enqueue(client.connect(endpoint.path))
65
- progress.advance(1)
59
+
60
+ if (i % 10000).zero?
61
+ count = i+1
62
+ duration = Async::Clock.now - start_time
63
+ Async.logger.info(self) {"Made #{count} connections: #{(count/duration).round(2)} connections/second..."}
64
+ end
65
+
66
+ if (i % 10000).zero?
67
+ duration = Async::Clock.measure{GC.start(full_mark: false, immediate_sweep: false)}
68
+ Async.logger.info(self) {"GC.start duration=#{duration.round(2)}s GC.count=#{GC.count}"}
69
+ end
66
70
  end
67
71
 
68
72
  connections.enqueue(nil)
73
+
74
+ Async.logger.info(self) {"Finished top level connection loop..."}
69
75
  end
70
-
71
- # ensure
72
- # result = profile.stop
73
- # printer = RubyProf::FlatPrinter.new(result)
74
- # printer.print(STDOUT, min_percent: 0.5)
75
- #
76
- # printer = RubyProf::GraphPrinter.new(result)
77
- # printer.print(STDOUT, min_percent: 0.5)
78
76
  end
79
77
  end
80
78
 
@@ -15,7 +15,7 @@ Async do |task|
15
15
 
16
16
  endpoint = Async::HTTP::Endpoint.parse(URL)
17
17
 
18
- Async::WebSocket::Client.open(endpoint) do |connection|
18
+ Async::WebSocket::Client.connect(endpoint) do |connection|
19
19
  task.async do
20
20
  $stdout.write "> "
21
21
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env falcon serve --count 1 --bind http://127.0.0.1:7070 -c
1
+ #!/usr/bin/env -S falcon serve --count 1 --bind http://127.0.0.1:7070 -c
2
2
 
3
3
  require 'async/websocket/adapters/rack'
4
4
 
@@ -124,7 +124,7 @@ class Server
124
124
  end
125
125
 
126
126
  def call(env)
127
- Async::WebSocket::Adapters::Rack.open(env, connect: User) do |user|
127
+ Async::WebSocket::Adapters::Rack.open(env, handler: User) do |user|
128
128
  @entrance.enter(user)
129
129
 
130
130
  while message = user.read
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'async'
4
- require 'async/io/stream'
5
4
  require 'async/http/endpoint'
6
5
  require 'async/websocket/client'
7
6
 
@@ -11,7 +10,7 @@ Async do |task|
11
10
  endpoint = Async::HTTP::Endpoint.parse(URL)
12
11
 
13
12
  Async::WebSocket::Client.connect(endpoint) do |connection|
14
- connection.write ["Hello World"]
13
+ connection.write ["Hello", "World"]
15
14
 
16
15
  while message = connection.read
17
16
  p message
@@ -3,11 +3,9 @@
3
3
  require 'async/websocket/adapters/rack'
4
4
 
5
5
  app = lambda do |env|
6
- Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
7
- p [env["REMOTE_ADDR"], "connected", env["VERSION"]]
6
+ Async::WebSocket::Adapters::Rack.open(env) do |connection|
8
7
  message = connection.read
9
- p message
10
- connection.write message
8
+ connection.write message.reverse
11
9
  end
12
10
  end
13
11
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  source "https://rubygems.org"
3
3
 
4
- gem "utopia", "~> 2.9.0"
4
+ gem "utopia", "~> 2.9.3"
5
5
 
6
6
  gem "rake"
7
7
  gem "bundler"
@@ -35,20 +35,26 @@ module Async
35
35
 
36
36
  def self.open(env, headers: [], protocols: [], handler: Connection, **options, &block)
37
37
  if request = env['async.http.request'] and Array(request.protocol).include?(PROTOCOL)
38
+ env = nil
39
+
38
40
  # Select websocket sub-protocol:
39
41
  if requested_protocol = request.headers[SEC_WEBSOCKET_PROTOCOL]
40
42
  protocol = (requested_protocol & protocols).first
41
43
  end
42
44
 
43
45
  response = Response.for(request, headers, protocol: protocol, **options) do |stream|
46
+ response = nil
47
+
44
48
  framer = Protocol::WebSocket::Framer.new(stream)
45
49
 
46
50
  connection = handler.call(framer, protocol)
51
+
47
52
  yield connection
48
53
 
49
54
  connection.close unless connection.closed?
50
55
  end
51
56
 
57
+ request = nil
52
58
  headers = response.headers
53
59
 
54
60
  if protocol = response.protocol
@@ -34,6 +34,7 @@ module Async
34
34
  class Client < ::Protocol::HTTP::Middleware
35
35
  include ::Protocol::WebSocket::Headers
36
36
 
37
+ # @return [Client] a client which can be used to establish websocket connections to the given endpoint.
37
38
  def self.open(endpoint, *args, &block)
38
39
  client = self.new(HTTP::Client.new(endpoint, *args), mask: endpoint.secure?)
39
40
 
@@ -46,6 +47,7 @@ module Async
46
47
  end
47
48
  end
48
49
 
50
+ # @return [Connection] an open websocket connection to the given endpoint.
49
51
  def self.connect(endpoint, *args, **options, &block)
50
52
  self.open(endpoint, *args) do |client|
51
53
  connection = client.connect(endpoint.path, **options)
@@ -60,33 +62,47 @@ module Async
60
62
  end
61
63
  end
62
64
 
63
- def initialize(delegate, **options)
64
- super(delegate)
65
+ def initialize(client, **options)
66
+ super(client)
65
67
 
66
68
  @options = options
67
69
  end
68
70
 
69
- def connect(path, headers: [], handler: Connection, **options)
71
+ class Framer < ::Protocol::WebSocket::Framer
72
+ def initialize(pool, connection, stream)
73
+ super(stream)
74
+ @pool = pool
75
+ @connection = connection
76
+ end
77
+
78
+ def close
79
+ super
80
+
81
+ if @pool
82
+ @pool.release(@connection)
83
+ @pool = nil
84
+ @connection = nil
85
+ end
86
+ end
87
+ end
88
+
89
+ def connect(path, headers: nil, handler: Connection, **options, &block)
90
+ headers = ::Protocol::HTTP::Headers[headers]
70
91
  request = Request.new(nil, nil, path, headers, **options)
71
92
 
72
- response = self.call(request)
93
+ pool = @delegate.pool
94
+ connection = pool.acquire
95
+
96
+ response = request.call(connection)
73
97
 
74
98
  unless response.stream?
75
99
  raise ProtocolError, "Failed to negotiate connection: #{response.status}"
76
100
  end
77
101
 
78
102
  protocol = response.headers[SEC_WEBSOCKET_PROTOCOL]&.first
79
- framer = Protocol::WebSocket::Framer.new(response.stream)
80
-
81
- connection = handler.call(framer, protocol, **@options)
82
-
83
- return connection unless block_given?
103
+ framer = Framer.new(pool, connection, response.stream)
84
104
 
85
- begin
86
- yield connection
87
- ensure
88
- connection.close
89
- end
105
+ handler.call(framer, protocol, **@options, &block)
90
106
  end
91
107
  end
92
108
  end
@@ -34,17 +34,19 @@ module Async
34
34
  include ::Protocol::WebSocket::Headers
35
35
 
36
36
  class Wrapper
37
- def initialize(output, response)
38
- @output = output
39
- @body = output
37
+ def initialize(stream, response)
40
38
  @response = response
41
- @stream = nil
39
+ @body = @response.body
40
+ @stream = stream
42
41
  end
43
42
 
43
+ attr_accessor :response
44
+
44
45
  attr_accessor :body
46
+ attr_accessor :stream
45
47
 
46
48
  def stream?
47
- @response.success?
49
+ @response.success? and @stream
48
50
  end
49
51
 
50
52
  def status
@@ -62,30 +64,39 @@ module Async
62
64
  def protocol
63
65
  @response.protocol
64
66
  end
67
+ end
68
+
69
+ class Hijack < Async::HTTP::Body::Readable
70
+ def initialize(request)
71
+ @request = request
72
+ @stream = nil
73
+ end
74
+
75
+ attr :stream
65
76
 
66
- def stream
67
- @stream ||= Async::HTTP::Body::Stream.new(@response.body, @output)
77
+ def call(stream)
78
+ @stream = stream
68
79
  end
69
80
  end
70
81
 
71
- def initialize(request, protocols: [], version: 13)
72
- body = Async::HTTP::Body::Writable.new
82
+ def initialize(request, protocols: [], version: 13, &block)
83
+ body = Hijack.new(self)
73
84
 
74
- headers = []
85
+ headers = ::Protocol::HTTP::Headers[request.headers]
75
86
 
76
- headers << [SEC_WEBSOCKET_VERSION, version]
87
+ headers.add(SEC_WEBSOCKET_VERSION, String(version))
77
88
 
78
89
  if protocols.any?
79
- headers << [SEC_WEBSOCKET_PROTOCOL, protocols.join(',')]
90
+ headers.add(SEC_WEBSOCKET_PROTOCOL, protocols.join(','))
80
91
  end
81
92
 
82
- merged_headers = ::Protocol::HTTP::Headers::Merged.new(request.headers, headers)
83
-
84
- super(request.scheme, request.authority, ::Protocol::HTTP::Methods::CONNECT, request.path, nil, merged_headers, body, PROTOCOL)
93
+ super(request.scheme, request.authority, ::Protocol::HTTP::Methods::CONNECT, request.path, nil, headers, body, PROTOCOL)
85
94
  end
86
95
 
87
96
  def call(connection)
88
- Wrapper.new(body, super)
97
+ response = super
98
+
99
+ Wrapper.new(@body.stream, response)
89
100
  end
90
101
  end
91
102
  end
@@ -30,10 +30,10 @@ module Async
30
30
  include ::Protocol::WebSocket::Headers
31
31
 
32
32
  def initialize(request, headers = nil, protocol: nil, &block)
33
- headers = Protocol::HTTP::Headers::Merged.new(headers)
33
+ headers = ::Protocol::HTTP::Headers[headers]
34
34
 
35
35
  if protocol
36
- headers << [[SEC_WEBSOCKET_PROTOCOL, protocol]]
36
+ headers.add(SEC_WEBSOCKET_PROTOCOL, protocol)
37
37
  end
38
38
 
39
39
  body = Async::HTTP::Body::Hijack.wrap(request, &block)
@@ -32,12 +32,30 @@ module Async
32
32
  include ::Protocol::WebSocket::Headers
33
33
 
34
34
  def self.call(framer, protocol = [], **options)
35
- return self.new(framer, Array(protocol).first, **options)
35
+ instance = self.new(framer, Array(protocol).first, **options)
36
+
37
+ return instance unless block_given?
38
+
39
+ begin
40
+ yield instance
41
+ ensure
42
+ instance.close
43
+ end
36
44
  end
37
45
 
38
- def initialize(framer, protocol = nil, **options)
46
+ def initialize(framer, protocol = nil, response: nil, **options)
39
47
  super(framer, **options)
40
48
  @protocol = protocol
49
+ @response = response
50
+ end
51
+
52
+ def close
53
+ super
54
+
55
+ if @response
56
+ @response.finish
57
+ @response = nil
58
+ end
41
59
  end
42
60
 
43
61
  attr :protocol
@@ -30,7 +30,7 @@ module Async
30
30
  Array(request.protocol).include?(PROTOCOL)
31
31
  end
32
32
 
33
- def initialize(scheme = nil, authority = nil, path = nil, headers = [], **options)
33
+ def initialize(scheme = nil, authority = nil, path = nil, headers = nil, **options, &block)
34
34
  @scheme = scheme
35
35
  @authority = authority
36
36
  @path = path
@@ -27,7 +27,7 @@ module Async
27
27
  module WebSocket
28
28
  module Response
29
29
  # Send the request to the given connection.
30
- def self.for(request, headers = [], **options, &body)
30
+ def self.for(request, headers = nil, **options, &body)
31
31
  if request.version =~ /http\/1/i
32
32
  return UpgradeResponse.new(request, headers, **options, &body)
33
33
  elsif request.version =~ /h2/i
@@ -49,6 +49,8 @@ module Async
49
49
  # Select websocket sub-protocol:
50
50
  protocol = select_protocol(request)
51
51
 
52
+ # request.headers = nil
53
+
52
54
  Response.for(request, headers, protocol: protocol, **options) do |stream|
53
55
  framer = Protocol::WebSocket::Framer.new(stream)
54
56
 
@@ -42,6 +42,8 @@ module Async
42
42
  @stream = nil
43
43
  end
44
44
 
45
+ attr_accessor :response
46
+
45
47
  def stream?
46
48
  @response.status == 101
47
49
  end
@@ -71,21 +73,19 @@ module Async
71
73
  end
72
74
  end
73
75
 
74
- def initialize(request, protocols: [], version: 13)
76
+ def initialize(request, protocols: [], version: 13, &block)
75
77
  @key = Nounce.generate_key
76
78
 
77
- headers = [
78
- [SEC_WEBSOCKET_KEY, @key],
79
- [SEC_WEBSOCKET_VERSION, version],
80
- ]
79
+ headers = ::Protocol::HTTP::Headers[request.headers]
80
+
81
+ headers.add(SEC_WEBSOCKET_KEY, @key)
82
+ headers.add(SEC_WEBSOCKET_VERSION, String(version))
81
83
 
82
84
  if protocols.any?
83
- headers << [SEC_WEBSOCKET_PROTOCOL, protocols.join(',')]
85
+ headers.add(SEC_WEBSOCKET_PROTOCOL, protocols.join(','))
84
86
  end
85
87
 
86
- merged_headers = ::Protocol::HTTP::Headers::Merged.new(request.headers, headers)
87
-
88
- super(request.scheme, request.authority, ::Protocol::HTTP::Methods::GET, request.path, nil, merged_headers, nil, PROTOCOL)
88
+ super(request.scheme, request.authority, ::Protocol::HTTP::Methods::GET, request.path, nil, headers, nil, PROTOCOL)
89
89
  end
90
90
 
91
91
  def call(connection)
@@ -31,17 +31,17 @@ module Async
31
31
  include ::Protocol::WebSocket::Headers
32
32
 
33
33
  def initialize(request, headers = nil, protocol: nil, &block)
34
- headers = Protocol::HTTP::Headers::Merged.new(headers)
34
+ headers = ::Protocol::HTTP::Headers[headers]
35
35
 
36
36
  if accept_nounce = request.headers[SEC_WEBSOCKET_KEY]&.first
37
- headers << [[SEC_WEBSOCKET_ACCEPT, Nounce.accept_digest(accept_nounce)]]
37
+ headers.add(SEC_WEBSOCKET_ACCEPT, Nounce.accept_digest(accept_nounce))
38
38
  status = 101
39
39
  else
40
40
  status = 400
41
41
  end
42
42
 
43
43
  if protocol
44
- headers << [[SEC_WEBSOCKET_PROTOCOL, protocol]]
44
+ headers.add(SEC_WEBSOCKET_PROTOCOL, protocol)
45
45
  end
46
46
 
47
47
  body = Async::HTTP::Body::Hijack.wrap(request, &block)
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module WebSocket
23
- VERSION = "0.13.1"
23
+ VERSION = "0.14.0"
24
24
  end
25
25
  end
@@ -72,10 +72,46 @@ RSpec.shared_context Async::WebSocket::Server do
72
72
  it "can establish connection" do
73
73
  connection = client.connect("/server")
74
74
 
75
- expect(connection.read).to be == message
76
- expect(connection.read).to be_nil
77
- expect(connection).to be_closed
75
+ begin
76
+ expect(connection.read).to be == message
77
+ expect(connection.read).to be_nil
78
+ expect(connection).to be_closed
79
+ ensure
80
+ connection.close
81
+ end
82
+ end
83
+
84
+ context "with headers" do
85
+ let(:headers) {{"foo" => "bar"}}
78
86
 
79
- connection.close
87
+ let(:server) do
88
+ Async::HTTP::Server.for(endpoint, protocol) do |request|
89
+ if Async::WebSocket::Request.websocket?(request)
90
+ Async::WebSocket::Response.for(request, headers) do |stream|
91
+ framer = Protocol::WebSocket::Framer.new(stream)
92
+
93
+ connection = handler.call(framer)
94
+
95
+ connection.write(request.headers.fields)
96
+
97
+ connection.close
98
+ end
99
+ else
100
+ Protocol::HTTP::Response[404, {}, []]
101
+ end
102
+ end
103
+ end
104
+
105
+ it "can send headers" do
106
+ connection = client.connect("/headers", headers: headers)
107
+
108
+ begin
109
+ expect(connection.read.to_h).to include(*headers.keys)
110
+ expect(connection.read).to be_nil
111
+ expect(connection).to be_closed
112
+ ensure
113
+ connection.close
114
+ end
115
+ end
80
116
  end
81
117
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-websocket
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-04 00:00:00.000000000 Z
11
+ date: 2020-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-io
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.43'
33
+ version: '0.51'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.43'
40
+ version: '0.51'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: protocol-websocket
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0.32'
75
+ version: '0.34'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0.32'
82
+ version: '0.34'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: covered
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -123,7 +123,7 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '3.6'
125
125
  - !ruby/object:Gem::Dependency
126
- name: rake
126
+ name: bake-bundler
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
@@ -149,8 +149,8 @@ files:
149
149
  - ".travis.yml"
150
150
  - Gemfile
151
151
  - README.md
152
- - Rakefile
153
152
  - async-websocket.gemspec
153
+ - examples/chat/.env
154
154
  - examples/chat/README.md
155
155
  - examples/chat/client.rb
156
156
  - examples/chat/config.ru
@@ -209,7 +209,6 @@ files:
209
209
  - spec/async/websocket/adapters/rack/client.rb
210
210
  - spec/async/websocket/adapters/rack/config.ru
211
211
  - spec/async/websocket/adapters/rack_spec.rb
212
- - spec/async/websocket/client_spec.rb
213
212
  - spec/async/websocket/connection_spec.rb
214
213
  - spec/async/websocket/server_examples.rb
215
214
  - spec/async/websocket/server_spec.rb
@@ -234,7 +233,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
233
  - !ruby/object:Gem::Version
235
234
  version: '0'
236
235
  requirements: []
237
- rubygems_version: 3.0.3
236
+ rubygems_version: 3.1.2
238
237
  signing_key:
239
238
  specification_version: 4
240
239
  summary: An async websocket library on top of websocket-driver.
@@ -242,7 +241,6 @@ test_files:
242
241
  - spec/async/websocket/adapters/rack/client.rb
243
242
  - spec/async/websocket/adapters/rack/config.ru
244
243
  - spec/async/websocket/adapters/rack_spec.rb
245
- - spec/async/websocket/client_spec.rb
246
244
  - spec/async/websocket/connection_spec.rb
247
245
  - spec/async/websocket/server_examples.rb
248
246
  - spec/async/websocket/server_spec.rb
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
@@ -1,48 +0,0 @@
1
- # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
20
-
21
- require "async/websocket/client"
22
- require "async/websocket/response"
23
-
24
- RSpec.describe Async::WebSocket::Client do
25
- describe '#connect' do
26
- let(:headers) {[
27
- ["Foo", "Bar"],
28
- ["Baz", "Qux"]
29
- ]}
30
-
31
- let(:client) {double}
32
- let(:stream) {double}
33
-
34
- subject {described_class.new(client)}
35
- let(:response) {Protocol::HTTP::Response.new(nil, 101, {}, nil, Protocol::WebSocket::Headers::PROTOCOL)}
36
-
37
- it "sets client request headers" do
38
- expect(response).to receive(:stream?).and_return(true)
39
- expect(response).to receive(:stream).and_return(stream)
40
-
41
- expect(client).to receive(:call) do |request|
42
- expect(request.headers.to_h).to include("Foo", "Baz")
43
- end.and_return(response)
44
-
45
- subject.connect("/server", headers: headers)
46
- end
47
- end
48
- end