socketry 0.5.0 → 0.6.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 +5 -5
- data/context/configuration.md +154 -0
- data/lib/socketry/version.rb +5 -1
- data/lib/socketry.rb +3 -22
- data/license.md +21 -0
- data/readme.md +47 -0
- data/releases.md +6 -0
- data/skills/socketry-github-repository/SKILL.md +95 -0
- data/skills/socketry-pull-request/SKILL.md +54 -0
- metadata +20 -68
- data/.gitignore +0 -9
- data/.rspec +0 -4
- data/.rubocop.yml +0 -59
- data/.ruby-version +0 -1
- data/.travis.yml +0 -16
- data/CHANGES.md +0 -31
- data/Gemfile +0 -20
- data/Guardfile +0 -10
- data/LICENSE.txt +0 -22
- data/README.md +0 -108
- data/Rakefile +0 -10
- data/lib/socketry/exceptions.rb +0 -40
- data/lib/socketry/resolver/resolv.rb +0 -67
- data/lib/socketry/resolver/system.rb +0 -48
- data/lib/socketry/ssl/server.rb +0 -53
- data/lib/socketry/ssl/socket.rb +0 -188
- data/lib/socketry/tcp/server.rb +0 -85
- data/lib/socketry/tcp/socket.rb +0 -345
- data/lib/socketry/timeout.rb +0 -76
- data/lib/socketry/udp/datagram.rb +0 -29
- data/lib/socketry/udp/socket.rb +0 -185
- data/socketry.gemspec +0 -29
data/lib/socketry/tcp/socket.rb
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# Transmission Control Protocol
|
|
5
|
-
module TCP
|
|
6
|
-
# Transmission Control Protocol sockets: Provide stream-like semantics
|
|
7
|
-
class Socket
|
|
8
|
-
include Socketry::Timeout
|
|
9
|
-
|
|
10
|
-
attr_reader :addr_fmaily, :remote_addr, :remote_port, :local_addr, :local_port
|
|
11
|
-
attr_reader :read_timeout, :write_timeout, :resolver, :socket_class
|
|
12
|
-
|
|
13
|
-
# Create a Socketry::TCP::Socket with the default options, then connect
|
|
14
|
-
# to the given host.
|
|
15
|
-
#
|
|
16
|
-
# @param remote_addr [String] DNS name or IP address of the host to connect to
|
|
17
|
-
# @param remote_port [Fixnum] TCP port to connect to
|
|
18
|
-
#
|
|
19
|
-
# @return [Socketry::TCP::Socket]
|
|
20
|
-
def self.connect(remote_addr, remote_port, **args)
|
|
21
|
-
new.connect(remote_addr, remote_port, **args)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Create an unconnected Socketry::TCP::Socket
|
|
25
|
-
#
|
|
26
|
-
# @param read_timeout [Numeric] Seconds to wait before an uncompleted read errors
|
|
27
|
-
# @param write_timeout [Numeric] Seconds to wait before an uncompleted write errors
|
|
28
|
-
# @param timer [Object] A timekeeping object to use for measuring timeouts
|
|
29
|
-
# @param resolver [Object] A resolver object to use for resolving DNS names
|
|
30
|
-
# @param socket_class [Object] Underlying socket class which implements I/O ops
|
|
31
|
-
#
|
|
32
|
-
# @return [Socketry::TCP::Socket]
|
|
33
|
-
def initialize(
|
|
34
|
-
read_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:read],
|
|
35
|
-
write_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:write],
|
|
36
|
-
timer: Socketry::Timeout::DEFAULT_TIMER.new,
|
|
37
|
-
resolver: Socketry::Resolver::DEFAULT_RESOLVER,
|
|
38
|
-
socket_class: ::Socket
|
|
39
|
-
)
|
|
40
|
-
@read_timeout = read_timeout
|
|
41
|
-
@write_timeout = write_timeout
|
|
42
|
-
|
|
43
|
-
@socket_class = socket_class
|
|
44
|
-
@resolver = resolver
|
|
45
|
-
|
|
46
|
-
@addr_family = nil
|
|
47
|
-
@socket = nil
|
|
48
|
-
|
|
49
|
-
@remote_addr = nil
|
|
50
|
-
@remote_port = nil
|
|
51
|
-
@local_addr = nil
|
|
52
|
-
@local_port = nil
|
|
53
|
-
|
|
54
|
-
start_timer(timer)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Connect to a remote host
|
|
58
|
-
#
|
|
59
|
-
# @param remote_addr [String] DNS name or IP address of the host to connect to
|
|
60
|
-
# @param remote_port [Fixnum] TCP port to connect to
|
|
61
|
-
# @param local_addr [String] DNS name or IP address to bind to locally
|
|
62
|
-
# @param local_port [Fixnum] Local TCP port to bind to
|
|
63
|
-
# @param timeout [Numeric] Number of seconds to wait before aborting connect
|
|
64
|
-
#
|
|
65
|
-
# @raise [Socketry::AddressError] an invalid address was given
|
|
66
|
-
# @raise [Socketry::TimeoutError] connect operation timed out
|
|
67
|
-
#
|
|
68
|
-
# @return [self]
|
|
69
|
-
def connect(
|
|
70
|
-
remote_addr,
|
|
71
|
-
remote_port,
|
|
72
|
-
local_addr: nil,
|
|
73
|
-
local_port: nil,
|
|
74
|
-
timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect]
|
|
75
|
-
)
|
|
76
|
-
ensure_disconnected
|
|
77
|
-
|
|
78
|
-
@remote_addr = remote_addr
|
|
79
|
-
@remote_port = remote_port
|
|
80
|
-
@local_addr = local_addr
|
|
81
|
-
@local_port = local_port
|
|
82
|
-
|
|
83
|
-
begin
|
|
84
|
-
set_timeout(timeout)
|
|
85
|
-
|
|
86
|
-
remote_addr = @resolver.resolve(remote_addr, timeout: time_remaining(timeout))
|
|
87
|
-
local_addr = @resolver.resolve(local_addr, timeout: time_remaining(timeout)) if local_addr
|
|
88
|
-
raise ArgumentError, "expected IPAddr from resolver, got #{remote_addr.class}" unless remote_addr.is_a?(IPAddr)
|
|
89
|
-
|
|
90
|
-
@addr_family = if remote_addr.ipv4? then ::Socket::AF_INET
|
|
91
|
-
elsif remote_addr.ipv6? then ::Socket::AF_INET6
|
|
92
|
-
else raise Socketry::AddressError, "unsupported IP address family: #{remote_addr}"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
socket = @socket_class.new(@addr_family, ::Socket::SOCK_STREAM, 0)
|
|
96
|
-
socket.bind Addrinfo.tcp(local_addr.to_s, local_port) if local_addr
|
|
97
|
-
remote_sockaddr = ::Socket.sockaddr_in(remote_port, remote_addr.to_s)
|
|
98
|
-
|
|
99
|
-
# Note: `exception: false` for Socket#connect_nonblock is only supported in Ruby 2.3+
|
|
100
|
-
begin
|
|
101
|
-
socket.connect_nonblock(remote_sockaddr)
|
|
102
|
-
rescue Errno::ECONNREFUSED => ex
|
|
103
|
-
raise Socketry::ConnectionRefusedError, "connection to #{remote_addr}:#{remote_port} refused", ex.backtrace
|
|
104
|
-
rescue Errno::EINPROGRESS, Errno::EALREADY
|
|
105
|
-
# Earlier JRuby 9.x versions do not seem to correctly support Socket#wait_writable in this case
|
|
106
|
-
# Newer versions seem to behave correctly
|
|
107
|
-
retry if IO.select(nil, [socket], nil, time_remaining(timeout))
|
|
108
|
-
|
|
109
|
-
socket.close
|
|
110
|
-
raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
|
|
111
|
-
rescue Errno::EISCONN
|
|
112
|
-
# Sometimes raised when we've connected successfully
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
@socket = socket
|
|
116
|
-
ensure
|
|
117
|
-
clear_timeout(timeout)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
self
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Re-establish a lost TCP connection
|
|
124
|
-
#
|
|
125
|
-
# @param timeout [Numeric] Number of seconds to wait before aborting re-connect
|
|
126
|
-
# @raise [Socketry::StateError] not in a disconnected state
|
|
127
|
-
def reconnect(timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect])
|
|
128
|
-
ensure_disconnected
|
|
129
|
-
raise StateError, "can't reconnect: never completed initial connection" unless @remote_addr
|
|
130
|
-
connect(@remote_addr, @remote_port, local_addr: @local_addr, local_port: @local_port, timeout: timeout)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Wrap a Ruby/low-level socket in an Socketry::TCP::Socket
|
|
134
|
-
#
|
|
135
|
-
# @param socket [::Socket] (or specified socket_class) low-level socket to wrap
|
|
136
|
-
def from_socket(socket)
|
|
137
|
-
ensure_disconnected
|
|
138
|
-
raise TypeError, "expected #{@socket_class}, got #{socket.class}" unless socket.is_a?(@socket_class)
|
|
139
|
-
@socket = socket
|
|
140
|
-
self
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Perform a non-blocking read operation
|
|
144
|
-
#
|
|
145
|
-
# @param size [Fixnum] number of bytes to attempt to read
|
|
146
|
-
# @param outbuf [String, NilClass] an optional buffer into which data should be read
|
|
147
|
-
#
|
|
148
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
149
|
-
#
|
|
150
|
-
# @return [String, :wait_readable] data read, or :wait_readable if operation would block
|
|
151
|
-
def read_nonblock(size, outbuf: nil)
|
|
152
|
-
ensure_connected
|
|
153
|
-
case outbuf
|
|
154
|
-
when String
|
|
155
|
-
@socket.read_nonblock(size, outbuf, exception: false)
|
|
156
|
-
when NilClass
|
|
157
|
-
@socket.read_nonblock(size, exception: false)
|
|
158
|
-
else raise TypeError, "unexpected outbuf class: #{outbuf.class}"
|
|
159
|
-
end
|
|
160
|
-
rescue IO::WaitReadable
|
|
161
|
-
# Some buggy Rubies continue to raise this exception
|
|
162
|
-
:wait_readable
|
|
163
|
-
rescue IOError => ex
|
|
164
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Read a partial amounth of data, blocking until it becomes available
|
|
168
|
-
#
|
|
169
|
-
# @param size [Fixnum] number of bytes to attempt to read
|
|
170
|
-
# @param outbuf [String] an output buffer to read data into
|
|
171
|
-
# @param timeout [Numeric] Number of seconds to wait for read operation to complete
|
|
172
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
173
|
-
# @return [String, :eof] bytes read, or :eof if socket closed while reading
|
|
174
|
-
def readpartial(size, outbuf: nil, timeout: @read_timeout)
|
|
175
|
-
set_timeout(timeout)
|
|
176
|
-
|
|
177
|
-
begin
|
|
178
|
-
while (result = read_nonblock(size, outbuf: outbuf)) == :wait_readable
|
|
179
|
-
next if @socket.wait_readable(time_remaining(timeout))
|
|
180
|
-
raise TimeoutError, "read timed out after #{timeout} seconds"
|
|
181
|
-
end
|
|
182
|
-
ensure
|
|
183
|
-
clear_timeout(timeout)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
result || :eof
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Read all of the data in a given string to a socket unless timeout or EOF
|
|
190
|
-
#
|
|
191
|
-
# @param size [Fixnum] number of bytes to attempt to read
|
|
192
|
-
# @param outbuf [String] an output buffer to read data into
|
|
193
|
-
# @param timeout [Numeric] Number of seconds to wait for read operation to complete
|
|
194
|
-
#
|
|
195
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
196
|
-
#
|
|
197
|
-
# @return [String, :eof] bytes read, or :eof if socket closed while reading
|
|
198
|
-
def read(size, outbuf: String.new, timeout: @write_timeout)
|
|
199
|
-
outbuf.clear
|
|
200
|
-
deadline = lifetime + timeout if timeout
|
|
201
|
-
|
|
202
|
-
begin
|
|
203
|
-
until outbuf.size == size
|
|
204
|
-
time_remaining = deadline - lifetime if deadline
|
|
205
|
-
raise Socketry::TimeoutError, "read timed out after #{timeout} seconds" if timeout && time_remaining <= 0
|
|
206
|
-
|
|
207
|
-
chunk = readpartial(size - outbuf.size, timeout: time_remaining)
|
|
208
|
-
return :eof if chunk == :eof
|
|
209
|
-
|
|
210
|
-
outbuf << chunk
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
outbuf
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Perform a non-blocking write operation
|
|
218
|
-
#
|
|
219
|
-
# @param data [String] data to write to the socket
|
|
220
|
-
#
|
|
221
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
222
|
-
#
|
|
223
|
-
# @return [Fixnum, :wait_writable] number of bytes written, or :wait_writable if op would block
|
|
224
|
-
def write_nonblock(data)
|
|
225
|
-
ensure_connected
|
|
226
|
-
@socket.write_nonblock(data, exception: false)
|
|
227
|
-
rescue IO::WaitWriteable
|
|
228
|
-
# Some buggy Rubies continue to raise this exception
|
|
229
|
-
:wait_writable
|
|
230
|
-
rescue IOError => ex
|
|
231
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Write a partial amounth of data, blocking until it's completed
|
|
235
|
-
#
|
|
236
|
-
# @param data [String] data to write to the socket
|
|
237
|
-
# @param timeout [Numeric] Number of seconds to wait for write operation to complete
|
|
238
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
239
|
-
# @return [Fixnum, :eof] number of bytes written, or :eof if socket closed during writing
|
|
240
|
-
def writepartial(data, timeout: @write_timeout)
|
|
241
|
-
set_timeout(timeout)
|
|
242
|
-
|
|
243
|
-
begin
|
|
244
|
-
while (result = write_nonblock(data)) == :wait_writable
|
|
245
|
-
next if @socket.wait_writable(time_remaining(timeout))
|
|
246
|
-
raise TimeoutError, "write timed out after #{timeout} seconds"
|
|
247
|
-
end
|
|
248
|
-
ensure
|
|
249
|
-
clear_timeout(timeout)
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
result || :eof
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Write all of the data in a given string to a socket unless timeout or EOF
|
|
256
|
-
#
|
|
257
|
-
# @param data [String] data to write to the socket
|
|
258
|
-
# @param timeout [Numeric] Number of seconds to wait for write operation to complete
|
|
259
|
-
#
|
|
260
|
-
# @raise [Socketry::Error] an I/O operation failed
|
|
261
|
-
#
|
|
262
|
-
# @return [Fixnum] number of bytes written, or :eof if socket closed during writing
|
|
263
|
-
def write(data, timeout: @write_timeout)
|
|
264
|
-
total_written = data.size
|
|
265
|
-
deadline = lifetime + timeout if timeout
|
|
266
|
-
|
|
267
|
-
begin
|
|
268
|
-
until data.empty?
|
|
269
|
-
time_remaining = deadline - lifetime if deadline
|
|
270
|
-
raise Socketry::TimeoutError, "write timed out after #{timeout} seconds" if timeout && time_remaining <= 0
|
|
271
|
-
|
|
272
|
-
bytes_written = writepartial(data, timeout: time_remaining)
|
|
273
|
-
return :eof if bytes_written == :eof
|
|
274
|
-
|
|
275
|
-
break if bytes_written == data.bytesize
|
|
276
|
-
data = data.byteslice(bytes_written..-1)
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
total_written
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# Check whether Nagle's algorithm has been disabled
|
|
284
|
-
#
|
|
285
|
-
# @return [true] Nagle's algorithm has been explicitly disabled
|
|
286
|
-
# @return [false] Nagle's algorithm is enabled (default)
|
|
287
|
-
def nodelay
|
|
288
|
-
ensure_connected
|
|
289
|
-
@socket.getsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY).int.nonzero?
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Disable or enable Nagle's algorithm
|
|
293
|
-
#
|
|
294
|
-
# @param flag [true, false] disable or enable coalescing multiple writesusing Nagle's algorithm
|
|
295
|
-
def nodelay=(flag)
|
|
296
|
-
ensure_connected
|
|
297
|
-
@socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, flag ? 1 : 0)
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# Return a raw Ruby I/O object
|
|
301
|
-
#
|
|
302
|
-
# @return [IO] Ruby I/O object
|
|
303
|
-
def to_io
|
|
304
|
-
ensure_connected
|
|
305
|
-
::IO.try_convert(@socket)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Close the socket
|
|
309
|
-
#
|
|
310
|
-
# @return [true, false] true if the socket was open, false if closed
|
|
311
|
-
def close
|
|
312
|
-
return false if closed?
|
|
313
|
-
@socket.close
|
|
314
|
-
true
|
|
315
|
-
ensure
|
|
316
|
-
@socket = nil
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# Is the socket closed?
|
|
320
|
-
#
|
|
321
|
-
# This method returns the local connection state. However, it's possible
|
|
322
|
-
# the remote side has closed the connection, so it's not actually
|
|
323
|
-
# possible to actually know if the socket is actually still open without
|
|
324
|
-
# reading from or writing to it. It's sort of like the Heisenberg
|
|
325
|
-
# uncertainty principle of sockets.
|
|
326
|
-
#
|
|
327
|
-
# @return [true, false] do we locally think the socket is closed?
|
|
328
|
-
def closed?
|
|
329
|
-
@socket.nil?
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
private
|
|
333
|
-
|
|
334
|
-
def ensure_connected
|
|
335
|
-
raise StateError, "not connected" if closed?
|
|
336
|
-
true
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def ensure_disconnected
|
|
340
|
-
return true if closed?
|
|
341
|
-
raise StateError, "already connected"
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
end
|
data/lib/socketry/timeout.rb
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# Timeout subsystem
|
|
5
|
-
module Timeout
|
|
6
|
-
DEFAULT_TIMER = Hitimes::Interval
|
|
7
|
-
|
|
8
|
-
# Default timeouts (in seconds)
|
|
9
|
-
DEFAULT_TIMEOUTS = {
|
|
10
|
-
read: 5,
|
|
11
|
-
write: 5,
|
|
12
|
-
connect: 5
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
# Start a timer in the included object
|
|
16
|
-
#
|
|
17
|
-
# @param timer [#start, #to_f] a timer object (ideally monotonic)
|
|
18
|
-
# @return [true] timer started successfully
|
|
19
|
-
# @raise [Socketry::InternalError] if timer is already started
|
|
20
|
-
def start_timer(timer = DEFAULT_TIMER_CLASS.new)
|
|
21
|
-
raise Socketry::InternalError, "timer already started" if defined?(@timer)
|
|
22
|
-
raise Socketry::InternalError, "deadline already set" if defined?(@deadline)
|
|
23
|
-
|
|
24
|
-
@deadline = nil
|
|
25
|
-
@timer = timer
|
|
26
|
-
@timer.start
|
|
27
|
-
true
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Return how long since the timer has been started
|
|
31
|
-
#
|
|
32
|
-
# @return [Float] number of seconds since the timer has been started
|
|
33
|
-
# @raise [Socketry::InternalError] if timer has not been started
|
|
34
|
-
def lifetime
|
|
35
|
-
raise Socketry::InternalError, "timer not started" unless @timer
|
|
36
|
-
@timer.to_f
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Set a timeout. Only one timeout may be active at a given time for a given object.
|
|
40
|
-
#
|
|
41
|
-
# @param timeout [Numeric] number of seconds until the timeout is reached
|
|
42
|
-
# @return [Float] deadline (relative to #lifetime) at which the timeout is reached
|
|
43
|
-
# @raise [Socketry::InternalError] if timeout is already set
|
|
44
|
-
def set_timeout(timeout)
|
|
45
|
-
raise Socketry::InternalError, "deadline already set" if @deadline
|
|
46
|
-
return unless timeout
|
|
47
|
-
raise Socketry::TimeoutError, "time expired" if timeout < 0
|
|
48
|
-
|
|
49
|
-
@deadline = lifetime + timeout
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Clear an already-set timeout
|
|
53
|
-
#
|
|
54
|
-
# @param timeout [Numeric] to gauge whether the timeout actually needs to be cleared
|
|
55
|
-
# @raise [Socketry::InternalError] if timeout has not been set
|
|
56
|
-
def clear_timeout(timeout)
|
|
57
|
-
return unless timeout
|
|
58
|
-
raise Socketry::InternalError, "no deadline set" unless @deadline
|
|
59
|
-
@deadline = nil
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Calculate number of seconds remaining until we hit the timeout
|
|
63
|
-
#
|
|
64
|
-
# @param timeout [Numeric] to gauge whether a timeout needs to be calculated
|
|
65
|
-
# @return [Float] number of seconds remaining until we hit the timeout
|
|
66
|
-
# @raise [Socketry::TimeoutError] if we've already hit the timeout
|
|
67
|
-
# @raise [Socketry::InternalError] if timeout has not been set
|
|
68
|
-
def time_remaining(timeout)
|
|
69
|
-
return unless timeout
|
|
70
|
-
raise Socketry::InternalError, "no deadline set" unless @deadline
|
|
71
|
-
remaining = @deadline - lifetime
|
|
72
|
-
raise Socketry::TimeoutError, "time expired" if remaining <= 0
|
|
73
|
-
remaining
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# User Datagram Protocol: "fire-and-forget" packet protocol
|
|
5
|
-
module UDP
|
|
6
|
-
# Represents a received UDP message
|
|
7
|
-
class Datagram
|
|
8
|
-
attr_reader :message, :sockaddr, :remote_host, :remote_addr, :remote_port
|
|
9
|
-
|
|
10
|
-
def initialize(message, sockaddr)
|
|
11
|
-
@message = message
|
|
12
|
-
@sockaddr = sockaddr
|
|
13
|
-
@remote_port = sockaddr[1]
|
|
14
|
-
@remote_host = sockaddr[2]
|
|
15
|
-
@remote_addr = sockaddr[3]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def addrinfo
|
|
19
|
-
addr_family = case @sockaddr[0]
|
|
20
|
-
when "AF_INET" then ::Socket::AF_INET
|
|
21
|
-
when "AF_INET6" then ::Socket::AF_INET6
|
|
22
|
-
else raise Socketry::AddressError, "unsupported IP address family: #{@sockaddr[0]}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
Addrinfo.new(@sockaddr, addr_family, ::Socket::SOCK_DGRAM)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
data/lib/socketry/udp/socket.rb
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Socketry
|
|
4
|
-
# User Datagram Protocol: "fire-and-forget" packet protocol
|
|
5
|
-
module UDP
|
|
6
|
-
# User Datagram Protocol sockets
|
|
7
|
-
class Socket
|
|
8
|
-
include Socketry::Timeout
|
|
9
|
-
|
|
10
|
-
attr_reader :addr_family, :read_timeout, :write_timeout, :resolver, :socket_class
|
|
11
|
-
|
|
12
|
-
# Create a UDP socket matching the given socket's address family
|
|
13
|
-
#
|
|
14
|
-
# @param remote_addr [String] Address to connect/bind to
|
|
15
|
-
# @param resolver [Object] Resolver object to use for resolving DNS names
|
|
16
|
-
#
|
|
17
|
-
# @return [Socketry::UDP::Socket]
|
|
18
|
-
def self.from_addr(remote_addr, resolver: Socketry::Resolver::DEFAULT_RESOLVER)
|
|
19
|
-
addr = resolver.resolve(remote_addr)
|
|
20
|
-
if addr.ipv4? then new(addr_family: :ipv4)
|
|
21
|
-
elsif addr.ipv6? then new(addr_family: :ipv6)
|
|
22
|
-
else raise Socketry::AddressError, "unsupported IP address family: #{addr}"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Create a UDP server bound to the given address and port
|
|
27
|
-
#
|
|
28
|
-
# @param local_addr [String] Local DNS name or IP address to listen on
|
|
29
|
-
# @param local_port [Fixnum] Local UDP port to listen on
|
|
30
|
-
# @param resolver [Object] Resolver object to use for resolving DNS names
|
|
31
|
-
#
|
|
32
|
-
# @return [Socketry::UDP::Socket]
|
|
33
|
-
def self.bind(local_addr, local_port, resolver: Socketry::Resolver::DEFAULT_RESOLVER)
|
|
34
|
-
from_addr(local_addr, resolver: resolver).bind(local_addr, local_port)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Connect to the given address and port
|
|
38
|
-
#
|
|
39
|
-
# @param remote_addr [String] DNS name or IP address of the host to connect to
|
|
40
|
-
# @param remote_port [Fixnum] UDP port to connect to
|
|
41
|
-
# @param resolver [Object] Resolver object to use for resolving DNS names
|
|
42
|
-
#
|
|
43
|
-
# @return [Socketry::UDP::Socket]
|
|
44
|
-
def self.connect(remote_addr, remote_port, resolver: Socketry::Resolver::DEFAULT_RESOLVER)
|
|
45
|
-
from_addr(remote_addr, resolver: resolver).connect(remote_addr, remote_port)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Create a new UDP socket
|
|
49
|
-
#
|
|
50
|
-
# @param addr_family [:ipv4, :ipv6] (default :ipv4) address family for this socket
|
|
51
|
-
# @param read_timeout [Numeric] Seconds to wait before an uncompleted read errors
|
|
52
|
-
# @param write_timeout [Numeric] Seconds to wait before an uncompleted write errors
|
|
53
|
-
# @param timer [Object] Time interval object to use for measuring timeouts
|
|
54
|
-
# @param resolver [Object] Resolver object to use for resolving DNS names
|
|
55
|
-
# @param socket_class [Object] Underlying socket class which implements I/O ops
|
|
56
|
-
#
|
|
57
|
-
# @raise [ArgumentError] an invalid argument was given
|
|
58
|
-
#
|
|
59
|
-
# @return [Socketry::UDP::Socket]
|
|
60
|
-
def initialize(
|
|
61
|
-
addr_family: :ipv4,
|
|
62
|
-
read_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:read],
|
|
63
|
-
write_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:write],
|
|
64
|
-
timer: Socketry::Timeout::DEFAULT_TIMER.new,
|
|
65
|
-
resolver: Socketry::Resolver::DEFAULT_RESOLVER,
|
|
66
|
-
socket_class: ::UDPSocket
|
|
67
|
-
)
|
|
68
|
-
@addr_family = case addr_family
|
|
69
|
-
when :ipv4 then ::Socket::AF_INET
|
|
70
|
-
when :ipv6 then ::Socket::AF_INET6
|
|
71
|
-
when ::Socket::AF_INET, ::Socket::AF_INET6 then addr_family
|
|
72
|
-
else raise ArgumentError, "invalid address family: #{addr_family.inspect}"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
@socket = socket_class.new(@addr_family)
|
|
76
|
-
@read_timeout = read_timeout
|
|
77
|
-
@write_timeout = write_timeout
|
|
78
|
-
@resolver = resolver
|
|
79
|
-
|
|
80
|
-
start_timer(timer)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Start a UDP server bound to a particular address and port
|
|
84
|
-
#
|
|
85
|
-
# @param local_addr [String] Local DNS name or IP address to listen on
|
|
86
|
-
# @param local_port [Fixnum] Local UDP port to listen on
|
|
87
|
-
#
|
|
88
|
-
# @return [self]
|
|
89
|
-
def bind(local_addr, local_port)
|
|
90
|
-
@socket.bind(@resolver.resolve(local_addr).to_s, local_port)
|
|
91
|
-
self
|
|
92
|
-
rescue Errno::EADDRINUSE => ex
|
|
93
|
-
raise AddressInUseError, ex.message, ex.backtrace
|
|
94
|
-
rescue => ex
|
|
95
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Make a UDP client connection to the given address and port
|
|
99
|
-
#
|
|
100
|
-
# @param remote_addr [String] DNS name or IP address of the host to connect to
|
|
101
|
-
# @param remote_port [Fixnum] UDP port to connect to
|
|
102
|
-
#
|
|
103
|
-
# @return [self]
|
|
104
|
-
def connect(remote_addr, remote_port)
|
|
105
|
-
@socket.connect(@resolver.resolve(remote_addr).to_s, remote_port)
|
|
106
|
-
self
|
|
107
|
-
rescue => ex
|
|
108
|
-
# TODO: more specific exceptions
|
|
109
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Perform a non-blocking receive
|
|
113
|
-
#
|
|
114
|
-
# @param maxlen [Fixnum] Maximum packet length to receive
|
|
115
|
-
#
|
|
116
|
-
# @return [Socketry::UDP::Datagram, :wait_readable] Received datagram or indication to wait
|
|
117
|
-
def recvfrom_nonblock(maxlen)
|
|
118
|
-
Socketry::UDP::Datagram.new(*@socket.recvfrom_nonblock(maxlen))
|
|
119
|
-
rescue ::IO::WaitReadable
|
|
120
|
-
:wait_readable
|
|
121
|
-
rescue => ex
|
|
122
|
-
# TODO: more specific exceptions
|
|
123
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Perform a blocking receive
|
|
127
|
-
#
|
|
128
|
-
# @param maxlen [Fixnum] Maximum packet length to receive
|
|
129
|
-
# @param timeout [Numeric] Number of seconds to wait for recvfrom operation to complete
|
|
130
|
-
#
|
|
131
|
-
# @return [String] Received data
|
|
132
|
-
def recvfrom(maxlen, timeout: @read_timeout)
|
|
133
|
-
set_timeout(timeout)
|
|
134
|
-
|
|
135
|
-
begin
|
|
136
|
-
while (result = recvfrom_nonblock(maxlen)) == :wait_readable
|
|
137
|
-
next if @socket.wait_readable(time_remaining(timeout))
|
|
138
|
-
raise Socketry::TimeoutError, "recvfrom timed out after #{timeout} seconds"
|
|
139
|
-
end
|
|
140
|
-
ensure
|
|
141
|
-
clear_timeout(timeout)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
result
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Send a UDP packet to a remote host
|
|
148
|
-
#
|
|
149
|
-
# @param msg [String] Data to write to the remote host/port
|
|
150
|
-
# @param host [String] Remote host to send data to. May be omitted if `connect` was called previously
|
|
151
|
-
# @param port [Fixnum] UDP port to send data to. May be omitted if `connect` was called previously
|
|
152
|
-
#
|
|
153
|
-
# @return [Fixum] Number of bytes sent
|
|
154
|
-
def send(msg, host: nil, port: nil)
|
|
155
|
-
host = @resolver.resolve(host).to_s if host
|
|
156
|
-
if host || port
|
|
157
|
-
@socket.send(msg, 0, host, port)
|
|
158
|
-
else
|
|
159
|
-
@socket.send(msg, 0)
|
|
160
|
-
end
|
|
161
|
-
rescue => ex
|
|
162
|
-
# TODO: more specific exceptions
|
|
163
|
-
raise Socketry::Error, ex.message, ex.backtrace
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Close the socket
|
|
167
|
-
#
|
|
168
|
-
# @return [true, false] true if the socket was open, false if closed
|
|
169
|
-
def close
|
|
170
|
-
return false if closed?
|
|
171
|
-
@socket.close
|
|
172
|
-
true
|
|
173
|
-
ensure
|
|
174
|
-
@socket = nil
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Is the socket closed?
|
|
178
|
-
#
|
|
179
|
-
# @return [true, false] do we locally think the socket is closed?
|
|
180
|
-
def closed?
|
|
181
|
-
@socket.nil?
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
data/socketry.gemspec
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require "socketry/version"
|
|
5
|
-
|
|
6
|
-
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name = "socketry"
|
|
8
|
-
spec.version = Socketry::VERSION
|
|
9
|
-
spec.authors = ["Tony Arcieri"]
|
|
10
|
-
spec.email = ["bascule@gmail.com"]
|
|
11
|
-
spec.licenses = ["MIT"]
|
|
12
|
-
spec.homepage = "https://github.com/socketry/socketry/"
|
|
13
|
-
spec.summary = "High-level wrappers for Ruby sockets with advanced thread-safe timeout support"
|
|
14
|
-
spec.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
|
|
15
|
-
Socketry wraps Ruby's sockets with an advanced timeout engine which is able to provide multiple
|
|
16
|
-
simultaneous timeout behaviors in a thread-safe way.
|
|
17
|
-
DESCRIPTION
|
|
18
|
-
|
|
19
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
20
|
-
spec.bindir = "exe"
|
|
21
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
|
-
spec.require_paths = ["lib"]
|
|
23
|
-
|
|
24
|
-
spec.required_ruby_version = ">= 2.2.6"
|
|
25
|
-
|
|
26
|
-
spec.add_runtime_dependency "hitimes", ">= 1.2"
|
|
27
|
-
|
|
28
|
-
spec.add_development_dependency "bundler", "~> 1.0"
|
|
29
|
-
end
|