socketry 0.5.1 → 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.
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Socketry
4
- # Transmission Control Protocol
5
- module TCP
6
- # Transmission Control Protocol servers: Accept connections from the network
7
- class Server
8
- include Socketry::Timeout
9
- alias uptime lifetime
10
-
11
- attr_reader :read_timeout, :write_timeout, :resolver, :socket_class
12
-
13
- # Create a new TCP server, yielding the server socket and auto-closing it
14
- def self.open(hostname_or_port, port = nil, **args)
15
- server = new(hostname_or_port, port, **args)
16
- result = yield server
17
- server.close
18
- result
19
- end
20
-
21
- # Create a new TCP server
22
- #
23
- # @return [Socketry::TCP::Server]
24
- def initialize(
25
- hostname_or_port,
26
- port = nil,
27
- read_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:read],
28
- write_timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:write],
29
- timer: Socketry::Timeout::DEFAULT_TIMER.new,
30
- resolver: Socketry::Resolver::DEFAULT_RESOLVER,
31
- server_class: ::TCPServer,
32
- socket_class: ::TCPSocket
33
- )
34
- @read_timeout = read_timeout
35
- @write_timeout = write_timeout
36
- @resolver = resolver
37
- @socket_class = socket_class
38
-
39
- if port
40
- @server = server_class.new(@resolver.resolve(hostname_or_port).to_s, port)
41
- else
42
- @server = server_class.new(hostname_or_port)
43
- end
44
-
45
- start_timer(timer)
46
- rescue Errno::EADDRINUSE => ex
47
- raise AddressInUseError, ex.message, ex.backtrace
48
- end
49
-
50
- # Accept a connection to the server
51
- #
52
- # @param timeout [Numeric, NilClass] seconds to wait before aborting the accept
53
- # @return [Socketry::TCP::Socket]
54
- def accept(timeout: nil)
55
- set_timeout(timeout)
56
-
57
- begin
58
- # Note: `exception: false` for TCPServer#accept_nonblock is only supported in Ruby 2.3+
59
- ruby_socket = @server.accept_nonblock
60
- rescue IO::WaitReadable, Errno::EAGAIN
61
- # Ruby 2.2 has trouble using io/wait here
62
- retry if IO.select([@server], nil, nil, time_remaining(timeout))
63
- raise Socketry::TimeoutError, "no connection received after #{timeout} seconds"
64
- end
65
-
66
- Socketry::TCP::Socket.new(
67
- read_timeout: @read_timeout,
68
- write_timeout: @write_timeout,
69
- resolver: @resolver,
70
- socket_class: @socket_class
71
- ).from_socket(ruby_socket)
72
- ensure
73
- clear_timeout(timeout)
74
- end
75
-
76
- # Close the server
77
- def close
78
- return false unless @server
79
- @server.close rescue nil
80
- @server = nil
81
- true
82
- end
83
- end
84
- end
85
- end
@@ -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
@@ -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