async-websocket 0.13.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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