tcp-client 0.5.1 → 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 +19 -17
- data/lib/tcp-client/address.rb +44 -1
- data/lib/tcp-client/configuration.rb +221 -62
- data/lib/tcp-client/deadline.rb +2 -0
- data/lib/tcp-client/default_configuration.rb +22 -0
- data/lib/tcp-client/errors.rb +84 -9
- data/lib/tcp-client/mixin/io_with_deadline.rb +31 -2
- data/lib/tcp-client/ssl_socket.rb +20 -2
- data/lib/tcp-client/version.rb +4 -1
- data/lib/tcp-client.rb +220 -50
- data/rakefile.rb +5 -8
- data/sample/google.rb +14 -15
- data/sample/google_ssl.rb +19 -13
- data/spec/helper.rb +12 -0
- data/spec/tcp-client/address_spec.rb +145 -0
- data/spec/tcp-client/configuration_spec.rb +269 -0
- data/spec/tcp-client/default_configuration_spec.rb +22 -0
- data/spec/tcp-client/version_spec.rb +13 -0
- data/spec/tcp_client_spec.rb +596 -0
- data/tcp-client.gemspec +3 -2
- metadata +31 -19
- data/test/tcp-client/address_test.rb +0 -67
- data/test/tcp-client/configuration_test.rb +0 -143
- data/test/tcp-client/deadline_test.rb +0 -26
- data/test/tcp-client/default_configuration_test.rb +0 -59
- data/test/tcp-client/version_test.rb +0 -11
- data/test/tcp_client_test.rb +0 -163
- data/test/test_helper.rb +0 -9
data/lib/tcp-client/errors.rb
CHANGED
@@ -1,70 +1,145 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class TCPClient
|
4
|
-
|
4
|
+
#
|
5
|
+
# Raised when a SSL connection should be establshed but the OpenSSL gem is not available.
|
6
|
+
#
|
7
|
+
class NoOpenSSLError < RuntimeError
|
5
8
|
def initialize
|
6
9
|
super('OpenSSL is not available')
|
7
10
|
end
|
8
11
|
end
|
9
12
|
|
10
|
-
|
13
|
+
#
|
14
|
+
# Raised when a method requires a callback block but no such block is specified.
|
15
|
+
#
|
16
|
+
class NoBlockGivenError < ArgumentError
|
11
17
|
def initialize
|
12
18
|
super('no block given')
|
13
19
|
end
|
14
20
|
end
|
15
21
|
|
16
|
-
|
22
|
+
#
|
23
|
+
# Raised when a an invalid timeout value was specified.
|
24
|
+
#
|
25
|
+
class InvalidDeadLineError < ArgumentError
|
17
26
|
def initialize(timeout)
|
18
27
|
super("invalid deadline - #{timeout}")
|
19
28
|
end
|
20
29
|
end
|
21
30
|
|
22
|
-
|
31
|
+
#
|
32
|
+
# Raised by {Configuration} when an undefined attribute should be set.
|
33
|
+
#
|
34
|
+
class UnknownAttributeError < ArgumentError
|
23
35
|
def initialize(attribute)
|
24
36
|
super("unknown attribute - #{attribute}")
|
25
37
|
end
|
26
38
|
end
|
27
39
|
|
28
|
-
|
40
|
+
#
|
41
|
+
# Raised when a given timeout exception parameter is not an exception class.
|
42
|
+
#
|
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
|
58
|
+
|
59
|
+
#
|
60
|
+
# Raised when a {TCPClient} instance should read/write from/to the network but is not connected.
|
61
|
+
#
|
62
|
+
class NotConnectedError < NetworkError
|
35
63
|
def initialize
|
36
64
|
super('client not connected')
|
37
65
|
end
|
38
66
|
end
|
39
67
|
|
40
|
-
|
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
|
+
#
|
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
|
+
#
|
41
83
|
def initialize(message = nil)
|
42
84
|
super(message || "unable to #{action} in time")
|
43
85
|
end
|
44
86
|
|
87
|
+
#
|
88
|
+
# @return [Symbol] the action which timed out
|
89
|
+
#
|
45
90
|
def action
|
46
91
|
:process
|
47
92
|
end
|
48
93
|
end
|
49
94
|
|
95
|
+
#
|
96
|
+
# Raised by default whenever a {TCPClient.connect} timed out.
|
97
|
+
#
|
50
98
|
class ConnectTimeoutError < TimeoutError
|
99
|
+
#
|
100
|
+
# @return [Symbol] the action which timed out: +:connect+
|
101
|
+
#
|
51
102
|
def action
|
52
103
|
:connect
|
53
104
|
end
|
54
105
|
end
|
55
106
|
|
107
|
+
#
|
108
|
+
# Raised by default whenever a {TCPClient.read} timed out.
|
109
|
+
#
|
56
110
|
class ReadTimeoutError < TimeoutError
|
111
|
+
#
|
112
|
+
# @return [Symbol] the action which timed out: +:read+
|
113
|
+
#
|
57
114
|
def action
|
58
115
|
:read
|
59
116
|
end
|
60
117
|
end
|
61
118
|
|
119
|
+
#
|
120
|
+
# Raised by default whenever a {TCPClient.write} timed out.
|
121
|
+
#
|
62
122
|
class WriteTimeoutError < TimeoutError
|
123
|
+
#
|
124
|
+
# @return [Symbol] the action which timed out: +:write+
|
125
|
+
#
|
63
126
|
def action
|
64
127
|
:write
|
65
128
|
end
|
66
129
|
end
|
67
130
|
|
68
|
-
|
69
|
-
|
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
|
137
|
+
deprecate_constant(
|
138
|
+
:NoOpenSSL,
|
139
|
+
:NoBlockGiven,
|
140
|
+
:InvalidDeadLine,
|
141
|
+
:UnknownAttribute,
|
142
|
+
:NotAnException,
|
143
|
+
:NotConnected
|
144
|
+
)
|
70
145
|
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
# @!visibility private
|
4
|
+
module IOWithDeadlineMixin # :nodoc:
|
4
5
|
def self.included(mod)
|
5
6
|
methods = mod.instance_methods
|
6
7
|
if methods.index(:wait_writable) && methods.index(:wait_readable)
|
7
8
|
mod.include(ViaWaitMethod)
|
9
|
+
elsif methods.index(:to_io)
|
10
|
+
mod.include(ViaIOWaitMethod)
|
8
11
|
else
|
9
12
|
mod.include(ViaSelect)
|
10
13
|
end
|
@@ -12,6 +15,13 @@ module IOWithDeadlineMixin
|
|
12
15
|
|
13
16
|
def read_with_deadline(bytes_to_read, deadline, exception)
|
14
17
|
raise(exception) unless deadline.remaining_time
|
18
|
+
if bytes_to_read.nil?
|
19
|
+
return(
|
20
|
+
with_deadline(deadline, exception) do
|
21
|
+
read_nonblock(65_536, exception: false)
|
22
|
+
end
|
23
|
+
)
|
24
|
+
end
|
15
25
|
result = ''.b
|
16
26
|
while result.bytesize < bytes_to_read
|
17
27
|
read =
|
@@ -59,6 +69,25 @@ module IOWithDeadlineMixin
|
|
59
69
|
end
|
60
70
|
end
|
61
71
|
|
72
|
+
module ViaIOWaitMethod
|
73
|
+
private def with_deadline(deadline, exception)
|
74
|
+
loop do
|
75
|
+
case ret = yield
|
76
|
+
when :wait_writable
|
77
|
+
remaining_time = deadline.remaining_time or raise(exception)
|
78
|
+
raise(exception) if to_io.wait_writable(remaining_time).nil?
|
79
|
+
when :wait_readable
|
80
|
+
remaining_time = deadline.remaining_time or raise(exception)
|
81
|
+
raise(exception) if to_io.wait_readable(remaining_time).nil?
|
82
|
+
else
|
83
|
+
return ret
|
84
|
+
end
|
85
|
+
end
|
86
|
+
rescue Errno::ETIMEDOUT
|
87
|
+
raise(exception)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
62
91
|
module ViaSelect
|
63
92
|
private def with_deadline(deadline, exception)
|
64
93
|
loop do
|
@@ -78,5 +107,5 @@ module IOWithDeadlineMixin
|
|
78
107
|
end
|
79
108
|
end
|
80
109
|
|
81
|
-
private_constant(:ViaWaitMethod, :ViaSelect)
|
110
|
+
private_constant(:ViaWaitMethod, :ViaIOWaitMethod, :ViaSelect)
|
82
111
|
end
|
@@ -18,6 +18,7 @@ class TCPClient
|
|
18
18
|
super(socket, create_context(ssl_params))
|
19
19
|
self.sync_close = true
|
20
20
|
self.hostname = address.hostname
|
21
|
+
check_new_session if @new_session
|
21
22
|
deadline.valid? ? connect_with_deadline(deadline, exception) : connect
|
22
23
|
post_connection_check(address.hostname) if should_verify?(ssl_params)
|
23
24
|
end
|
@@ -25,7 +26,19 @@ class TCPClient
|
|
25
26
|
private
|
26
27
|
|
27
28
|
def create_context(ssl_params)
|
28
|
-
|
29
|
+
@new_session = nil
|
30
|
+
::OpenSSL::SSL::SSLContext.new.tap do |ctx|
|
31
|
+
ctx.set_params(ssl_params)
|
32
|
+
ctx.session_cache_mode = CONTEXT_CACHE_MODE
|
33
|
+
ctx.session_new_cb = proc { |_, sess| @new_session = sess }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def check_new_session
|
38
|
+
time = @new_session.time.to_f + @new_session.timeout
|
39
|
+
if Process.clock_gettime(Process::CLOCK_REALTIME) < time
|
40
|
+
self.session = @new_session
|
41
|
+
end
|
29
42
|
end
|
30
43
|
|
31
44
|
def connect_with_deadline(deadline, exception)
|
@@ -33,8 +46,13 @@ class TCPClient
|
|
33
46
|
end
|
34
47
|
|
35
48
|
def should_verify?(ssl_params)
|
36
|
-
ssl_params[:verify_mode] != OpenSSL::SSL::VERIFY_NONE
|
49
|
+
ssl_params[:verify_mode] != ::OpenSSL::SSL::VERIFY_NONE &&
|
50
|
+
context.verify_hostname
|
37
51
|
end
|
52
|
+
|
53
|
+
CONTEXT_CACHE_MODE =
|
54
|
+
::OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
|
55
|
+
::OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
|
38
56
|
end
|
39
57
|
|
40
58
|
private_constant(:SSLSocket)
|
data/lib/tcp-client/version.rb
CHANGED
data/lib/tcp-client.rb
CHANGED
@@ -9,102 +9,272 @@ 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
|
13
|
-
|
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
|
+
#
|
49
|
+
def self.open(address, configuration = nil)
|
14
50
|
client = new
|
15
51
|
client.connect(Address.new(address), configuration)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
52
|
+
block_given? ? yield(client) : client
|
53
|
+
ensure
|
54
|
+
client.close if block_given?
|
55
|
+
end
|
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
|
+
#
|
79
|
+
def self.with_deadline(timeout, address, configuration = nil)
|
80
|
+
client = nil
|
81
|
+
raise(NoBlockGivenError) unless block_given?
|
82
|
+
address = Address.new(address)
|
83
|
+
client = new
|
84
|
+
client.with_deadline(timeout) do
|
85
|
+
yield(client.connect(address, configuration))
|
21
86
|
end
|
87
|
+
ensure
|
88
|
+
client&.close
|
22
89
|
end
|
23
90
|
|
24
|
-
|
91
|
+
#
|
92
|
+
# @return [Address] the address used for this client
|
93
|
+
#
|
94
|
+
attr_reader :address
|
25
95
|
|
26
|
-
|
27
|
-
|
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?
|
28
107
|
end
|
29
108
|
|
109
|
+
#
|
110
|
+
# @return [String] the currently used address as text.
|
111
|
+
#
|
112
|
+
# @see Address#to_s
|
113
|
+
#
|
30
114
|
def to_s
|
31
115
|
@address&.to_s || ''
|
32
116
|
end
|
33
117
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
+
#
|
134
|
+
def connect(address, configuration = nil, timeout: nil, exception: nil)
|
135
|
+
close if @socket
|
136
|
+
raise(NoOpenSSLError) if configuration.ssl? && !defined?(SSLSocket)
|
37
137
|
@address = Address.new(address)
|
38
|
-
@configuration = configuration.dup
|
39
|
-
@socket = create_socket(exception)
|
138
|
+
@configuration = (configuration || Configuration.default).dup
|
139
|
+
@socket = create_socket(timeout, exception)
|
40
140
|
self
|
41
141
|
end
|
42
142
|
|
143
|
+
#
|
144
|
+
# Close the current connection.
|
145
|
+
#
|
146
|
+
# @return [self]
|
147
|
+
#
|
43
148
|
def close
|
44
149
|
@socket&.close
|
45
150
|
self
|
46
|
-
rescue
|
151
|
+
rescue *NETWORK_ERRORS
|
47
152
|
self
|
48
153
|
ensure
|
49
154
|
@socket = @deadline = nil
|
50
155
|
end
|
51
156
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
+
#
|
56
178
|
def with_deadline(timeout)
|
57
179
|
previous_deadline = @deadline
|
58
|
-
raise(
|
180
|
+
raise(NoBlockGivenError) unless block_given?
|
59
181
|
@deadline = Deadline.new(timeout)
|
60
|
-
raise(
|
182
|
+
raise(InvalidDeadLineError, timeout) unless @deadline.valid?
|
61
183
|
yield(self)
|
62
184
|
ensure
|
63
185
|
@deadline = previous_deadline
|
64
186
|
end
|
65
187
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
+
#
|
199
|
+
def read(nbytes = nil, timeout: nil, exception: nil)
|
200
|
+
raise(NotConnectedError) if closed?
|
201
|
+
deadline = create_deadline(timeout, configuration.read_timeout)
|
202
|
+
return stem_errors { @socket.read(nbytes) } unless deadline.valid?
|
203
|
+
exception ||= configuration.read_timeout_error
|
204
|
+
stem_errors(exception) do
|
205
|
+
@socket.read_with_deadline(nbytes, deadline, exception)
|
206
|
+
end
|
73
207
|
end
|
74
208
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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)
|
221
|
+
raise(NotConnectedError) if closed?
|
222
|
+
deadline = create_deadline(timeout, configuration.write_timeout)
|
223
|
+
return stem_errors { @socket.write(*messages) } unless deadline.valid?
|
224
|
+
exception ||= configuration.write_timeout_error
|
225
|
+
stem_errors(exception) do
|
226
|
+
messages.sum do |chunk|
|
227
|
+
@socket.write_with_deadline(chunk.b, deadline, exception)
|
228
|
+
end
|
229
|
+
end
|
82
230
|
end
|
83
231
|
|
232
|
+
#
|
233
|
+
# Flush all internal buffers (write all through).
|
234
|
+
#
|
235
|
+
# @return [self]
|
236
|
+
#
|
84
237
|
def flush
|
85
|
-
@socket
|
238
|
+
stem_errors { @socket&.flush }
|
86
239
|
self
|
87
240
|
end
|
88
241
|
|
89
242
|
private
|
90
243
|
|
91
|
-
def
|
92
|
-
|
93
|
-
deadline = Deadline.new(@configuration.connect_timeout)
|
94
|
-
@socket = TCPSocket.new(@address, @configuration, deadline, exception)
|
95
|
-
return @socket unless @configuration.ssl?
|
96
|
-
SSLSocket.new(@socket, @address, @configuration, deadline, exception)
|
244
|
+
def create_deadline(timeout, default)
|
245
|
+
timeout.nil? && @deadline ? @deadline : Deadline.new(timeout || default)
|
97
246
|
end
|
98
247
|
|
99
|
-
def
|
100
|
-
|
101
|
-
|
248
|
+
def create_socket(timeout, exception)
|
249
|
+
deadline = create_deadline(timeout, configuration.connect_timeout)
|
250
|
+
exception ||= configuration.connect_timeout_error
|
251
|
+
stem_errors(exception) do
|
252
|
+
@socket = TCPSocket.new(address, configuration, deadline, exception)
|
253
|
+
return @socket unless configuration.ssl?
|
254
|
+
SSLSocket.new(@socket, address, configuration, deadline, exception)
|
255
|
+
end
|
102
256
|
end
|
103
257
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
258
|
+
def stem_errors(except = nil)
|
259
|
+
yield
|
260
|
+
rescue *NETWORK_ERRORS => e
|
261
|
+
raise unless configuration.normalize_network_errors
|
262
|
+
(except && e.is_a?(except)) ? raise : raise(NetworkError, e)
|
109
263
|
end
|
264
|
+
|
265
|
+
NETWORK_ERRORS =
|
266
|
+
[
|
267
|
+
Errno::EADDRNOTAVAIL,
|
268
|
+
Errno::ECONNABORTED,
|
269
|
+
Errno::ECONNREFUSED,
|
270
|
+
Errno::ECONNRESET,
|
271
|
+
Errno::EHOSTUNREACH,
|
272
|
+
Errno::EINVAL,
|
273
|
+
Errno::ENETUNREACH,
|
274
|
+
Errno::EPIPE,
|
275
|
+
IOError,
|
276
|
+
SocketError
|
277
|
+
].tap do |errors|
|
278
|
+
errors << ::OpenSSL::SSL::SSLError if defined?(::OpenSSL::SSL::SSLError)
|
279
|
+
end.freeze
|
110
280
|
end
|
data/rakefile.rb
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rake/clean'
|
4
|
-
require 'rake/testtask'
|
5
4
|
require 'bundler/gem_tasks'
|
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
|
-
Rake::
|
14
|
-
task.pattern = 'test/**/*_test.rb'
|
15
|
-
task.warning = task.verbose = true
|
16
|
-
end
|
12
|
+
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
13
|
+
YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
|
data/sample/google.rb
CHANGED
@@ -2,21 +2,20 @@
|
|
2
2
|
|
3
3
|
require_relative '../lib/tcp-client'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# + 0.5 seconds to read a response
|
5
|
+
# global configuration.
|
6
|
+
# - 0.5 seconds to connect the server
|
7
|
+
# - 0.25 seconds to write a single data junk
|
8
|
+
# - 0.25 seconds to read some bytes
|
9
|
+
TCPClient.configure do |cfg|
|
10
|
+
cfg.connect_timeout = 0.5
|
11
|
+
cfg.write_timeout = 0.25
|
12
|
+
cfg.read_timeout = 0.25
|
13
|
+
end
|
15
14
|
|
15
|
+
# request to Google:
|
16
|
+
# - send a simple HTTP get request
|
17
|
+
# - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
|
16
18
|
TCPClient.open('www.google.com:80') do |client|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# read "HTTP/1.1 " + 3 byte HTTP status code
|
21
|
-
pp client.read(12)
|
19
|
+
p client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
|
20
|
+
p client.read(12)
|
22
21
|
end
|