async-io 0.1.0 → 0.3.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/async-io.gemspec +1 -1
- data/lib/async/io/address.rb +1 -1
- data/lib/async/io/socket.rb +36 -19
- data/lib/async/io/stream.rb +307 -0
- data/lib/async/io/version.rb +1 -1
- data/spec/async/io/c10k_spec.rb +129 -0
- data/spec/async/io/socket_spec.rb +45 -0
- data/spec/async/io/stream_spec.rb +65 -0
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc74a7c6901edc284210da8593a505e0eb87ff89
|
4
|
+
data.tar.gz: d5c4e46d022296613e94dfbcc6409727dc33dcb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79274376a216fce20ee3a1d8f56f2767f9a9c6cc107d20bc9780ceb4ce29c5e32f7e5a34a7ac84b6d57d55eb28cd00d4d6fd8ee32208a7e1a7015b146f5e10b4
|
7
|
+
data.tar.gz: dc4e658c5411145c90359ef91dc565027b8c1ed36e22abd92e4907b20958f2e3b03e9cccb3a503c443c0cdfbceb58a4204226cd6d735681e3df6cec60c4317b6
|
data/async-io.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.has_rdoc = "yard"
|
18
18
|
|
19
19
|
spec.add_dependency "async", "~> 0.14"
|
20
|
-
spec.add_development_dependency "async-rspec", "~> 1.
|
20
|
+
spec.add_development_dependency "async-rspec", "~> 1.1"
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.13"
|
23
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/async/io/address.rb
CHANGED
data/lib/async/io/socket.rb
CHANGED
@@ -84,6 +84,18 @@ module Async
|
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
|
+
def self.build(*args, task: Task.current)
|
88
|
+
socket = ::Socket.new(*args)
|
89
|
+
|
90
|
+
yield socket
|
91
|
+
|
92
|
+
return self.new(socket, task.reactor)
|
93
|
+
rescue Exception
|
94
|
+
socket.close if socket
|
95
|
+
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
|
87
99
|
# Establish a connection to a given `remote_address`.
|
88
100
|
# @example
|
89
101
|
# socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
|
@@ -93,21 +105,26 @@ module Async
|
|
93
105
|
def self.connect(remote_address, local_address = nil, protocol: 0, task: Task.current)
|
94
106
|
task.annotate "connecting to #{remote_address.inspect}"
|
95
107
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
108
|
+
wrapper = build(remote_address.afamily, remote_address.socktype, protocol) do |socket|
|
109
|
+
if local_address
|
110
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true)
|
111
|
+
socket.bind(local_address.to_sockaddr) if local_address
|
112
|
+
end
|
113
|
+
|
114
|
+
self.new(socket, task.reactor)
|
101
115
|
end
|
102
116
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
117
|
+
begin
|
118
|
+
wrapper.connect(remote_address.to_sockaddr)
|
119
|
+
task.annotate "connected to #{remote_address.inspect}"
|
120
|
+
rescue
|
121
|
+
wrapper.close
|
122
|
+
raise
|
123
|
+
end
|
107
124
|
|
108
125
|
if block_given?
|
109
126
|
begin
|
110
|
-
|
127
|
+
yield wrapper, task
|
111
128
|
ensure
|
112
129
|
wrapper.close
|
113
130
|
end
|
@@ -120,20 +137,20 @@ module Async
|
|
120
137
|
# @example
|
121
138
|
# socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
|
122
139
|
# @param local_address [Address] The local address to bind to.
|
123
|
-
# @option
|
124
|
-
|
140
|
+
# @option protocol [Integer] The socket protocol to use.
|
141
|
+
# @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
|
142
|
+
def self.bind(local_address, protocol: 0, reuse_port: false, task: Task.current, &block)
|
125
143
|
task.annotate "binding to #{local_address.inspect}"
|
126
144
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
wrapper = self.new(socket, task.reactor)
|
145
|
+
wrapper = build(local_address.afamily, local_address.socktype, protocol) do |socket|
|
146
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true)
|
147
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, true) if reuse_port
|
148
|
+
socket.bind(local_address.to_sockaddr)
|
149
|
+
end
|
133
150
|
|
134
151
|
if block_given?
|
135
152
|
begin
|
136
|
-
|
153
|
+
yield wrapper, task
|
137
154
|
ensure
|
138
155
|
wrapper.close
|
139
156
|
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
# Copyright, 2017, 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 'socket'
|
22
|
+
require_relative 'generic'
|
23
|
+
|
24
|
+
module Async
|
25
|
+
module IO
|
26
|
+
class BinaryString < String
|
27
|
+
def initialize(*args)
|
28
|
+
super
|
29
|
+
|
30
|
+
force_encoding(Encoding::BINARY)
|
31
|
+
end
|
32
|
+
|
33
|
+
def << string
|
34
|
+
super
|
35
|
+
|
36
|
+
force_encoding(Encoding::BINARY)
|
37
|
+
end
|
38
|
+
|
39
|
+
alias concat <<
|
40
|
+
end
|
41
|
+
|
42
|
+
class Stream
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
def initialize(io, block_size: 1024, eol: $/)
|
46
|
+
@io = io
|
47
|
+
@eof = false
|
48
|
+
@sync = false
|
49
|
+
|
50
|
+
@block_size = block_size
|
51
|
+
@eol = eol
|
52
|
+
|
53
|
+
@read_buffer = BinaryString.new
|
54
|
+
@write_buffer = BinaryString.new
|
55
|
+
end
|
56
|
+
|
57
|
+
attr :io
|
58
|
+
|
59
|
+
# The "sync mode" of the stream. See IO#sync for full details.
|
60
|
+
attr_accessor :sync
|
61
|
+
|
62
|
+
# Reads `size` bytes from the stream. If `buffer` is provided it must
|
63
|
+
# reference a string which will receive the data.
|
64
|
+
#
|
65
|
+
# See IO#read for full details.
|
66
|
+
def read(size = nil, output_buffer = nil)
|
67
|
+
if size == 0
|
68
|
+
if output_buffer
|
69
|
+
output_buffer.clear
|
70
|
+
return output_buffer
|
71
|
+
else
|
72
|
+
return ""
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
until @eof
|
77
|
+
break if size && size <= @read_buffer.size
|
78
|
+
fill_read_buffer
|
79
|
+
break unless size
|
80
|
+
end
|
81
|
+
|
82
|
+
buffer = consume_read_buffer(size)
|
83
|
+
|
84
|
+
if buffer && output_buffer
|
85
|
+
output_buffer.replace(buffer)
|
86
|
+
buffer = output_buffer
|
87
|
+
end
|
88
|
+
|
89
|
+
if size
|
90
|
+
return buffer
|
91
|
+
else
|
92
|
+
return buffer || ""
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Writes `string` to the buffer. When the buffer is full or #sync is true the
|
97
|
+
# buffer is flushed to the underlying `io`.
|
98
|
+
# @param string the string to write to the buffer.
|
99
|
+
# @return the number of bytes appended to the buffer.
|
100
|
+
def write(string)
|
101
|
+
@write_buffer << string
|
102
|
+
|
103
|
+
if @sync || @write_buffer.size > @block_size
|
104
|
+
flush
|
105
|
+
end
|
106
|
+
|
107
|
+
return string.bytesize
|
108
|
+
end
|
109
|
+
|
110
|
+
# Flushes buffered data to the stream.
|
111
|
+
def flush
|
112
|
+
syswrite(@write_buffer)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Closes the stream and flushes any unwritten data.
|
116
|
+
def close
|
117
|
+
flush rescue nil
|
118
|
+
|
119
|
+
@io.close
|
120
|
+
end
|
121
|
+
|
122
|
+
# Reads the next line from the stream. Lines are separated by +eol+. If
|
123
|
+
# +limit+ is provided the result will not be longer than the given number of
|
124
|
+
# bytes.
|
125
|
+
#
|
126
|
+
# +eol+ may be a String or Regexp.
|
127
|
+
#
|
128
|
+
# Unlike IO#gets the line read will not be assigned to +$_+.
|
129
|
+
#
|
130
|
+
# Unlike IO#gets the separator must be provided if a limit is provided.
|
131
|
+
def gets(eol = @eol, limit = nil)
|
132
|
+
index = @read_buffer.index(eol)
|
133
|
+
|
134
|
+
until index || @eof
|
135
|
+
fill_read_buffer
|
136
|
+
index = @read_buffer.index(eol)
|
137
|
+
end
|
138
|
+
|
139
|
+
if eol.is_a?(Regexp)
|
140
|
+
size = index ? index+$&.bytesize : nil
|
141
|
+
else
|
142
|
+
size = index ? index+eol.bytesize : nil
|
143
|
+
end
|
144
|
+
|
145
|
+
if limit && limit >= 0
|
146
|
+
size = [size, limit].min
|
147
|
+
end
|
148
|
+
|
149
|
+
consume_read_buffer(size)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Executes the block for every line in the stream where lines are separated
|
153
|
+
# by +eol+.
|
154
|
+
#
|
155
|
+
# See also #gets
|
156
|
+
def each(eol = @eol)
|
157
|
+
while line = self.gets(eol)
|
158
|
+
yield line
|
159
|
+
end
|
160
|
+
end
|
161
|
+
alias each_line each
|
162
|
+
|
163
|
+
# Reads lines from the stream which are separated by +eol+.
|
164
|
+
#
|
165
|
+
# See also #gets
|
166
|
+
def readlines(eol = @eol)
|
167
|
+
lines = []
|
168
|
+
|
169
|
+
while line = self.gets(eol)
|
170
|
+
lines << line
|
171
|
+
end
|
172
|
+
|
173
|
+
lines
|
174
|
+
end
|
175
|
+
|
176
|
+
# Reads a line from the stream which is separated by +eol+.
|
177
|
+
#
|
178
|
+
# Raises EOFError if at end of file.
|
179
|
+
def readline(eol=@eol)
|
180
|
+
gets(eol) or raise EOFError
|
181
|
+
end
|
182
|
+
|
183
|
+
# Reads one character from the stream. Returns nil if called at end of
|
184
|
+
# file.
|
185
|
+
def getc
|
186
|
+
read(1)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Calls the given block once for each byte in the stream.
|
190
|
+
def each_byte # :yields: byte
|
191
|
+
while c = getc
|
192
|
+
yield(c.ord)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Reads a one-character string from the stream. Raises an EOFError at end
|
197
|
+
# of file.
|
198
|
+
def readchar
|
199
|
+
getc or raise EOFError
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns true if the stream is at file which means there is no more data to be read.
|
203
|
+
def eof?
|
204
|
+
fill_read_buffer if !@eof && @read_buffer.empty?
|
205
|
+
|
206
|
+
@eof && @read_buffer.empty?
|
207
|
+
end
|
208
|
+
alias eof eof?
|
209
|
+
|
210
|
+
# Writes `string` to the stream and returns self.
|
211
|
+
def <<(string)
|
212
|
+
write(string)
|
213
|
+
|
214
|
+
return self
|
215
|
+
end
|
216
|
+
|
217
|
+
# Writes `args` to the stream along with a record separator. See `IO#puts` for full details.
|
218
|
+
def puts(*args, eol: @eol)
|
219
|
+
if args.empty?
|
220
|
+
write(eol)
|
221
|
+
else
|
222
|
+
args.each do |arg|
|
223
|
+
string = arg.to_s
|
224
|
+
if string.end_with? eol
|
225
|
+
write(string)
|
226
|
+
else
|
227
|
+
write(string)
|
228
|
+
write(eol)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
return nil
|
234
|
+
end
|
235
|
+
|
236
|
+
# Writes `args` to the stream. See `IO#print` for full details.
|
237
|
+
def print(*args)
|
238
|
+
args.each do |arg|
|
239
|
+
write(arg.to_s)
|
240
|
+
end
|
241
|
+
|
242
|
+
return nil
|
243
|
+
end
|
244
|
+
|
245
|
+
# Formats and writes to the stream converting parameters under control of the format string. See `Kernel#sprintf` for format string details.
|
246
|
+
def printf(s, *args)
|
247
|
+
write(s % args)
|
248
|
+
|
249
|
+
return nil
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
# Write a buffer to the underlying stream.
|
255
|
+
# @param buffer [String] The string to write, any encoding is okay.
|
256
|
+
def syswrite(buffer)
|
257
|
+
remaining = buffer.bytesize
|
258
|
+
|
259
|
+
# Fast path:
|
260
|
+
written = @io.write(buffer)
|
261
|
+
return if written == remaining
|
262
|
+
|
263
|
+
# Slow path:
|
264
|
+
remaining -= written
|
265
|
+
|
266
|
+
while remaining > 0
|
267
|
+
wrote = @io.write(buffer.byteslice(written, remaining))
|
268
|
+
|
269
|
+
remaining -= wrote
|
270
|
+
written += wrote
|
271
|
+
end
|
272
|
+
|
273
|
+
return written
|
274
|
+
end
|
275
|
+
|
276
|
+
# Fills the buffer from the underlying stream.
|
277
|
+
def fill_read_buffer
|
278
|
+
if buffer = @io.read(@block_size)
|
279
|
+
# We guarantee that the read_buffer remains ASCII-8BIT because read should always return ASCII-8BIT
|
280
|
+
@read_buffer << buffer
|
281
|
+
else
|
282
|
+
@eof = true
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Consumes `size` bytes from the buffer.
|
287
|
+
# @param size [Integer|nil] The amount of data to consume. If nil, consume entire buffer.
|
288
|
+
def consume_read_buffer(size = nil)
|
289
|
+
# If we are at eof, and the read buffer is empty, we can't consume anything.
|
290
|
+
return nil if @eof && @read_buffer.empty?
|
291
|
+
|
292
|
+
result = nil
|
293
|
+
|
294
|
+
if size == nil || size == @read_buffer.size
|
295
|
+
# Consume the entire read buffer:
|
296
|
+
result = @read_buffer.dup
|
297
|
+
@read_buffer.clear
|
298
|
+
else
|
299
|
+
# Consume only part of the read buffer:
|
300
|
+
result = @read_buffer.slice!(0, size)
|
301
|
+
end
|
302
|
+
|
303
|
+
return result
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
data/lib/async/io/version.rb
CHANGED
@@ -0,0 +1,129 @@
|
|
1
|
+
# Copyright, 2017, 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/io'
|
22
|
+
require 'benchmark'
|
23
|
+
|
24
|
+
RSpec.describe "echo client/server" do
|
25
|
+
let(:repeats) {10000}
|
26
|
+
let(:server_address) {Async::IO::Address.tcp('0.0.0.0', 9000)}
|
27
|
+
|
28
|
+
def echo_server(server_address)
|
29
|
+
Async::Reactor.run do |task|
|
30
|
+
connection_count = 0
|
31
|
+
|
32
|
+
connections_complete = task.async do
|
33
|
+
last_count = 0
|
34
|
+
|
35
|
+
while connection_count < repeats
|
36
|
+
if connection_count != last_count
|
37
|
+
puts "#{connection_count}/#{repeats} simultaneous connections."
|
38
|
+
last_count = connection_count
|
39
|
+
end
|
40
|
+
|
41
|
+
task.sleep(1.0)
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "Releasing all connections..."
|
45
|
+
end
|
46
|
+
|
47
|
+
# This is a synchronous block within the current task:
|
48
|
+
Async::IO::Socket.accept(server_address) do |client|
|
49
|
+
connection_count += 1
|
50
|
+
|
51
|
+
# Wait until we've got all the connections:
|
52
|
+
connections_complete.wait
|
53
|
+
|
54
|
+
# This is an asynchronous block within the current reactor:
|
55
|
+
data = client.read(512)
|
56
|
+
client.write(data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def echo_client(server_address, data, responses)
|
62
|
+
Async::Reactor.run do |task|
|
63
|
+
begin
|
64
|
+
Async::IO::Socket.connect(server_address) do |peer|
|
65
|
+
result = peer.write(data)
|
66
|
+
|
67
|
+
message = peer.read(512)
|
68
|
+
|
69
|
+
responses << message
|
70
|
+
end
|
71
|
+
rescue Errno::ECONNREFUSED
|
72
|
+
puts "Connection refused..."
|
73
|
+
# If the connection was refused, it means the server probably can't accept connections any faster than it currently is, so we simply retry.
|
74
|
+
retry
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def fork_server
|
80
|
+
pid = fork do
|
81
|
+
echo_server(server_address)
|
82
|
+
end
|
83
|
+
|
84
|
+
yield
|
85
|
+
ensure
|
86
|
+
Process.kill(:KILL, pid)
|
87
|
+
Process.wait(pid)
|
88
|
+
end
|
89
|
+
|
90
|
+
around(:each) do |example|
|
91
|
+
duration = Benchmark.realtime do
|
92
|
+
example.run
|
93
|
+
end
|
94
|
+
|
95
|
+
example.reporter.message "Handled #{repeats} connections in #{duration}: #{repeats/duration}req/s"
|
96
|
+
end
|
97
|
+
|
98
|
+
around(:each) do |example|
|
99
|
+
previous_level = Async.logger.level
|
100
|
+
# Supress logging:
|
101
|
+
Async.logger.level = Logger::WARN
|
102
|
+
|
103
|
+
begin
|
104
|
+
example.run
|
105
|
+
ensure
|
106
|
+
Async.logger.level = previous_level
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should send/receive 10,000 messages" do
|
111
|
+
fork_server do
|
112
|
+
Async::Reactor.run do |task|
|
113
|
+
responses = []
|
114
|
+
|
115
|
+
tasks = repeats.times.collect do |i|
|
116
|
+
# puts "Starting client #{i} on #{task}..." if (i % 1000) == 0
|
117
|
+
|
118
|
+
echo_client(server_address, "Hello World #{i}", responses)
|
119
|
+
end
|
120
|
+
|
121
|
+
# task.reactor.print_hierarchy
|
122
|
+
|
123
|
+
tasks.each(&:wait)
|
124
|
+
|
125
|
+
expect(responses.count).to be repeats
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright, 2017, 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/io/socket'
|
22
|
+
|
23
|
+
RSpec.describe Async::IO::Socket do
|
24
|
+
include_context Async::RSpec::Reactor
|
25
|
+
|
26
|
+
describe '#connect' do
|
27
|
+
let(:address) {Async::IO::Address.tcp('127.0.0.1', 12345)}
|
28
|
+
|
29
|
+
it "should fail to connect if no listening server" do
|
30
|
+
expect do
|
31
|
+
Async::IO::Socket.connect(address)
|
32
|
+
end.to raise_error(Errno::ECONNREFUSED)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#bind' do
|
37
|
+
let(:address) {Async::IO::Address.tcp('127.0.0.1', 1)}
|
38
|
+
|
39
|
+
it "should fail to bind to port < 1024" do
|
40
|
+
expect do
|
41
|
+
Async::IO::Socket.bind(address)
|
42
|
+
end.to raise_error(Errno::EACCES)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Copyright, 2017, 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/io/stream'
|
22
|
+
|
23
|
+
RSpec.describe Async::IO::Stream do
|
24
|
+
let(:io) {StringIO.new}
|
25
|
+
let(:stream) {Async::IO::Stream.new(io, eol: "\n")}
|
26
|
+
|
27
|
+
describe '#puts' do
|
28
|
+
it "should write line" do
|
29
|
+
stream.puts "Hello World"
|
30
|
+
stream.flush
|
31
|
+
|
32
|
+
expect(io.string).to be == "Hello World\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#readline' do
|
37
|
+
before(:each) do
|
38
|
+
io.puts "Hello World"
|
39
|
+
io.seek(0)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should read one line" do
|
43
|
+
expect(stream.readline).to be == "Hello World\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should be binary encoding" do
|
47
|
+
expect(stream.readline.encoding).to be == Encoding::BINARY
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#readlines' do
|
52
|
+
before(:each) do
|
53
|
+
io << "Hello\nWorld\n"
|
54
|
+
io.seek(0)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should read multiple lines" do
|
58
|
+
expect(stream.readlines).to be == ["Hello\n", "World\n"]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should be binary encoding" do
|
62
|
+
expect(stream.readlines.first.encoding).to be == Encoding::BINARY
|
63
|
+
end
|
64
|
+
end
|
65
|
+
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: 0.
|
4
|
+
version: 0.3.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: 2017-
|
11
|
+
date: 2017-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1.
|
33
|
+
version: '1.1'
|
34
34
|
type: :development
|
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: '1.
|
40
|
+
version: '1.1'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,14 +98,18 @@ files:
|
|
98
98
|
- lib/async/io/address.rb
|
99
99
|
- lib/async/io/generic.rb
|
100
100
|
- lib/async/io/socket.rb
|
101
|
+
- lib/async/io/stream.rb
|
101
102
|
- lib/async/io/tcp_socket.rb
|
102
103
|
- lib/async/io/udp_socket.rb
|
103
104
|
- lib/async/io/unix_socket.rb
|
104
105
|
- lib/async/io/version.rb
|
105
106
|
- lib/async/io/wrap/tcp.rb
|
106
107
|
- spec/async/io/address_spec.rb
|
108
|
+
- spec/async/io/c10k_spec.rb
|
107
109
|
- spec/async/io/echo_spec.rb
|
108
110
|
- spec/async/io/generic_spec.rb
|
111
|
+
- spec/async/io/socket_spec.rb
|
112
|
+
- spec/async/io/stream_spec.rb
|
109
113
|
- spec/async/io/tcp_socket_spec.rb
|
110
114
|
- spec/async/io/udp_socket_spec.rb
|
111
115
|
- spec/async/io/unix_socket_spec.rb
|
@@ -136,8 +140,11 @@ specification_version: 4
|
|
136
140
|
summary: Provides support for asynchonous TCP, UDP, UNIX and SSL sockets.
|
137
141
|
test_files:
|
138
142
|
- spec/async/io/address_spec.rb
|
143
|
+
- spec/async/io/c10k_spec.rb
|
139
144
|
- spec/async/io/echo_spec.rb
|
140
145
|
- spec/async/io/generic_spec.rb
|
146
|
+
- spec/async/io/socket_spec.rb
|
147
|
+
- spec/async/io/stream_spec.rb
|
141
148
|
- spec/async/io/tcp_socket_spec.rb
|
142
149
|
- spec/async/io/udp_socket_spec.rb
|
143
150
|
- spec/async/io/unix_socket_spec.rb
|