tcp-client 0.9.3 → 0.10.1
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/README.md +8 -5
- data/lib/tcp-client/address.rb +16 -7
- data/lib/tcp-client/configuration.rb +37 -45
- data/lib/tcp-client/deadline.rb +1 -3
- data/lib/tcp-client/default_configuration.rb +1 -1
- data/lib/tcp-client/errors.rb +13 -7
- data/lib/tcp-client/mixin/io_with_deadline.rb +126 -88
- data/lib/tcp-client/ssl_socket.rb +1 -1
- data/lib/tcp-client/version.rb +2 -1
- data/lib/tcp-client.rb +87 -51
- data/rakefile.rb +7 -1
- data/sample/google_ssl.rb +8 -5
- data/spec/tcp-client/address_spec.rb +15 -28
- data/spec/tcp-client/configuration_spec.rb +4 -4
- data/spec/tcp_client_spec.rb +227 -23
- metadata +3 -3
data/lib/tcp-client.rb
CHANGED
@@ -13,15 +13,18 @@ require_relative 'tcp-client/version'
|
|
13
13
|
# Client class to communicate with a server via TCP w/o SSL.
|
14
14
|
#
|
15
15
|
# All connect/read/write actions can be monitored to ensure that all actions
|
16
|
-
# terminate before given time
|
16
|
+
# terminate before given time limit - or raise an exception.
|
17
17
|
#
|
18
18
|
# @example request to Google.com and limit network interactions to 1.5 seconds
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# client.read(12)
|
22
|
-
# end
|
23
|
-
# # => "HTTP/1.1 200"
|
19
|
+
# # create a configuration to use at least TLS 1.2
|
20
|
+
# cfg = TCPClient::Configuration.create(ssl_params: {min_version: :TLS1_2})
|
24
21
|
#
|
22
|
+
# response =
|
23
|
+
# TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
24
|
+
# client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
25
|
+
# client.readline("\r\n\r\n") #=> see response
|
26
|
+
# end
|
27
|
+
# # response contains the returned message and header
|
25
28
|
#
|
26
29
|
class TCPClient
|
27
30
|
#
|
@@ -30,57 +33,59 @@ class TCPClient
|
|
30
33
|
#
|
31
34
|
# If no `configuration` is given, the {.default_configuration} will be used.
|
32
35
|
#
|
36
|
+
# @overload open(address, configuration = nil)
|
37
|
+
# @yieldparam client [TCPClient] the connected client
|
38
|
+
#
|
39
|
+
# @return [Object] the block result
|
40
|
+
#
|
41
|
+
# @overload open(address, configuration = nil)
|
42
|
+
# @return [TCPClient] the connected client
|
43
|
+
#
|
33
44
|
# If an optional block is given, then the block's result is returned and the
|
34
45
|
# connection will be closed when the block execution ends.
|
35
|
-
# This can be used to create an ad-hoc connection which is
|
46
|
+
# This can be used to create an ad-hoc connection which is guaranteed to be
|
36
47
|
# closed.
|
37
48
|
#
|
38
|
-
# If no block is
|
49
|
+
# If no block is given the connected client instance is returned.
|
39
50
|
# This can be used as a shorthand to create & connect a client.
|
40
51
|
#
|
41
|
-
# @param address [Address, String, Addrinfo, Integer] the address
|
42
|
-
#
|
52
|
+
# @param address [Address, String, Addrinfo, Integer] the target address see
|
53
|
+
# {Address#initialize} for valid formats
|
43
54
|
# @param configuration [Configuration] the {Configuration} to be used for
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# @yieldparam client [TCPClient] the connected client
|
47
|
-
# @yieldreturn [Object] any result
|
48
|
-
#
|
49
|
-
# @return [Object, TCPClient] the block result or the connected client
|
55
|
+
# the new instance
|
50
56
|
#
|
51
57
|
# @see #connect
|
52
58
|
#
|
53
59
|
def self.open(address, configuration = nil)
|
54
60
|
client = new
|
55
|
-
client.connect(
|
61
|
+
client.connect(address, configuration)
|
56
62
|
block_given? ? yield(client) : client
|
57
63
|
ensure
|
58
64
|
client.close if block_given?
|
59
65
|
end
|
60
66
|
|
61
67
|
#
|
62
|
-
# Yields
|
63
|
-
# `address`.It limits all {#read} and {#write} actions within the block to
|
68
|
+
# Yields an instance which is connected to the server on the given
|
69
|
+
# `address`. It limits all {#read} and {#write} actions within the block to
|
64
70
|
# the given time.
|
65
71
|
#
|
66
72
|
# It ensures to close the connection when the block execution ends and returns
|
67
|
-
# the block
|
73
|
+
# the block's result.
|
68
74
|
#
|
69
|
-
# This can be used to create an ad-hoc connection which is
|
75
|
+
# This can be used to create an ad-hoc connection which is guaranteed to be
|
70
76
|
# closed and which {#read}/{#write} call sequence should not last longer than
|
71
|
-
# the `timeout
|
77
|
+
# the `timeout` seconds.
|
72
78
|
#
|
73
79
|
# If no `configuration` is given, the {.default_configuration} will be used.
|
74
80
|
#
|
75
81
|
# @param timeout [Numeric] maximum time in seconds for all {#read} and
|
76
82
|
# {#write} calls within the block
|
77
|
-
# @param address [Address, String, Addrinfo, Integer] the address
|
78
|
-
#
|
83
|
+
# @param address [Address, String, Addrinfo, Integer] the target address see
|
84
|
+
# {Address#initialize} for valid formats
|
79
85
|
# @param configuration [Configuration] the {Configuration} to be used for
|
80
|
-
#
|
86
|
+
# the instance
|
81
87
|
#
|
82
88
|
# @yieldparam client [TCPClient] the connected client
|
83
|
-
# @yieldreturn [Object] any result
|
84
89
|
#
|
85
90
|
# @return [Object] the block's result
|
86
91
|
#
|
@@ -89,7 +94,6 @@ class TCPClient
|
|
89
94
|
def self.with_deadline(timeout, address, configuration = nil)
|
90
95
|
client = nil
|
91
96
|
raise(NoBlockGivenError) unless block_given?
|
92
|
-
address = Address.new(address)
|
93
97
|
client = new
|
94
98
|
client.with_deadline(timeout) do
|
95
99
|
yield(client.connect(address, configuration))
|
@@ -110,7 +114,7 @@ class TCPClient
|
|
110
114
|
|
111
115
|
#
|
112
116
|
# @!parse attr_reader :closed?
|
113
|
-
# @return [Boolean]
|
117
|
+
# @return [Boolean] whether the connection is closed
|
114
118
|
#
|
115
119
|
def closed?
|
116
120
|
@socket.nil? || @socket.closed?
|
@@ -119,7 +123,7 @@ class TCPClient
|
|
119
123
|
#
|
120
124
|
# Close the current connection if connected.
|
121
125
|
#
|
122
|
-
# @return [
|
126
|
+
# @return [TCPClient] itself
|
123
127
|
#
|
124
128
|
def close
|
125
129
|
@socket&.close
|
@@ -131,25 +135,23 @@ class TCPClient
|
|
131
135
|
end
|
132
136
|
|
133
137
|
#
|
134
|
-
# Establishes a new connection to a given `address`.
|
138
|
+
# Establishes a new connection to a server on given `address`.
|
135
139
|
#
|
136
|
-
# It accepts a connection-specific configuration or uses the
|
137
|
-
# {.default_configuration}.
|
138
|
-
# be a copy of the configuration used for this method call. This allows to
|
139
|
-
# configure the behavior per connection.
|
140
|
+
# It accepts a connection-specific `configuration` or uses the
|
141
|
+
# {.default_configuration}.
|
140
142
|
#
|
141
143
|
# The optional `timeout` and `exception` parameters allow to override the
|
142
144
|
# `connect_timeout` and `connect_timeout_error` values.
|
143
145
|
#
|
144
|
-
# @param address [Address, String, Addrinfo, Integer] the address
|
145
|
-
#
|
146
|
+
# @param address [Address, String, Addrinfo, Integer] the target address, see
|
147
|
+
# {Address#initialize} for valid formats
|
146
148
|
# @param configuration [Configuration] the {Configuration} to be used for
|
147
149
|
# this instance
|
148
150
|
# @param timeout [Numeric] maximum time in seconds to connect
|
149
|
-
# @param exception [Class] exception class to be used when the
|
150
|
-
# reached
|
151
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
152
|
+
# connect timeout reached
|
151
153
|
#
|
152
|
-
# @return [
|
154
|
+
# @return [TCPClient] itself
|
153
155
|
#
|
154
156
|
# @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
|
155
157
|
#
|
@@ -157,17 +159,17 @@ class TCPClient
|
|
157
159
|
#
|
158
160
|
def connect(address, configuration = nil, timeout: nil, exception: nil)
|
159
161
|
close if @socket
|
160
|
-
@address = Address.new(address)
|
161
162
|
@configuration = (configuration || Configuration.default).dup
|
162
163
|
raise(NoOpenSSLError) if @configuration.ssl? && !defined?(SSLSocket)
|
164
|
+
@address = stem_errors { Address.new(address) }
|
163
165
|
@socket = create_socket(timeout, exception)
|
164
166
|
self
|
165
167
|
end
|
166
168
|
|
167
169
|
#
|
168
|
-
#
|
170
|
+
# Flushes all internal buffers (write all buffered data).
|
169
171
|
#
|
170
|
-
# @return [
|
172
|
+
# @return [TCPClient] itself
|
171
173
|
#
|
172
174
|
def flush
|
173
175
|
stem_errors { @socket&.flush }
|
@@ -182,8 +184,8 @@ class TCPClient
|
|
182
184
|
#
|
183
185
|
# @param nbytes [Integer] the number of bytes to read
|
184
186
|
# @param timeout [Numeric] maximum time in seconds to read
|
185
|
-
# @param exception [Class] exception class to be used when the
|
186
|
-
# reached
|
187
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
188
|
+
# read timeout reached
|
187
189
|
#
|
188
190
|
# @return [String] the read buffer
|
189
191
|
#
|
@@ -201,6 +203,39 @@ class TCPClient
|
|
201
203
|
end
|
202
204
|
end
|
203
205
|
|
206
|
+
#
|
207
|
+
# Reads the next line from server.
|
208
|
+
#
|
209
|
+
# The standard record separator is used as `separator`.
|
210
|
+
#
|
211
|
+
# The optional `timeout` and `exception` parameters allow to override the
|
212
|
+
# `read_timeout` and `read_timeout_error` values of the used {#configuration}.
|
213
|
+
#
|
214
|
+
# @param separator [String] the line separator to be used
|
215
|
+
# @param timeout [Numeric] maximum time in seconds to read
|
216
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
217
|
+
# read timeout reached
|
218
|
+
#
|
219
|
+
# @return [String] the read line
|
220
|
+
#
|
221
|
+
# @raise [NotConnectedError] if {#connect} was not called before
|
222
|
+
#
|
223
|
+
# @see NetworkError
|
224
|
+
#
|
225
|
+
def readline(separator = $/, chomp: false, timeout: nil, exception: nil)
|
226
|
+
raise(NotConnectedError) if closed?
|
227
|
+
deadline = create_deadline(timeout, configuration.read_timeout)
|
228
|
+
unless deadline.valid?
|
229
|
+
return stem_errors { @socket.readline(separator, chomp: chomp) }
|
230
|
+
end
|
231
|
+
exception ||= configuration.read_timeout_error
|
232
|
+
line =
|
233
|
+
stem_errors(exception) do
|
234
|
+
@socket.readto_with_deadline(separator, deadline, exception)
|
235
|
+
end
|
236
|
+
chomp ? line.chomp : line
|
237
|
+
end
|
238
|
+
|
204
239
|
#
|
205
240
|
# @return [String] the currently used address as text.
|
206
241
|
#
|
@@ -211,7 +246,7 @@ class TCPClient
|
|
211
246
|
end
|
212
247
|
|
213
248
|
#
|
214
|
-
#
|
249
|
+
# Executes a block with a given overall time limit.
|
215
250
|
#
|
216
251
|
# When you like to ensure that a complete {#read}/{#write} communication
|
217
252
|
# sequence with the server is finished before a given amount of time you use
|
@@ -228,9 +263,8 @@ class TCPClient
|
|
228
263
|
# {#write} calls within the block
|
229
264
|
#
|
230
265
|
# @yieldparam client [TCPClient] self
|
231
|
-
# @yieldreturn [Object] any result
|
232
266
|
#
|
233
|
-
# @return [Object] the block
|
267
|
+
# @return [Object] the block's result
|
234
268
|
#
|
235
269
|
# @raise [NoBlockGivenError] if the block is missing
|
236
270
|
#
|
@@ -245,21 +279,23 @@ class TCPClient
|
|
245
279
|
end
|
246
280
|
|
247
281
|
#
|
248
|
-
#
|
282
|
+
# Writes the given `messages` to the server.
|
249
283
|
#
|
250
284
|
# The optional `timeout` and `exception` parameters allow to override the
|
251
285
|
# `write_timeout` and `write_timeout_error` values of the used
|
252
286
|
# {#configuration}.
|
253
287
|
#
|
254
|
-
# @param messages [String] one or more messages to write
|
288
|
+
# @param messages [Array<String>] one or more messages to write
|
255
289
|
# @param timeout [Numeric] maximum time in seconds to write
|
256
|
-
# @param exception [Class] exception class to be used when the
|
257
|
-
# reached
|
290
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
291
|
+
# write timeout reached
|
258
292
|
#
|
259
293
|
# @return [Integer] bytes written
|
260
294
|
#
|
261
295
|
# @raise [NotConnectedError] if {#connect} was not called before
|
262
296
|
#
|
297
|
+
# @see NetworkError
|
298
|
+
#
|
263
299
|
def write(*messages, timeout: nil, exception: nil)
|
264
300
|
raise(NotConnectedError) if closed?
|
265
301
|
deadline = create_deadline(timeout, configuration.write_timeout)
|
data/rakefile.rb
CHANGED
@@ -6,7 +6,13 @@ require 'rspec/core/rake_task'
|
|
6
6
|
require 'yard'
|
7
7
|
|
8
8
|
$stdout.sync = $stderr.sync = true
|
9
|
-
|
9
|
+
|
10
|
+
CLEAN << 'prj' << 'doc'
|
11
|
+
|
12
|
+
CLOBBER << '.yardoc'
|
13
|
+
|
10
14
|
task(:default) { exec('rake --tasks') }
|
15
|
+
|
11
16
|
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
17
|
+
|
12
18
|
YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
|
data/sample/google_ssl.rb
CHANGED
@@ -18,8 +18,11 @@ cfg =
|
|
18
18
|
# - limit all network interactions to 1.5 seconds
|
19
19
|
# - use the Configuration cfg
|
20
20
|
# - send a simple HTTP get request
|
21
|
-
# - read
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
# - read the returned message and headers
|
22
|
+
response =
|
23
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
24
|
+
client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
25
|
+
client.readline("\r\n\r\n") #=> see response
|
26
|
+
end
|
27
|
+
|
28
|
+
puts(response)
|
@@ -9,8 +9,8 @@ RSpec.describe TCPClient::Address do
|
|
9
9
|
|
10
10
|
it 'points to the given port on localhost' do
|
11
11
|
expect(address.hostname).to eq 'localhost'
|
12
|
+
expect(address.port).to be 42
|
12
13
|
expect(address.to_s).to eq 'localhost:42'
|
13
|
-
expect(address.addrinfo.ip_port).to be 42
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'uses IPv6' do
|
@@ -29,8 +29,9 @@ RSpec.describe TCPClient::Address do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'points to the given host and port' do
|
32
|
-
expect(address.hostname).to eq
|
33
|
-
expect(address.
|
32
|
+
expect(address.hostname).to eq 'localhost'
|
33
|
+
expect(address.port).to be 42
|
34
|
+
expect(address.to_s).to eq 'localhost:42'
|
34
35
|
end
|
35
36
|
|
36
37
|
it 'uses IPv6' do
|
@@ -46,30 +47,21 @@ RSpec.describe TCPClient::Address do
|
|
46
47
|
|
47
48
|
it 'points to the given host and port' do
|
48
49
|
expect(address.hostname).to eq 'localhost'
|
50
|
+
expect(address.port).to be 42
|
49
51
|
expect(address.to_s).to eq 'localhost:42'
|
50
|
-
expect(address.addrinfo.ip_port).to be 42
|
51
|
-
end
|
52
|
-
|
53
|
-
it 'uses IPv6' do
|
54
52
|
expect(address.addrinfo.ip?).to be true
|
55
|
-
expect(address.addrinfo.ipv6?).to be true
|
56
|
-
expect(address.addrinfo.ipv4?).to be false
|
57
53
|
end
|
54
|
+
|
58
55
|
end
|
59
56
|
|
60
57
|
context 'when only a port is provided' do
|
61
|
-
subject(:address) { TCPClient::Address.new(':
|
58
|
+
subject(:address) { TCPClient::Address.new(':42') }
|
62
59
|
|
63
60
|
it 'points to the given port on localhost' do
|
64
|
-
expect(address.hostname).to eq ''
|
65
|
-
expect(address.
|
66
|
-
expect(address.
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'uses IPv4' do
|
61
|
+
expect(address.hostname).to eq 'localhost'
|
62
|
+
expect(address.port).to be 42
|
63
|
+
expect(address.to_s).to eq 'localhost:42'
|
70
64
|
expect(address.addrinfo.ip?).to be true
|
71
|
-
expect(address.addrinfo.ipv6?).to be false
|
72
|
-
expect(address.addrinfo.ipv4?).to be true
|
73
65
|
end
|
74
66
|
end
|
75
67
|
|
@@ -78,14 +70,9 @@ RSpec.describe TCPClient::Address do
|
|
78
70
|
|
79
71
|
it 'points to the given port on localhost' do
|
80
72
|
expect(address.hostname).to eq '::1'
|
73
|
+
expect(address.port).to be 42
|
81
74
|
expect(address.to_s).to eq '[::1]:42'
|
82
|
-
expect(address.addrinfo.ip_port).to be 42
|
83
|
-
end
|
84
|
-
|
85
|
-
it 'uses IPv6' do
|
86
75
|
expect(address.addrinfo.ip?).to be true
|
87
|
-
expect(address.addrinfo.ipv6?).to be true
|
88
|
-
expect(address.addrinfo.ipv4?).to be false
|
89
76
|
end
|
90
77
|
end
|
91
78
|
end
|
@@ -108,13 +95,13 @@ RSpec.describe TCPClient::Address do
|
|
108
95
|
expect(address_a).to eq address_b
|
109
96
|
end
|
110
97
|
|
111
|
-
context 'using the ==
|
98
|
+
context 'using the == operator' do
|
112
99
|
it 'compares to equal' do
|
113
100
|
expect(address_a == address_b).to be true
|
114
101
|
end
|
115
102
|
end
|
116
103
|
|
117
|
-
context 'using the ===
|
104
|
+
context 'using the === operator' do
|
118
105
|
it 'compares to equal' do
|
119
106
|
expect(address_a === address_b).to be true
|
120
107
|
end
|
@@ -129,13 +116,13 @@ RSpec.describe TCPClient::Address do
|
|
129
116
|
expect(address_a).not_to eq address_b
|
130
117
|
end
|
131
118
|
|
132
|
-
context 'using the ==
|
119
|
+
context 'using the == operator' do
|
133
120
|
it 'compares not to equal' do
|
134
121
|
expect(address_a == address_b).to be false
|
135
122
|
end
|
136
123
|
end
|
137
124
|
|
138
|
-
context 'using the ===
|
125
|
+
context 'using the === operator' do
|
139
126
|
it 'compares not to equal' do
|
140
127
|
expect(address_a === address_b).to be false
|
141
128
|
end
|
@@ -233,13 +233,13 @@ RSpec.describe TCPClient::Configuration do
|
|
233
233
|
expect(config_a).to eq config_b
|
234
234
|
end
|
235
235
|
|
236
|
-
context 'using the ==
|
236
|
+
context 'using the == operator' do
|
237
237
|
it 'compares to equal' do
|
238
238
|
expect(config_a == config_b).to be true
|
239
239
|
end
|
240
240
|
end
|
241
241
|
|
242
|
-
context 'using the ===
|
242
|
+
context 'using the === operator' do
|
243
243
|
it 'compares to equal' do
|
244
244
|
expect(config_a === config_b).to be true
|
245
245
|
end
|
@@ -254,13 +254,13 @@ RSpec.describe TCPClient::Configuration do
|
|
254
254
|
expect(config_a).not_to eq config_b
|
255
255
|
end
|
256
256
|
|
257
|
-
context 'using the ==
|
257
|
+
context 'using the == operator' do
|
258
258
|
it 'compares not to equal' do
|
259
259
|
expect(config_a == config_b).to be false
|
260
260
|
end
|
261
261
|
end
|
262
262
|
|
263
|
-
context 'using the ===
|
263
|
+
context 'using the === operator' do
|
264
264
|
it 'compares not to equal' do
|
265
265
|
expect(config_a === config_b).to be false
|
266
266
|
end
|