tcp-client 0.8.0 → 0.9.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/.gitignore +3 -1
- data/README.md +1 -1
- data/lib/tcp-client/address.rb +46 -7
- data/lib/tcp-client/configuration.rb +213 -64
- data/lib/tcp-client/default_configuration.rb +22 -0
- data/lib/tcp-client/errors.rb +68 -7
- data/lib/tcp-client/mixin/io_with_deadline.rb +2 -1
- data/lib/tcp-client/version.rb +4 -1
- data/lib/tcp-client.rb +152 -8
- data/rakefile.rb +3 -3
- data/spec/tcp-client/address_spec.rb +0 -13
- data/spec/tcp-client/configuration_spec.rb +0 -39
- data/tcp-client.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20df105b07989653ac9e9c877672318e1da4eb6a5171b088b137422ff03d22db
|
4
|
+
data.tar.gz: 393c2bf3f983f1da7dc8e3517b0cd7b170c2cdede3c546b603def84512e93806
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c94f0a7c51cd5b33edf0de9fb02d454e539f95b79891ad81c81adb4832829059fc34b8c3d4e141594dd0b95752807f1c32dbc9c5192914a5ec02345bb056075c
|
7
|
+
data.tar.gz: 1ab3ef36b1534cf697c911c5d4ce01137dc9860598daa34765521cbec48fe6698294402cf0a0b768f2c460212cb3ce9378adb12ba7e65c76f1531e7bd66a1688
|
data/.gitignore
CHANGED
data/README.md
CHANGED
data/lib/tcp-client/address.rb
CHANGED
@@ -3,9 +3,44 @@
|
|
3
3
|
require 'socket'
|
4
4
|
|
5
5
|
class TCPClient
|
6
|
+
#
|
7
|
+
# The address used by a TCPClient
|
8
|
+
#
|
6
9
|
class Address
|
7
|
-
|
10
|
+
#
|
11
|
+
# @return [String] the host name
|
12
|
+
#
|
13
|
+
attr_reader :hostname
|
8
14
|
|
15
|
+
#
|
16
|
+
# @return [Addrinfo] the address info
|
17
|
+
#
|
18
|
+
attr_reader :addrinfo
|
19
|
+
|
20
|
+
#
|
21
|
+
# Initializes an address
|
22
|
+
# @overload initialize(addr)
|
23
|
+
# The addr can be specified as
|
24
|
+
# - a valid named address containing the port like +my.host.test:80+
|
25
|
+
# - a valid TCPv4 address like +142.250.181.206:80+
|
26
|
+
# - a valid TCPv6 address like +[2001:16b8:5093:3500:ad77:abe6:eb88:47b6]:80+
|
27
|
+
#
|
28
|
+
# @param addr [String] address string
|
29
|
+
#
|
30
|
+
# @overload initialize(address)
|
31
|
+
# Used to create a copy
|
32
|
+
#
|
33
|
+
# @param address [Address]
|
34
|
+
#
|
35
|
+
# @overload initialize(addrinfo)
|
36
|
+
#
|
37
|
+
# @param addrinfo [Addrinfo] containing the addressed host and port
|
38
|
+
#
|
39
|
+
# @overload initialize(port)
|
40
|
+
# Adresses the port on the local machine.
|
41
|
+
#
|
42
|
+
# @param port [Integer] the addressed port
|
43
|
+
#
|
9
44
|
def initialize(addr)
|
10
45
|
case addr
|
11
46
|
when self.class
|
@@ -20,24 +55,28 @@ class TCPClient
|
|
20
55
|
@addrinfo.freeze
|
21
56
|
end
|
22
57
|
|
58
|
+
#
|
59
|
+
# @return [String] text representation of self as "<host>:<port>"
|
60
|
+
#
|
23
61
|
def to_s
|
24
62
|
return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
|
25
63
|
"#{@hostname}:#{@addrinfo.ip_port}"
|
26
64
|
end
|
27
65
|
|
28
|
-
|
66
|
+
#
|
67
|
+
# @return [Hash] containing the host and port
|
68
|
+
#
|
69
|
+
def to_h
|
29
70
|
{ host: @hostname, port: @addrinfo.ip_port }
|
30
71
|
end
|
31
72
|
|
32
|
-
|
33
|
-
args.empty? ? to_hash : to_hash.slice(*args)
|
34
|
-
end
|
35
|
-
|
73
|
+
# @!visibility private
|
36
74
|
def ==(other)
|
37
|
-
|
75
|
+
to_h == other.to_h
|
38
76
|
end
|
39
77
|
alias eql? ==
|
40
78
|
|
79
|
+
# @!visibility private
|
41
80
|
def equal?(other)
|
42
81
|
self.class == other.class && self == other
|
43
82
|
end
|
@@ -3,25 +3,59 @@
|
|
3
3
|
require_relative 'errors'
|
4
4
|
|
5
5
|
class TCPClient
|
6
|
+
#
|
7
|
+
# A Configuration is used to configure the behavior of a {TCPClient} instance.
|
8
|
+
#
|
9
|
+
# It allows to specify to monitor timeout, how to handle exceptions, if SSL
|
10
|
+
# should be used and to setup the underlying Socket.
|
11
|
+
#
|
6
12
|
class Configuration
|
13
|
+
#
|
14
|
+
# @overload create(options)
|
15
|
+
# Shorthand to create a new configuration with given options.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# config = TCPClient::Configuration.create(buffered: false)
|
19
|
+
#
|
20
|
+
# @param options [Hash] see {#initialize} for details
|
21
|
+
#
|
22
|
+
# @return [Configuration] the initialized configuration
|
23
|
+
#
|
24
|
+
# @overload create(&block)
|
25
|
+
# Shorthand to initialize a new configuration.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# config = TCPClient::Configuration.create do |cfg|
|
29
|
+
# cfg.buffered = false
|
30
|
+
# cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @yieldparam cfg {Configuration}
|
34
|
+
#
|
35
|
+
# @return [Configuration] the initialized configuration
|
36
|
+
#
|
7
37
|
def self.create(options = {})
|
8
|
-
|
9
|
-
yield(
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
attr_reader :buffered,
|
14
|
-
:keep_alive,
|
15
|
-
:reverse_lookup,
|
16
|
-
:normalize_network_errors,
|
17
|
-
:connect_timeout,
|
18
|
-
:read_timeout,
|
19
|
-
:write_timeout,
|
20
|
-
:connect_timeout_error,
|
21
|
-
:read_timeout_error,
|
22
|
-
:write_timeout_error
|
23
|
-
attr_accessor :ssl_params
|
38
|
+
cfg = new(options)
|
39
|
+
yield(cfg) if block_given?
|
40
|
+
cfg
|
41
|
+
end
|
24
42
|
|
43
|
+
#
|
44
|
+
# Intializes the instance with given options.
|
45
|
+
#
|
46
|
+
# @param options [Hash]
|
47
|
+
# @option options [Boolean] :buffered, see {#buffered}
|
48
|
+
# @option options [Boolean] :keep_alive, see {#keep_alive}
|
49
|
+
# @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
|
50
|
+
# @option options [Boolean] :normalize_network_errors, see {#normalize_network_errors}
|
51
|
+
# @option options [Numeric] :connect_timeout, see {#connect_timeout}
|
52
|
+
# @option options [Exception] :connect_timeout_error, see {#connect_timeout_error}
|
53
|
+
# @option options [Numeric] :read_timeout, see {#read_timeout}
|
54
|
+
# @option options [Exception] :read_timeout_error, see {#read_timeout_error}
|
55
|
+
# @option options [Numeric] :write_timeout, see {#write_timeout}
|
56
|
+
# @option options [Exception] :write_timeout_error, see {#write_timeout_error}
|
57
|
+
# @option options [Hash<Symbol, Object>] :ssl_params, see {#ssl_params}
|
58
|
+
#
|
25
59
|
def initialize(options = {})
|
26
60
|
@buffered = @keep_alive = @reverse_lookup = true
|
27
61
|
self.timeout = @ssl_params = nil
|
@@ -32,84 +66,188 @@ class TCPClient
|
|
32
66
|
options.each_pair { |attribute, value| set(attribute, value) }
|
33
67
|
end
|
34
68
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
@ssl_params = Hash[@ssl_params] if @ssl_params
|
43
|
-
self
|
44
|
-
end
|
45
|
-
|
46
|
-
def ssl?
|
47
|
-
@ssl_params ? true : false
|
48
|
-
end
|
49
|
-
|
50
|
-
def ssl=(value)
|
51
|
-
@ssl_params =
|
52
|
-
if value.respond_to?(:to_hash)
|
53
|
-
Hash[value.to_hash]
|
54
|
-
else
|
55
|
-
value ? {} : nil
|
56
|
-
end
|
57
|
-
end
|
69
|
+
#
|
70
|
+
# Enables/disables use of Socket-level buffers.
|
71
|
+
#
|
72
|
+
# @return [true] if the connection is allowed to use internal buffers (default)
|
73
|
+
# @return [false] if buffering is not allowed
|
74
|
+
#
|
75
|
+
attr_reader :buffered
|
58
76
|
|
59
77
|
def buffered=(value)
|
60
78
|
@buffered = value ? true : false
|
61
79
|
end
|
62
80
|
|
81
|
+
#
|
82
|
+
# Enables/disables use of Socket-level keep alive handling.
|
83
|
+
#
|
84
|
+
# @return [true] if the connection is allowed to use keep alive signals (default)
|
85
|
+
# @return [false] if the connection should not check keep alive
|
86
|
+
#
|
87
|
+
attr_reader :keep_alive
|
88
|
+
|
63
89
|
def keep_alive=(value)
|
64
90
|
@keep_alive = value ? true : false
|
65
91
|
end
|
66
92
|
|
93
|
+
#
|
94
|
+
# Enables/disables address lookup.
|
95
|
+
#
|
96
|
+
# @return [true] if the connection is allowed to lookup the address (default)
|
97
|
+
# @return [false] if the address lookup is not required
|
98
|
+
#
|
99
|
+
attr_reader :reverse_lookup
|
100
|
+
|
67
101
|
def reverse_lookup=(value)
|
68
102
|
@reverse_lookup = value ? true : false
|
69
103
|
end
|
70
104
|
|
105
|
+
#
|
106
|
+
# Enables/disables if network exceptions should be raised as {NetworkError}.
|
107
|
+
#
|
108
|
+
# @return [true] if all network exceptions should be raised as {NetworkError}
|
109
|
+
# @return [false] if socket/system errors should not be normalzed (default)
|
110
|
+
#
|
111
|
+
attr_reader :normalize_network_errors
|
112
|
+
|
71
113
|
def normalize_network_errors=(value)
|
72
114
|
@normalize_network_errors = value ? true : false
|
73
115
|
end
|
74
116
|
|
75
|
-
|
76
|
-
|
117
|
+
#
|
118
|
+
# @attribute [w] timeout
|
119
|
+
# Shorthand to set timeout value for connect, read and write at once or to disable any timeout monitoring
|
120
|
+
#
|
121
|
+
# @return [Numeric] maximum time in seconds for any action
|
122
|
+
# @return [nil] if all timeout monitoring should be disabled (default)
|
123
|
+
#
|
124
|
+
# @see #connect_timeout
|
125
|
+
# @see #read_timeout
|
126
|
+
# @see #write_timeout
|
127
|
+
#
|
128
|
+
def timeout=(value)
|
129
|
+
@connect_timeout = @write_timeout = @read_timeout = seconds(value)
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# @attribute [w] timeout_error
|
134
|
+
# Shorthand to configure exception class raised when connect, read or write exceeded the configured timeout
|
135
|
+
#
|
136
|
+
# @return [Class] exception class raised
|
137
|
+
#
|
138
|
+
# @raise [NotAnExceptionError] if given argument is not an Exception class
|
139
|
+
#
|
140
|
+
# @see #connect_timeout_error
|
141
|
+
# @see #read_timeout_error
|
142
|
+
# @see #write_timeout_error
|
143
|
+
#
|
144
|
+
def timeout_error=(value)
|
145
|
+
raise(NotAnExceptionError, value) unless exception_class?(value)
|
146
|
+
@connect_timeout_error =
|
147
|
+
@read_timeout_error = @write_timeout_error = value
|
77
148
|
end
|
78
149
|
|
79
|
-
|
80
|
-
|
150
|
+
#
|
151
|
+
# Configures maximum time in seconds to establish a connection.
|
152
|
+
#
|
153
|
+
# @return [Numeric] maximum time in seconds to establish a connection
|
154
|
+
# @return [nil] if the connect time should not be checked (default)
|
155
|
+
#
|
156
|
+
attr_reader :connect_timeout
|
157
|
+
|
158
|
+
def connect_timeout=(value)
|
159
|
+
@connect_timeout = seconds(value)
|
81
160
|
end
|
82
161
|
|
83
|
-
|
84
|
-
|
162
|
+
#
|
163
|
+
# @return [Class] exception class raised if a {TCPClient#connect} timed out
|
164
|
+
# @raise [NotAnExceptionError] if given argument is not an Exception class
|
165
|
+
#
|
166
|
+
attr_reader :connect_timeout_error
|
167
|
+
|
168
|
+
def connect_timeout_error=(value)
|
169
|
+
raise(NotAnExceptionError, value) unless exception_class?(value)
|
170
|
+
@connect_timeout_error = value
|
85
171
|
end
|
86
172
|
|
87
|
-
|
88
|
-
|
173
|
+
#
|
174
|
+
# Configures maximum time in seconds to finish a {TCPClient#read}.
|
175
|
+
#
|
176
|
+
# @return [Numeric] maximum time in seconds to finish a {TCPClient#read} request
|
177
|
+
# @return [nil] if the read time should not be checked (default)
|
178
|
+
#
|
179
|
+
attr_reader :read_timeout
|
180
|
+
|
181
|
+
def read_timeout=(value)
|
182
|
+
@read_timeout = seconds(value)
|
89
183
|
end
|
90
184
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
185
|
+
#
|
186
|
+
# @return [Class] exception class raised if a {TCPClient#read} timed out
|
187
|
+
# @raise [NotAnExceptionError] if given argument is not an Exception class
|
188
|
+
#
|
189
|
+
attr_reader :read_timeout_error
|
190
|
+
|
191
|
+
def read_timeout_error=(value)
|
192
|
+
raise(NotAnExceptionError, value) unless exception_class?(value)
|
193
|
+
@read_timeout_error = value
|
194
|
+
end
|
195
|
+
|
196
|
+
#
|
197
|
+
# Configures maximum time in seconds to finish a {TCPClient#write}.
|
198
|
+
#
|
199
|
+
# @return [Numeric] maximum time in seconds to finish a {TCPClient#write} request
|
200
|
+
# @return [nil] if the write time should not be checked (default)
|
201
|
+
#
|
202
|
+
attr_reader :write_timeout
|
203
|
+
|
204
|
+
def write_timeout=(value)
|
205
|
+
@write_timeout = seconds(value)
|
95
206
|
end
|
96
207
|
|
97
|
-
|
98
|
-
|
99
|
-
|
208
|
+
#
|
209
|
+
# @return [Class] exception class raised if a {TCPClient#write} timed out
|
210
|
+
# @raise [NotAnExceptionError] if given argument is not an Exception class
|
211
|
+
#
|
212
|
+
attr_reader :write_timeout_error
|
213
|
+
|
214
|
+
def write_timeout_error=(value)
|
215
|
+
raise(NotAnExceptionError, value) unless exception_class?(value)
|
216
|
+
@write_timeout_error = value
|
100
217
|
end
|
101
218
|
|
102
|
-
|
103
|
-
|
104
|
-
|
219
|
+
#
|
220
|
+
# @attribute ssl?
|
221
|
+
# @return [Boolean] wheter SSL is configured, see {#ssl_params}
|
222
|
+
#
|
223
|
+
def ssl?
|
224
|
+
@ssl_params ? true : false
|
105
225
|
end
|
106
226
|
|
107
|
-
|
108
|
-
|
109
|
-
|
227
|
+
#
|
228
|
+
# Configures the SSL parameters used to initialize a SSL context.
|
229
|
+
#
|
230
|
+
# @return [Hash<Symbol, Object>] SSL parameters for the SSL context
|
231
|
+
# @return [nil] if no SSL should be used (default)
|
232
|
+
#
|
233
|
+
attr_reader :ssl_params
|
234
|
+
|
235
|
+
def ssl_params=(value)
|
236
|
+
@ssl_params =
|
237
|
+
if value.respond_to?(:to_hash)
|
238
|
+
Hash[value.to_hash]
|
239
|
+
elsif value.respond_to?(:to_h)
|
240
|
+
Hash[value.to_h]
|
241
|
+
else
|
242
|
+
value ? {} : nil
|
243
|
+
end
|
110
244
|
end
|
245
|
+
alias ssl= ssl_params=
|
111
246
|
|
112
|
-
|
247
|
+
#
|
248
|
+
# @return [Hash] configuration as a Hash
|
249
|
+
#
|
250
|
+
def to_h
|
113
251
|
{
|
114
252
|
buffered: @buffered,
|
115
253
|
keep_alive: @keep_alive,
|
@@ -124,15 +262,26 @@ class TCPClient
|
|
124
262
|
}
|
125
263
|
end
|
126
264
|
|
127
|
-
|
128
|
-
|
265
|
+
# @!visibility private
|
266
|
+
def freeze
|
267
|
+
@ssl_params.freeze
|
268
|
+
super
|
269
|
+
end
|
270
|
+
|
271
|
+
# @!visibility private
|
272
|
+
def initialize_copy(_org)
|
273
|
+
super
|
274
|
+
@ssl_params = Hash[@ssl_params] if @ssl_params
|
275
|
+
self
|
129
276
|
end
|
130
277
|
|
278
|
+
# @!visibility private
|
131
279
|
def ==(other)
|
132
|
-
|
280
|
+
to_h == other.to_h
|
133
281
|
end
|
134
282
|
alias eql? ==
|
135
283
|
|
284
|
+
# @!visibility private
|
136
285
|
def equal?(other)
|
137
286
|
self.class == other.class && self == other
|
138
287
|
end
|
@@ -6,14 +6,36 @@ class TCPClient
|
|
6
6
|
@default_configuration = Configuration.new
|
7
7
|
|
8
8
|
class << self
|
9
|
+
#
|
10
|
+
# @return [Configuration] used by default if no dedicated configuration was specified
|
11
|
+
#
|
9
12
|
attr_reader :default_configuration
|
10
13
|
|
14
|
+
#
|
15
|
+
# Configure the default configuration which is used if no dedicated
|
16
|
+
# configuration was specified.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# TCPClient.configure do |cfg|
|
20
|
+
# cfg.buffered = false
|
21
|
+
# cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @param options [Hash] see {Configuration#initialize} for details
|
25
|
+
#
|
26
|
+
# @yieldparam cfg {Configuration} the new configuration
|
27
|
+
#
|
28
|
+
# @return [Configuration] the new default configuration
|
29
|
+
#
|
11
30
|
def configure(options = {}, &block)
|
12
31
|
@default_configuration = Configuration.create(options, &block)
|
13
32
|
end
|
14
33
|
end
|
15
34
|
|
16
35
|
class Configuration
|
36
|
+
#
|
37
|
+
# @return [Configuration] used by default if no dedicated configuration was specified
|
38
|
+
#
|
17
39
|
def self.default
|
18
40
|
TCPClient.default_configuration
|
19
41
|
end
|
data/lib/tcp-client/errors.rb
CHANGED
@@ -1,78 +1,139 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class TCPClient
|
4
|
+
#
|
5
|
+
# Raised when a SSL connection should be establshed but the OpenSSL gem is not available.
|
6
|
+
#
|
4
7
|
class NoOpenSSLError < RuntimeError
|
5
8
|
def initialize
|
6
9
|
super('OpenSSL is not available')
|
7
10
|
end
|
8
11
|
end
|
9
12
|
|
13
|
+
#
|
14
|
+
# Raised when a method requires a callback block but no such block is specified.
|
15
|
+
#
|
10
16
|
class NoBlockGivenError < ArgumentError
|
11
17
|
def initialize
|
12
18
|
super('no block given')
|
13
19
|
end
|
14
20
|
end
|
15
21
|
|
22
|
+
#
|
23
|
+
# Raised when a an invalid timeout value was specified.
|
24
|
+
#
|
16
25
|
class InvalidDeadLineError < ArgumentError
|
17
26
|
def initialize(timeout)
|
18
27
|
super("invalid deadline - #{timeout}")
|
19
28
|
end
|
20
29
|
end
|
21
30
|
|
31
|
+
#
|
32
|
+
# Raised by {Configuration} when an undefined attribute should be set.
|
33
|
+
#
|
22
34
|
class UnknownAttributeError < ArgumentError
|
23
35
|
def initialize(attribute)
|
24
36
|
super("unknown attribute - #{attribute}")
|
25
37
|
end
|
26
38
|
end
|
27
39
|
|
40
|
+
#
|
41
|
+
# Raised when a given timeout exception parameter is not an exception class.
|
42
|
+
#
|
28
43
|
class NotAnExceptionError < TypeError
|
29
44
|
def initialize(object)
|
30
45
|
super("exception class required - #{object.inspect}")
|
31
46
|
end
|
32
47
|
end
|
33
48
|
|
34
|
-
|
49
|
+
#
|
50
|
+
# Base exception class for all network related errors.
|
51
|
+
#
|
52
|
+
# Will be raised for any system level network error when {Configuration.normalize_network_errors} is configured.
|
53
|
+
#
|
54
|
+
# You should catch this exception class when you like to handle any relevant {TCPClient} error.
|
55
|
+
#
|
56
|
+
class NetworkError < StandardError
|
57
|
+
end
|
35
58
|
|
59
|
+
#
|
60
|
+
# Raised when a {TCPClient} instance should read/write from/to the network but is not connected.
|
61
|
+
#
|
36
62
|
class NotConnectedError < NetworkError
|
37
63
|
def initialize
|
38
64
|
super('client not connected')
|
39
65
|
end
|
40
66
|
end
|
41
67
|
|
68
|
+
#
|
69
|
+
# Base exception class for a detected timeout.
|
70
|
+
#
|
71
|
+
# You should catch this exception class when you like to handle any timeout error.
|
72
|
+
#
|
42
73
|
class TimeoutError < NetworkError
|
74
|
+
#
|
75
|
+
# Initializes the instance with an optional message.
|
76
|
+
#
|
77
|
+
# the message will be generated from {#action} when not specified.
|
78
|
+
# @overload initialize
|
79
|
+
# @overload initialize(message)
|
80
|
+
#
|
81
|
+
# @param message [String, #to_s] the error message
|
82
|
+
#
|
43
83
|
def initialize(message = nil)
|
44
84
|
super(message || "unable to #{action} in time")
|
45
85
|
end
|
46
86
|
|
87
|
+
#
|
88
|
+
# @return [Symbol] the action which timed out
|
89
|
+
#
|
47
90
|
def action
|
48
91
|
:process
|
49
92
|
end
|
50
93
|
end
|
51
94
|
|
95
|
+
#
|
96
|
+
# Raised by default whenever a {TCPClient.connect} timed out.
|
97
|
+
#
|
52
98
|
class ConnectTimeoutError < TimeoutError
|
99
|
+
#
|
100
|
+
# @return [Symbol] the action which timed out: +:connect+
|
101
|
+
#
|
53
102
|
def action
|
54
103
|
:connect
|
55
104
|
end
|
56
105
|
end
|
57
106
|
|
107
|
+
#
|
108
|
+
# Raised by default whenever a {TCPClient.read} timed out.
|
109
|
+
#
|
58
110
|
class ReadTimeoutError < TimeoutError
|
111
|
+
#
|
112
|
+
# @return [Symbol] the action which timed out: +:read+
|
113
|
+
#
|
59
114
|
def action
|
60
115
|
:read
|
61
116
|
end
|
62
117
|
end
|
63
118
|
|
119
|
+
#
|
120
|
+
# Raised by default whenever a {TCPClient.write} timed out.
|
121
|
+
#
|
64
122
|
class WriteTimeoutError < TimeoutError
|
123
|
+
#
|
124
|
+
# @return [Symbol] the action which timed out: +:write+
|
125
|
+
#
|
65
126
|
def action
|
66
127
|
:write
|
67
128
|
end
|
68
129
|
end
|
69
130
|
|
70
|
-
NoOpenSSL = NoOpenSSLError
|
71
|
-
NoBlockGiven = NoBlockGivenError
|
72
|
-
InvalidDeadLine = InvalidDeadLineError
|
73
|
-
UnknownAttribute = UnknownAttributeError
|
74
|
-
NotAnException = NotAnExceptionError
|
75
|
-
NotConnected = NotConnectedError
|
131
|
+
NoOpenSSL = NoOpenSSLError # @!visibility private
|
132
|
+
NoBlockGiven = NoBlockGivenError # @!visibility private
|
133
|
+
InvalidDeadLine = InvalidDeadLineError # @!visibility private
|
134
|
+
UnknownAttribute = UnknownAttributeError # @!visibility private
|
135
|
+
NotAnException = NotAnExceptionError # @!visibility private
|
136
|
+
NotConnected = NotConnectedError # @!visibility private
|
76
137
|
deprecate_constant(
|
77
138
|
:NoOpenSSL,
|
78
139
|
:NoBlockGiven,
|
data/lib/tcp-client/version.rb
CHANGED
data/lib/tcp-client.rb
CHANGED
@@ -9,7 +9,43 @@ require_relative 'tcp-client/configuration'
|
|
9
9
|
require_relative 'tcp-client/default_configuration'
|
10
10
|
require_relative 'tcp-client/version'
|
11
11
|
|
12
|
+
#
|
13
|
+
# Client class to communicate with a server via TCP w/o SSL.
|
14
|
+
#
|
15
|
+
# All connect/read/write actions can be monitored to ensure that all actions
|
16
|
+
# terminate before given time limits - or raise an exception.
|
17
|
+
#
|
18
|
+
# @example - request to Google.com and limit all network interactions to 1.5 seconds
|
19
|
+
# TCPClient.with_deadline(1.5, 'www.google.com:443') do |client|
|
20
|
+
# client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
|
21
|
+
# client.read(12)
|
22
|
+
# end
|
23
|
+
# # => "HTTP/1.1 200"
|
24
|
+
#
|
25
|
+
#
|
12
26
|
class TCPClient
|
27
|
+
#
|
28
|
+
# Creates a new instance which is connected to the server on the given
|
29
|
+
# address and uses the given or the {.default_configuration}.
|
30
|
+
#
|
31
|
+
# If an optional block is given, then the block's result is returned and the
|
32
|
+
# connection will be closed when the block execution ends.
|
33
|
+
# This can be used to create an ad-hoc connection which is garanteed to be
|
34
|
+
# closed.
|
35
|
+
#
|
36
|
+
# If no block is giiven the connected client instance is returned.
|
37
|
+
# This can be used as a shorthand to create & connect a client.
|
38
|
+
#
|
39
|
+
# @param address [Address, String, Addrinfo, Integer] the address to connect to, see {Address#initialize} for valid formats
|
40
|
+
# @param configuration [Configuration] the {Configuration} to be used for this instance
|
41
|
+
#
|
42
|
+
# @yieldparam client [TCPClient] the connected client
|
43
|
+
# @yieldreturn [Object] any result
|
44
|
+
#
|
45
|
+
# @return [Object, TCPClient] the block result or the connected client
|
46
|
+
#
|
47
|
+
# @see #connect
|
48
|
+
#
|
13
49
|
def self.open(address, configuration = nil)
|
14
50
|
client = new
|
15
51
|
client.connect(Address.new(address), configuration)
|
@@ -18,6 +54,28 @@ class TCPClient
|
|
18
54
|
client.close if block_given?
|
19
55
|
end
|
20
56
|
|
57
|
+
#
|
58
|
+
# Yields a new instance which is connected to the server on the given
|
59
|
+
# address and uses the given or the {.default_configuration}.
|
60
|
+
# It ensures to close the connection when the block execution ends.
|
61
|
+
# It also limits all {#read} and {#write} actions within the block to a given
|
62
|
+
# time.
|
63
|
+
#
|
64
|
+
# This can be used to create an ad-hoc connection which is garanteed to be
|
65
|
+
# closed and which read/write calls should not last longer than the timeout
|
66
|
+
# limit.
|
67
|
+
#
|
68
|
+
# @param timeout [Numeric] maximum time in seconds for all {#read} and {#write} calls within the block
|
69
|
+
# @param address [Address, String, Addrinfo, Integer] the address to connect to, see {Address#initialize} for valid formats
|
70
|
+
# @param configuration [Configuration] the {Configuration} to be used for this instance
|
71
|
+
#
|
72
|
+
# @yieldparam client [TCPClient] the connected client
|
73
|
+
# @yieldreturn [Object] any result
|
74
|
+
#
|
75
|
+
# @return [Object] the block result
|
76
|
+
#
|
77
|
+
# @see #with_deadline
|
78
|
+
#
|
21
79
|
def self.with_deadline(timeout, address, configuration = nil)
|
22
80
|
client = nil
|
23
81
|
raise(NoBlockGivenError) unless block_given?
|
@@ -30,12 +88,49 @@ class TCPClient
|
|
30
88
|
client&.close
|
31
89
|
end
|
32
90
|
|
33
|
-
|
91
|
+
#
|
92
|
+
# @return [Address] the address used for this client
|
93
|
+
#
|
94
|
+
attr_reader :address
|
34
95
|
|
96
|
+
#
|
97
|
+
# @return [Configuration] the configuration used by this client.
|
98
|
+
#
|
99
|
+
attr_reader :configuration
|
100
|
+
|
101
|
+
#
|
102
|
+
# @attribute [r] closed?
|
103
|
+
# @return [Boolean] true when the connection is closed, false when connected
|
104
|
+
#
|
105
|
+
def closed?
|
106
|
+
@socket.nil? || @socket.closed?
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# @return [String] the currently used address as text.
|
111
|
+
#
|
112
|
+
# @see Address#to_s
|
113
|
+
#
|
35
114
|
def to_s
|
36
115
|
@address&.to_s || ''
|
37
116
|
end
|
38
117
|
|
118
|
+
#
|
119
|
+
# Establishes a new connection to a given address.
|
120
|
+
#
|
121
|
+
# It accepts a connection-specific configuration or uses the global {.default_configuration}. The {#configuration} used by this instance will
|
122
|
+
# be a copy of the configuration used for this method call. This allows to
|
123
|
+
# configure the behavior per connection.
|
124
|
+
#
|
125
|
+
# @param address [Address, String, Addrinfo, Integer] the address to connect to, see {Address#initialize} for valid formats
|
126
|
+
# @param configuration [Configuration] the {Configuration} to be used for this instance
|
127
|
+
# @param timeout [Numeric] maximum time in seconds to read; used to override the configuration's +connect_timeout+.
|
128
|
+
# @param exception [Class] exception class to be used when the read timeout reached; used to override the configuration's +connect_timeout_error+.
|
129
|
+
#
|
130
|
+
# @return [self]
|
131
|
+
#
|
132
|
+
# @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
|
133
|
+
#
|
39
134
|
def connect(address, configuration = nil, timeout: nil, exception: nil)
|
40
135
|
close if @socket
|
41
136
|
raise(NoOpenSSLError) if configuration.ssl? && !defined?(SSLSocket)
|
@@ -45,6 +140,11 @@ class TCPClient
|
|
45
140
|
self
|
46
141
|
end
|
47
142
|
|
143
|
+
#
|
144
|
+
# Close the current connection.
|
145
|
+
#
|
146
|
+
# @return [self]
|
147
|
+
#
|
48
148
|
def close
|
49
149
|
@socket&.close
|
50
150
|
self
|
@@ -54,10 +154,27 @@ class TCPClient
|
|
54
154
|
@socket = @deadline = nil
|
55
155
|
end
|
56
156
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
157
|
+
#
|
158
|
+
# Executes a block with a given overall timeout.
|
159
|
+
#
|
160
|
+
# When you like to ensure that a complete read/write communication sequence
|
161
|
+
# with the server is finished before a given amount of time you can use this
|
162
|
+
# method to define such a deadline.
|
163
|
+
#
|
164
|
+
# @example - ensure to send a welcome message and receive a 64 byte answer from server
|
165
|
+
# answer = client.with_deadline(2.5) do
|
166
|
+
# client.write('Helo')
|
167
|
+
# client.read(64)
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# @param timeout [Numeric] maximum time in seconds for all {#read} and {#write} calls within the block
|
171
|
+
#
|
172
|
+
# @yieldparam client [TCPClient] self
|
173
|
+
#
|
174
|
+
# @return [Object] result of the given block
|
175
|
+
#
|
176
|
+
# @raise [NoBlockGivenError] if the block is missing
|
177
|
+
#
|
61
178
|
def with_deadline(timeout)
|
62
179
|
previous_deadline = @deadline
|
63
180
|
raise(NoBlockGivenError) unless block_given?
|
@@ -68,6 +185,17 @@ class TCPClient
|
|
68
185
|
@deadline = previous_deadline
|
69
186
|
end
|
70
187
|
|
188
|
+
#
|
189
|
+
# Read the given nbytes or the next available buffer from server.
|
190
|
+
#
|
191
|
+
# @param nbytes [Integer] the number of bytes to read
|
192
|
+
# @param timeout [Numeric] maximum time in seconds to read; used to override the configuration's +read_timeout+.
|
193
|
+
# @param exception [Class] exception class to be used when the read timeout reached; used to override the configuration's +read_timeout_error+.
|
194
|
+
#
|
195
|
+
# @return [String] buffer read
|
196
|
+
#
|
197
|
+
# @raise [NotConnectedError] if {#connect} was not called before
|
198
|
+
#
|
71
199
|
def read(nbytes = nil, timeout: nil, exception: nil)
|
72
200
|
raise(NotConnectedError) if closed?
|
73
201
|
deadline = create_deadline(timeout, configuration.read_timeout)
|
@@ -78,18 +206,34 @@ class TCPClient
|
|
78
206
|
end
|
79
207
|
end
|
80
208
|
|
81
|
-
|
209
|
+
#
|
210
|
+
# Write the given messages to the server.
|
211
|
+
#
|
212
|
+
# @param messages [Array<String>] messages to write
|
213
|
+
# @param timeout [Numeric] maximum time in seconds to read; used to override the configuration's +write_timeout+.
|
214
|
+
# @param exception [Class] exception class to be used when the read timeout reached; used to override the configuration's +write_timeout_error+.
|
215
|
+
#
|
216
|
+
# @return [Integer] bytes written
|
217
|
+
#
|
218
|
+
# @raise [NotConnectedError] if {#connect} was not called before
|
219
|
+
#
|
220
|
+
def write(*messages, timeout: nil, exception: nil)
|
82
221
|
raise(NotConnectedError) if closed?
|
83
222
|
deadline = create_deadline(timeout, configuration.write_timeout)
|
84
|
-
return stem_errors { @socket.write(*
|
223
|
+
return stem_errors { @socket.write(*messages) } unless deadline.valid?
|
85
224
|
exception ||= configuration.write_timeout_error
|
86
225
|
stem_errors(exception) do
|
87
|
-
|
226
|
+
messages.sum do |chunk|
|
88
227
|
@socket.write_with_deadline(chunk.b, deadline, exception)
|
89
228
|
end
|
90
229
|
end
|
91
230
|
end
|
92
231
|
|
232
|
+
#
|
233
|
+
# Flush all internal buffers (write all through).
|
234
|
+
#
|
235
|
+
# @return [self]
|
236
|
+
#
|
93
237
|
def flush
|
94
238
|
stem_errors { @socket&.flush }
|
95
239
|
self
|
data/rakefile.rb
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
require 'rake/clean'
|
4
4
|
require 'bundler/gem_tasks'
|
5
5
|
require 'rspec/core/rake_task'
|
6
|
+
require 'yard'
|
6
7
|
|
7
8
|
$stdout.sync = $stderr.sync = true
|
8
9
|
|
9
|
-
CLOBBER << 'prj'
|
10
|
-
|
10
|
+
CLOBBER << 'prj' << 'doc'
|
11
11
|
task(:default) { exec('rake --tasks') }
|
12
|
-
|
13
12
|
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
13
|
+
YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
|
@@ -91,25 +91,12 @@ RSpec.describe TCPClient::Address do
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
describe '#to_hash' do
|
95
|
-
subject(:address) { TCPClient::Address.new('localhost:42') }
|
96
|
-
|
97
|
-
it 'returns itself as an Hash' do
|
98
|
-
expect(address.to_hash).to eq(host: 'localhost', port: 42)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
94
|
describe '#to_h' do
|
103
95
|
subject(:address) { TCPClient::Address.new('localhost:42') }
|
104
96
|
|
105
97
|
it 'returns itself as an Hash' do
|
106
98
|
expect(address.to_h).to eq(host: 'localhost', port: 42)
|
107
99
|
end
|
108
|
-
|
109
|
-
it 'allows to specify the keys the result should contain' do
|
110
|
-
expect(address.to_h(:port)).to eq(port: 42)
|
111
|
-
expect(address.to_h(:host)).to eq(host: 'localhost')
|
112
|
-
end
|
113
100
|
end
|
114
101
|
|
115
102
|
describe 'comparison' do
|
@@ -157,39 +157,6 @@ RSpec.describe TCPClient::Configuration do
|
|
157
157
|
end
|
158
158
|
end
|
159
159
|
|
160
|
-
describe '#to_hash' do
|
161
|
-
subject(:configuration) do
|
162
|
-
TCPClient::Configuration.new(
|
163
|
-
buffered: false,
|
164
|
-
connect_timeout: 1,
|
165
|
-
read_timeout: 2,
|
166
|
-
write_timeout: 3,
|
167
|
-
ssl: {
|
168
|
-
min_version: :TLS1_2,
|
169
|
-
max_version: :TLS1_3
|
170
|
-
}
|
171
|
-
)
|
172
|
-
end
|
173
|
-
|
174
|
-
it 'returns itself as an Hash' do
|
175
|
-
expect(configuration.to_hash).to eq(
|
176
|
-
buffered: false,
|
177
|
-
keep_alive: true,
|
178
|
-
reverse_lookup: true,
|
179
|
-
connect_timeout: 1,
|
180
|
-
connect_timeout_error: TCPClient::ConnectTimeoutError,
|
181
|
-
read_timeout: 2,
|
182
|
-
read_timeout_error: TCPClient::ReadTimeoutError,
|
183
|
-
write_timeout: 3,
|
184
|
-
write_timeout_error: TCPClient::WriteTimeoutError,
|
185
|
-
ssl_params: {
|
186
|
-
min_version: :TLS1_2,
|
187
|
-
max_version: :TLS1_3
|
188
|
-
}
|
189
|
-
)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
160
|
describe '#to_h' do
|
194
161
|
subject(:configuration) do
|
195
162
|
TCPClient::Configuration.new(
|
@@ -221,12 +188,6 @@ RSpec.describe TCPClient::Configuration do
|
|
221
188
|
}
|
222
189
|
)
|
223
190
|
end
|
224
|
-
|
225
|
-
it 'allows to specify the keys the result should contain' do
|
226
|
-
expect(
|
227
|
-
configuration.to_h(:keep_alive, :read_timeout, :write_timeout)
|
228
|
-
).to eq(keep_alive: true, read_timeout: 2, write_timeout: 3)
|
229
|
-
end
|
230
191
|
end
|
231
192
|
|
232
193
|
describe '#dup' do
|
data/tcp-client.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_development_dependency 'bundler'
|
30
30
|
spec.add_development_dependency 'rake'
|
31
31
|
spec.add_development_dependency 'rspec'
|
32
|
+
spec.add_development_dependency 'yard'
|
32
33
|
|
33
34
|
all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
|
34
35
|
spec.test_files = all_files.grep(%r{^spec/})
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
description: |
|
56
70
|
This Gem implements a TCP client with (optional) SSL support.
|
57
71
|
It is an easy to use, versatile configurable client that can correctly
|