async-io 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/examples/udp/client.rb +13 -0
- data/examples/udp/server.rb +15 -0
- data/lib/async/io/endpoint.rb +62 -3
- data/lib/async/io/endpoint/each.rb +1 -1
- data/lib/async/io/host_endpoint.rb +21 -5
- data/lib/async/io/shared_endpoint.rb +11 -13
- data/lib/async/io/socket.rb +20 -19
- data/lib/async/io/ssl_endpoint.rb +7 -1
- data/lib/async/io/standard.rb +21 -3
- data/lib/async/io/tcp_socket.rb +2 -2
- data/lib/async/io/unix_endpoint.rb +5 -0
- data/lib/async/io/version.rb +1 -1
- data/spec/async/io/endpoint_spec.rb +5 -0
- data/spec/async/io/standard_spec.rb +2 -2
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef50d94f2e881c86637e32d46f307a88b457537e9a649592d851e768474ffe2a
|
4
|
+
data.tar.gz: 24aac313f12b07478e080fa5713432b6e270be5d74e21688bb39efed2d4cf296
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '02688618b5035611cc4753acd581c0691c957ea0daefbdd4c357c52a4a31c12783b44dae77a5654aed36022abccac05a0813d300974dd94be26e97d5b25c47e9'
|
7
|
+
data.tar.gz: 811063ba4e91b823be5e8037fcf537bb40aa35bbbf7210df5fe21348b1394e579f27a7c0721114da9671c2ad108d0a70413040850d0643f5602cfc7ad5abc44f
|
data/.yardopts
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'async'
|
4
|
+
require 'async/io'
|
5
|
+
|
6
|
+
endpoint = Async::IO::Endpoint.udp("localhost", 5678)
|
7
|
+
|
8
|
+
Async do |task|
|
9
|
+
endpoint.bind do |socket|
|
10
|
+
while true
|
11
|
+
data, address = socket.recvfrom(1024)
|
12
|
+
socket.send(data.reverse, 0, address)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/async/io/endpoint.rb
CHANGED
@@ -28,29 +28,62 @@ module Async
|
|
28
28
|
# Endpoints represent a way of connecting or binding to an address.
|
29
29
|
class Endpoint
|
30
30
|
def initialize(**options)
|
31
|
-
@options = options
|
31
|
+
@options = options.freeze
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
def with(**options)
|
35
|
+
dup = self.dup
|
36
|
+
|
37
|
+
dup.options = @options.merge(options)
|
38
|
+
|
39
|
+
return dup
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_accessor :options
|
35
43
|
|
44
|
+
# @return [String] The hostname of the bound socket.
|
36
45
|
def hostname
|
37
46
|
@options[:hostname]
|
38
47
|
end
|
39
48
|
|
49
|
+
# If `SO_REUSEPORT` is enabled on a socket, the socket can be successfully bound even if there are existing sockets bound to the same address, as long as all prior bound sockets also had `SO_REUSEPORT` set before they were bound.
|
50
|
+
# @return [Boolean, nil] The value for `SO_REUSEPORT`.
|
40
51
|
def reuse_port
|
41
52
|
@options[:reuse_port]
|
42
53
|
end
|
43
54
|
|
55
|
+
# If `SO_REUSEADDR` is enabled on a socket prior to binding it, the socket can be successfully bound unless there is a conflict with another socket bound to exactly the same combination of source address and port. Additionally, when set, binding a socket to the address of an existing socket in `TIME_WAIT` is not an error.
|
56
|
+
# @return [Boolean] The value for `SO_REUSEADDR`.
|
57
|
+
def reuse_address
|
58
|
+
@options[:reuse_address]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Controls SO_LINGER. The amount of time the socket will stay in the `TIME_WAIT` state after being closed.
|
62
|
+
# @return [Integer, nil] The value for SO_LINGER.
|
63
|
+
def linger
|
64
|
+
@options[:linger]
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Numeric] The default timeout for socket operations.
|
44
68
|
def timeout
|
45
69
|
@options[:timeout]
|
46
70
|
end
|
47
71
|
|
72
|
+
# @return [Address] the address to bind to before connecting.
|
73
|
+
def local_address
|
74
|
+
@options[:local_address]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Endpoints sometimes have multiple paths.
|
78
|
+
# @yield [Endpoint] Enumerate all discrete paths as endpoints.
|
48
79
|
def each
|
49
80
|
return to_enum unless block_given?
|
50
81
|
|
51
82
|
yield self
|
52
83
|
end
|
53
84
|
|
85
|
+
# Accept connections from the specified endpoint.
|
86
|
+
# @param backlog [Integer] the number of connections to listen for.
|
54
87
|
def accept(backlog = Socket::SOMAXCONN, &block)
|
55
88
|
bind do |server|
|
56
89
|
server.listen(backlog)
|
@@ -59,10 +92,36 @@ module Async
|
|
59
92
|
end
|
60
93
|
end
|
61
94
|
|
95
|
+
# Map all endpoints by invoking `#bind`.
|
96
|
+
# @yield the bound wrapper.
|
97
|
+
def bound
|
98
|
+
wrappers = []
|
99
|
+
|
100
|
+
self.each do |endpoint|
|
101
|
+
wrapper = endpoint.bind
|
102
|
+
wrappers << wrapper
|
103
|
+
|
104
|
+
yield wrapper
|
105
|
+
end
|
106
|
+
|
107
|
+
return wrappers
|
108
|
+
ensure
|
109
|
+
wrappers.each(&:close) if $!
|
110
|
+
end
|
111
|
+
|
112
|
+
# Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options.
|
113
|
+
#
|
114
|
+
# @param string [String] URI as string. Scheme will decide implementation used.
|
115
|
+
# @param options keyword arguments passed through to {#initialize}
|
116
|
+
#
|
117
|
+
# @see Endpoint.ssl ssl - invoked when parsing a URL with the ssl scheme "ssl://127.0.0.1"
|
118
|
+
# @see Endpoint.tcp tcp - invoked when parsing a URL with the tcp scheme: "tcp://127.0.0.1"
|
119
|
+
# @see Endpoint.udp udp - invoked when parsing a URL with the udp scheme: "udp://127.0.0.1"
|
120
|
+
# @see Endpoint.unix unix - invoked when parsing a URL with the unix scheme: "unix://127.0.0.1"
|
62
121
|
def self.parse(string, **options)
|
63
122
|
uri = URI.parse(string)
|
64
123
|
|
65
|
-
self.
|
124
|
+
self.public_send(uri.scheme, uri.host, uri.port, **options)
|
66
125
|
end
|
67
126
|
end
|
68
127
|
end
|
@@ -43,15 +43,24 @@ module Async
|
|
43
43
|
# @yield [Socket] the socket which is being connected, may be invoked more than once
|
44
44
|
# @return [Socket] the connected socket
|
45
45
|
# @raise if no connection could complete successfully
|
46
|
-
def connect
|
46
|
+
def connect
|
47
47
|
last_error = nil
|
48
48
|
|
49
|
+
task = Task.current
|
50
|
+
|
49
51
|
Addrinfo.foreach(*@specification) do |address|
|
50
52
|
begin
|
51
|
-
|
52
|
-
|
53
|
-
rescue
|
53
|
+
wrapper = Socket.connect(address, **@options, task: task)
|
54
|
+
rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EAGAIN
|
54
55
|
last_error = $!
|
56
|
+
else
|
57
|
+
return wrapper unless block_given?
|
58
|
+
|
59
|
+
begin
|
60
|
+
return yield wrapper, task
|
61
|
+
ensure
|
62
|
+
wrapper.close
|
63
|
+
end
|
55
64
|
end
|
56
65
|
end
|
57
66
|
|
@@ -78,13 +87,20 @@ module Async
|
|
78
87
|
end
|
79
88
|
|
80
89
|
class Endpoint
|
81
|
-
# args
|
90
|
+
# @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM.
|
91
|
+
# @param options keyword arguments passed on to {HostEndpoint#initialize}
|
92
|
+
#
|
93
|
+
# @return [HostEndpoint]
|
82
94
|
def self.tcp(*args, **options)
|
83
95
|
args[3] = ::Socket::SOCK_STREAM
|
84
96
|
|
85
97
|
HostEndpoint.new(args, **options)
|
86
98
|
end
|
87
99
|
|
100
|
+
# @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM.
|
101
|
+
# @param options keyword arguments passed on to {HostEndpoint#initialize}
|
102
|
+
#
|
103
|
+
# @return [HostEndpoint]
|
88
104
|
def self.udp(*args, **options)
|
89
105
|
args[3] = ::Socket::SOCK_DGRAM
|
90
106
|
|
@@ -22,31 +22,27 @@ require_relative 'endpoint'
|
|
22
22
|
|
23
23
|
module Async
|
24
24
|
module IO
|
25
|
+
# Pre-connect and pre-bind sockets so that it can be used between processes.
|
25
26
|
class SharedEndpoint < Endpoint
|
27
|
+
# Create a new `SharedEndpoint` by binding to the given endpoint.
|
26
28
|
def self.bound(endpoint, backlog = Socket::SOMAXCONN)
|
27
|
-
wrappers =
|
28
|
-
|
29
|
-
endpoint.each do |endpoint|
|
30
|
-
server = endpoint.bind
|
31
|
-
|
29
|
+
wrappers = endpoint.bound do |server|
|
32
30
|
server.listen(backlog)
|
33
|
-
|
34
31
|
server.close_on_exec = false
|
35
32
|
server.reactor = nil
|
36
|
-
|
37
|
-
wrappers << server
|
38
33
|
end
|
39
34
|
|
40
|
-
self.new(endpoint, wrappers)
|
35
|
+
return self.new(endpoint, wrappers)
|
41
36
|
end
|
42
37
|
|
38
|
+
# Create a new `SharedEndpoint` by connecting to the given endpoint.
|
43
39
|
def self.connected(endpoint)
|
44
|
-
|
40
|
+
wrapper = endpoint.connect
|
45
41
|
|
46
|
-
|
47
|
-
|
42
|
+
wrapper.close_on_exec = false
|
43
|
+
wrapper.reactor = nil
|
48
44
|
|
49
|
-
self.new(endpoint, [
|
45
|
+
return self.new(endpoint, [wrapper])
|
50
46
|
end
|
51
47
|
|
52
48
|
def initialize(endpoint, wrappers, **options)
|
@@ -59,8 +55,10 @@ module Async
|
|
59
55
|
attr :endpoint
|
60
56
|
attr :wrappers
|
61
57
|
|
58
|
+
# Close all the internal wrappers.
|
62
59
|
def close
|
63
60
|
@wrappers.each(&:close)
|
61
|
+
@wrappers.clear
|
64
62
|
end
|
65
63
|
|
66
64
|
def bind
|
data/lib/async/io/socket.rb
CHANGED
@@ -108,6 +108,7 @@ module Async
|
|
108
108
|
|
109
109
|
include ::Socket::Constants
|
110
110
|
|
111
|
+
# @raise Errno::EAGAIN the connection failed due to the remote end being overloaded.
|
111
112
|
def connect(*args)
|
112
113
|
begin
|
113
114
|
async_send(:connect_nonblock, *args)
|
@@ -142,9 +143,23 @@ module Async
|
|
142
143
|
alias sysaccept accept
|
143
144
|
|
144
145
|
# Build and wrap the underlying io.
|
145
|
-
|
146
|
+
# @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
|
147
|
+
# @option reuse_address [Boolean] Allow this port to be bound in multiple processes.
|
148
|
+
def self.build(*args, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil, task: Task.current)
|
146
149
|
socket = wrapped_klass.new(*args)
|
147
150
|
|
151
|
+
if reuse_address
|
152
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
|
153
|
+
end
|
154
|
+
|
155
|
+
if reuse_port
|
156
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
157
|
+
end
|
158
|
+
|
159
|
+
if linger
|
160
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_LINGER, linger)
|
161
|
+
end
|
162
|
+
|
148
163
|
yield socket
|
149
164
|
|
150
165
|
wrapper = self.new(socket, task.reactor)
|
@@ -160,19 +175,14 @@ module Async
|
|
160
175
|
# Establish a connection to a given `remote_address`.
|
161
176
|
# @example
|
162
177
|
# socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
|
163
|
-
# @param remote_address [
|
164
|
-
# @
|
165
|
-
|
166
|
-
def self.connect(remote_address, local_address = nil, reuse_port: nil, task: Task.current, **options)
|
178
|
+
# @param remote_address [Address] The remote address to connect to.
|
179
|
+
# @option local_address [Address] The local address to bind to before connecting.
|
180
|
+
def self.connect(remote_address, local_address: nil, task: Task.current, **options)
|
167
181
|
Async.logger.debug(self) {"Connecting to #{remote_address.inspect}"}
|
168
182
|
|
169
183
|
task.annotate "connecting to #{remote_address.inspect}"
|
170
184
|
|
171
185
|
wrapper = build(remote_address.afamily, remote_address.socktype, remote_address.protocol, **options) do |socket|
|
172
|
-
if reuse_port
|
173
|
-
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
|
174
|
-
end
|
175
|
-
|
176
186
|
if local_address
|
177
187
|
socket.bind(local_address.to_sockaddr)
|
178
188
|
end
|
@@ -200,19 +210,10 @@ module Async
|
|
200
210
|
# socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
|
201
211
|
# @param local_address [Address] The local address to bind to.
|
202
212
|
# @option protocol [Integer] The socket protocol to use.
|
203
|
-
|
204
|
-
def self.bind(local_address, protocol: 0, reuse_port: nil, reuse_address: true, task: Task.current, **options, &block)
|
213
|
+
def self.bind(local_address, protocol: 0, task: Task.current, **options, &block)
|
205
214
|
Async.logger.debug(self) {"Binding to #{local_address.inspect}"}
|
206
215
|
|
207
216
|
wrapper = build(local_address.afamily, local_address.socktype, protocol, **options) do |socket|
|
208
|
-
if reuse_address
|
209
|
-
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
|
210
|
-
end
|
211
|
-
|
212
|
-
if reuse_port
|
213
|
-
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
214
|
-
end
|
215
|
-
|
216
217
|
socket.bind(local_address.to_sockaddr)
|
217
218
|
end
|
218
219
|
|
@@ -90,7 +90,7 @@ module Async
|
|
90
90
|
return to_enum unless block_given?
|
91
91
|
|
92
92
|
@endpoint.each do |endpoint|
|
93
|
-
yield self.class.new(endpoint,
|
93
|
+
yield self.class.new(endpoint, **@options)
|
94
94
|
end
|
95
95
|
end
|
96
96
|
end
|
@@ -99,6 +99,12 @@ module Async
|
|
99
99
|
SecureEndpoint = SSLEndpoint
|
100
100
|
|
101
101
|
class Endpoint
|
102
|
+
# @param args
|
103
|
+
# @param ssl_context [OpenSSL::SSL::SSLContext, nil]
|
104
|
+
# @param hostname [String, nil]
|
105
|
+
# @param options keyword arguments passed through to {Endpoint.tcp}
|
106
|
+
#
|
107
|
+
# @return [SSLEndpoint]
|
102
108
|
def self.ssl(*args, ssl_context: nil, hostname: nil, **options)
|
103
109
|
SSLEndpoint.new(self.tcp(*args, **options), ssl_context: ssl_context, hostname: hostname)
|
104
110
|
end
|
data/lib/async/io/standard.rb
CHANGED
@@ -22,8 +22,26 @@ require_relative 'generic'
|
|
22
22
|
|
23
23
|
module Async
|
24
24
|
module IO
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
class StandardInput < Generic
|
26
|
+
def initialize(io = $stdin)
|
27
|
+
super(io)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class StandardOutput < Generic
|
32
|
+
def initialize(io = $stdout)
|
33
|
+
super(io)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class StandardError < Generic
|
38
|
+
def initialize(io = $stderr)
|
39
|
+
super(io)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
STDIN = StandardInput.new
|
44
|
+
STDOUT = StandardOutput.new
|
45
|
+
STDERR = StandardError.new
|
28
46
|
end
|
29
47
|
end
|
data/lib/async/io/tcp_socket.rb
CHANGED
@@ -54,7 +54,7 @@ module Async
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
def initialize(remote_host, remote_port = nil, local_host = nil, local_port =
|
57
|
+
def initialize(remote_host, remote_port = nil, local_host = nil, local_port = 0)
|
58
58
|
if remote_host.is_a? ::TCPSocket
|
59
59
|
super(remote_host)
|
60
60
|
else
|
@@ -62,7 +62,7 @@ module Async
|
|
62
62
|
local_address = Addrinfo.tcp(local_host, local_port) if local_host
|
63
63
|
|
64
64
|
# We do this unusual dance to avoid leaking an "open" socket instance.
|
65
|
-
socket = Socket.connect(remote_address, local_address)
|
65
|
+
socket = Socket.connect(remote_address, local_address: local_address)
|
66
66
|
fd = socket.fcntl(Fcntl::F_DUPFD)
|
67
67
|
Async.logger.debug(self) {"Connected to #{remote_address.inspect}: #{fd}"}
|
68
68
|
socket.close
|
@@ -59,6 +59,11 @@ module Async
|
|
59
59
|
end
|
60
60
|
|
61
61
|
class Endpoint
|
62
|
+
# @param path [String]
|
63
|
+
# @param type Socket type
|
64
|
+
# @param options keyword arguments passed through to {UNIXEndpoint#initialize}
|
65
|
+
#
|
66
|
+
# @return [UNIXEndpoint]
|
62
67
|
def self.unix(path, type = ::Socket::SOCK_STREAM, **options)
|
63
68
|
UNIXEndpoint.new(path, type, **options)
|
64
69
|
end
|
data/lib/async/io/version.rb
CHANGED
@@ -57,6 +57,11 @@ RSpec.describe Async::IO::Endpoint do
|
|
57
57
|
expect(subject.hostname).to be == '0.0.0.0'
|
58
58
|
end
|
59
59
|
|
60
|
+
it "has local address" do
|
61
|
+
address = Async::IO::Address.tcp('127.0.0.1', 8080)
|
62
|
+
expect(subject.with(local_address: address).local_address).to be == address
|
63
|
+
end
|
64
|
+
|
60
65
|
let(:message) {"Hello World!"}
|
61
66
|
|
62
67
|
it "can connect to bound server" do
|
@@ -31,7 +31,7 @@ end
|
|
31
31
|
RSpec.describe Async::IO::STDOUT do
|
32
32
|
include_context Async::RSpec::Reactor
|
33
33
|
|
34
|
-
it "should be able to
|
34
|
+
it "should be able to write" do
|
35
35
|
expect(subject.write("")).to be == 0
|
36
36
|
end
|
37
37
|
end
|
@@ -39,7 +39,7 @@ end
|
|
39
39
|
RSpec.describe Async::IO::STDERR do
|
40
40
|
include_context Async::RSpec::Reactor
|
41
41
|
|
42
|
-
it "should be able to
|
42
|
+
it "should be able to write" do
|
43
43
|
expect(subject.write("")).to be == 0
|
44
44
|
end
|
45
45
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-io
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.21.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-
|
11
|
+
date: 2019-04-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -119,6 +119,7 @@ files:
|
|
119
119
|
- ".gitignore"
|
120
120
|
- ".rspec"
|
121
121
|
- ".travis.yml"
|
122
|
+
- ".yardopts"
|
122
123
|
- Gemfile
|
123
124
|
- README.md
|
124
125
|
- Rakefile
|
@@ -131,6 +132,8 @@ files:
|
|
131
132
|
- examples/issues/broken_ssl.rb
|
132
133
|
- examples/millions/client.rb
|
133
134
|
- examples/millions/server.rb
|
135
|
+
- examples/udp/client.rb
|
136
|
+
- examples/udp/server.rb
|
134
137
|
- lib/async/io.rb
|
135
138
|
- lib/async/io/address.rb
|
136
139
|
- lib/async/io/address_endpoint.rb
|
@@ -201,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
201
204
|
- !ruby/object:Gem::Version
|
202
205
|
version: '0'
|
203
206
|
requirements: []
|
204
|
-
rubygems_version: 3.0.
|
207
|
+
rubygems_version: 3.0.3
|
205
208
|
signing_key:
|
206
209
|
specification_version: 4
|
207
210
|
summary: Provides support for asynchonous TCP, UDP, UNIX and SSL sockets.
|