tcp-client 0.1.3 → 0.2.3
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 -12
- data/lib/tcp-client.rb +56 -38
- data/lib/tcp-client/address.rb +1 -1
- data/lib/tcp-client/configuration.rb +55 -14
- data/lib/tcp-client/default_configuration.rb +2 -4
- data/lib/tcp-client/errors.rb +49 -0
- data/lib/tcp-client/mixin/io_timeout.rb +24 -2
- data/lib/tcp-client/mixin/io_with_deadline.rb +83 -0
- data/lib/tcp-client/ssl_socket.rb +6 -5
- data/lib/tcp-client/tcp_socket.rb +4 -3
- data/lib/tcp-client/version.rb +1 -1
- data/rakefile.rb +6 -6
- data/sample/google_ssl.rb +7 -11
- data/test/tcp-client/address_test.rb +1 -2
- data/test/tcp-client/configuration_test.rb +44 -1
- data/test/tcp_client_test.rb +46 -8
- data/test/test_helper.rb +2 -4
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2791642003fb16623a27988401ff3bb532194032a5bbd4f62b853aae94790055
|
4
|
+
data.tar.gz: 030a2b3ebecf1334dea84e2c8f18aecd3b900c7c467c10c624c1354429a66195
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4ef14d49cd323a418190e62bc190af01fcc9be1b7008b7fd27c8fa7b8779ae29947045a660080e9403ba1d932e1318a6f98c819006d816f91eb093caf6d7873
|
7
|
+
data.tar.gz: 29617e860ad0b419638b54a8258541ccf3c668530a38aa2e629badcb373b94df71c33e538d54faf12d732ffd51670e1ad57cc8fabcaac11bb2d4de3d28cd29b1
|
data/README.md
CHANGED
@@ -12,22 +12,18 @@ require 'tcp-client'
|
|
12
12
|
|
13
13
|
TCPClient.configure do |cfg|
|
14
14
|
cfg.connect_timeout = 1 # second to connect the server
|
15
|
-
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
16
|
-
cfg.read_timeout = 0.5 # seconds to read some bytes
|
17
15
|
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
18
16
|
end
|
19
17
|
|
20
|
-
# the following request sequence is not allowed to last longer than 2 seconds:
|
21
|
-
# 1 second to connect (incl. SSL handshake etc.)
|
22
|
-
# + 0.25 seconds to write data
|
23
|
-
# + 0.5 seconds to read a response
|
24
|
-
|
25
18
|
TCPClient.open('www.google.com:443') do |client|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
19
|
+
# query should not last longer than 0.5 seconds
|
20
|
+
client.with_deadline(0.5) do
|
21
|
+
# simple HTTP get request
|
22
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
23
|
+
|
24
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
25
|
+
pp client.read(12)
|
26
|
+
end
|
31
27
|
end
|
32
28
|
```
|
33
29
|
|
data/lib/tcp-client.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'tcp-client/errors'
|
3
4
|
require_relative 'tcp-client/address'
|
4
5
|
require_relative 'tcp-client/tcp_socket'
|
5
6
|
require_relative 'tcp-client/ssl_socket'
|
@@ -8,30 +9,9 @@ require_relative 'tcp-client/default_configuration'
|
|
8
9
|
require_relative 'tcp-client/version'
|
9
10
|
|
10
11
|
class TCPClient
|
11
|
-
class NoOpenSSL < RuntimeError
|
12
|
-
def self.raise!
|
13
|
-
raise(self, 'OpenSSL is not avail', caller(1))
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class NotConnected < SocketError
|
18
|
-
def self.raise!(reason)
|
19
|
-
raise(self, "client not connected - #{reason}", caller(1))
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
TimeoutError = Class.new(IOError)
|
24
|
-
ConnectTimeoutError = Class.new(TimeoutError)
|
25
|
-
ReadTimeoutError = Class.new(TimeoutError)
|
26
|
-
WriteTimeoutError = Class.new(TimeoutError)
|
27
|
-
|
28
|
-
Timeout = TimeoutError # backward compatibility
|
29
|
-
deprecate_constant(:Timeout)
|
30
|
-
|
31
12
|
def self.open(addr, configuration = Configuration.default)
|
32
|
-
addr = Address.new(addr)
|
33
13
|
client = new
|
34
|
-
client.connect(addr, configuration)
|
14
|
+
client.connect(Address.new(addr), configuration)
|
35
15
|
block_given? ? yield(client) : client
|
36
16
|
ensure
|
37
17
|
client&.close if block_given?
|
@@ -40,50 +20,88 @@ class TCPClient
|
|
40
20
|
attr_reader :address
|
41
21
|
|
42
22
|
def initialize
|
43
|
-
@socket = @address = @
|
23
|
+
@socket = @address = @deadline = @cfg = nil
|
44
24
|
end
|
45
25
|
|
46
26
|
def to_s
|
47
27
|
@address ? @address.to_s : ''
|
48
28
|
end
|
49
29
|
|
50
|
-
def connect(addr, configuration)
|
30
|
+
def connect(addr, configuration, exception: nil)
|
51
31
|
close
|
52
32
|
NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
|
53
33
|
@address = Address.new(addr)
|
54
|
-
@
|
55
|
-
configuration.
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
@read_timeout = configuration.read_timeout
|
34
|
+
@cfg = configuration.dup
|
35
|
+
exception ||= configuration.connect_timeout_error
|
36
|
+
@socket = TCPSocket.new(@address, @cfg, exception)
|
37
|
+
@cfg.ssl? &&
|
38
|
+
@socket = SSLSocket.new(@socket, @address, configuration, exception)
|
60
39
|
self
|
61
40
|
end
|
62
41
|
|
63
42
|
def close
|
64
|
-
|
65
|
-
socket&.close
|
43
|
+
@socket&.close
|
66
44
|
self
|
67
45
|
rescue IOError
|
68
46
|
self
|
47
|
+
ensure
|
48
|
+
@socket = @deadline = nil
|
69
49
|
end
|
70
50
|
|
71
51
|
def closed?
|
72
52
|
@socket.nil? || @socket.closed?
|
73
53
|
end
|
74
54
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
55
|
+
def with_deadline(timeout)
|
56
|
+
previous_deadline = @deadline
|
57
|
+
NoBlockGiven.raise! unless block_given?
|
58
|
+
tm = timeout&.to_f
|
59
|
+
InvalidDeadLine.raise! unless tm&.positive?
|
60
|
+
@deadline = Time.now + tm
|
61
|
+
yield(self)
|
62
|
+
ensure
|
63
|
+
@deadline = previous_deadline
|
64
|
+
end
|
65
|
+
|
66
|
+
def read(nbytes, timeout: nil, exception: nil)
|
67
|
+
NotConnected.raise! if closed?
|
68
|
+
timeout.nil? && @deadline and
|
69
|
+
return read_with_deadline(nbytes, @deadline, exception)
|
70
|
+
timeout = (timeout || @cfg.read_timeout).to_f
|
71
|
+
return @socket.read(nbytes) unless timeout.positive?
|
72
|
+
read_with_deadline(nbytes, Time.now + timeout, exception)
|
78
73
|
end
|
79
74
|
|
80
|
-
def write(*msg, timeout:
|
81
|
-
NotConnected.raise!
|
82
|
-
|
75
|
+
def write(*msg, timeout: nil, exception: nil)
|
76
|
+
NotConnected.raise! if closed?
|
77
|
+
timeout.nil? && @deadline and
|
78
|
+
return write_with_deadline(msg, @deadline, exception)
|
79
|
+
timeout = (timeout || @cfg.write_timeout).to_f
|
80
|
+
return @socket.write(*msg) unless timeout.positive?
|
81
|
+
write_with_deadline(msg, Time.now + timeout, exception)
|
83
82
|
end
|
84
83
|
|
85
84
|
def flush
|
86
85
|
@socket.flush unless closed?
|
87
86
|
self
|
88
87
|
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def read_with_deadline(nbytes, deadline, exception)
|
92
|
+
@socket.read_with_deadline(
|
93
|
+
nbytes,
|
94
|
+
deadline,
|
95
|
+
exception || @cfg.read_timeout_error
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def write_with_deadline(msg, deadline, exception)
|
100
|
+
exception ||= @cfg.write_timeout_error
|
101
|
+
result = 0
|
102
|
+
msg.each do |chunk|
|
103
|
+
result += @socket.write_with_deadline(chunk.b, deadline, exception)
|
104
|
+
end
|
105
|
+
result
|
106
|
+
end
|
89
107
|
end
|
data/lib/tcp-client/address.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative 'errors'
|
2
|
+
|
1
3
|
class TCPClient
|
2
4
|
class Configuration
|
3
5
|
def self.create(options = {})
|
@@ -6,15 +8,38 @@ class TCPClient
|
|
6
8
|
ret
|
7
9
|
end
|
8
10
|
|
9
|
-
attr_reader :buffered,
|
11
|
+
attr_reader :buffered,
|
12
|
+
:keep_alive,
|
13
|
+
:reverse_lookup,
|
14
|
+
:timeout,
|
15
|
+
:connect_timeout,
|
16
|
+
:read_timeout,
|
17
|
+
:write_timeout,
|
18
|
+
:connect_timeout_error,
|
19
|
+
:read_timeout_error,
|
20
|
+
:write_timeout_error
|
10
21
|
attr_accessor :ssl_params
|
11
22
|
|
12
23
|
def initialize(options = {})
|
13
24
|
@buffered = @keep_alive = @reverse_lookup = true
|
14
25
|
self.timeout = @ssl_params = nil
|
26
|
+
@connect_timeout_error = ConnectTimeoutError
|
27
|
+
@read_timeout_error = ReadTimeoutError
|
28
|
+
@write_timeout_error = WriteTimeoutError
|
15
29
|
options.each_pair { |attribute, value| set(attribute, value) }
|
16
30
|
end
|
17
31
|
|
32
|
+
def freeze
|
33
|
+
@ssl_params.freeze
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize_copy(_org)
|
38
|
+
super
|
39
|
+
@ssl_params = @ssl_params.dup
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
18
43
|
def ssl?
|
19
44
|
@ssl_params ? true : false
|
20
45
|
end
|
@@ -38,32 +63,41 @@ class TCPClient
|
|
38
63
|
end
|
39
64
|
|
40
65
|
def timeout=(seconds)
|
41
|
-
@timeout =
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
def connect_timeout
|
46
|
-
@connect_timeout || @timeout
|
66
|
+
@timeout =
|
67
|
+
@connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
|
47
68
|
end
|
48
69
|
|
49
70
|
def connect_timeout=(seconds)
|
50
71
|
@connect_timeout = seconds(seconds)
|
51
72
|
end
|
52
73
|
|
53
|
-
def
|
54
|
-
@
|
74
|
+
def read_timeout=(seconds)
|
75
|
+
@read_timeout = seconds(seconds)
|
55
76
|
end
|
56
77
|
|
57
78
|
def write_timeout=(seconds)
|
58
79
|
@write_timeout = seconds(seconds)
|
59
80
|
end
|
60
81
|
|
61
|
-
def
|
62
|
-
|
82
|
+
def timeout_error=(exception)
|
83
|
+
NotAnException.raise!(exception) unless exception_class?(exception)
|
84
|
+
@connect_timeout_error =
|
85
|
+
@read_timeout_error = @write_timeout_error = exception
|
63
86
|
end
|
64
87
|
|
65
|
-
def
|
66
|
-
|
88
|
+
def connect_timeout_error=(exception)
|
89
|
+
NotAnException.raise!(exception) unless exception_class?(exception)
|
90
|
+
@connect_timeout_error = exception
|
91
|
+
end
|
92
|
+
|
93
|
+
def read_timeout_error=(exception)
|
94
|
+
NotAnException.raise!(exception) unless exception_class?(exception)
|
95
|
+
@read_timeout_error = exception
|
96
|
+
end
|
97
|
+
|
98
|
+
def write_timeout_error=(exception)
|
99
|
+
NotAnException.raise!(exception) unless exception_class?(exception)
|
100
|
+
@write_timeout_error = exception
|
67
101
|
end
|
68
102
|
|
69
103
|
def to_h
|
@@ -75,6 +109,9 @@ class TCPClient
|
|
75
109
|
connect_timeout: @connect_timeout,
|
76
110
|
read_timeout: @read_timeout,
|
77
111
|
write_timeout: @write_timeout,
|
112
|
+
connect_timeout_error: @connect_timeout_error,
|
113
|
+
read_timeout_error: @read_timeout_error,
|
114
|
+
write_timeout_error: @write_timeout_error,
|
78
115
|
ssl_params: @ssl_params
|
79
116
|
}
|
80
117
|
end
|
@@ -90,10 +127,14 @@ class TCPClient
|
|
90
127
|
|
91
128
|
private
|
92
129
|
|
130
|
+
def exception_class?(value)
|
131
|
+
value.is_a?(Class) && value < Exception
|
132
|
+
end
|
133
|
+
|
93
134
|
def set(attribute, value)
|
94
135
|
public_send("#{attribute}=", value)
|
95
136
|
rescue NoMethodError
|
96
|
-
raise(
|
137
|
+
UnknownAttribute.raise!(attribute)
|
97
138
|
end
|
98
139
|
|
99
140
|
def seconds(value)
|
@@ -6,10 +6,8 @@ class TCPClient
|
|
6
6
|
class << self
|
7
7
|
attr_reader :default_configuration
|
8
8
|
|
9
|
-
def configure(options = {})
|
10
|
-
|
11
|
-
yield(cfg) if block_given?
|
12
|
-
@default_configuration = cfg
|
9
|
+
def configure(options = {}, &block)
|
10
|
+
@default_configuration = Configuration.create(options, &block)
|
13
11
|
end
|
14
12
|
end
|
15
13
|
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class TCPClient
|
6
|
+
class NoOpenSSL < RuntimeError
|
7
|
+
def self.raise!
|
8
|
+
raise(self, 'OpenSSL is not avail', caller(1))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoBlockGiven < RuntimeError
|
13
|
+
def self.raise!
|
14
|
+
raise(self, 'no block given', caller(1))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidDeadLine < ArgumentError
|
19
|
+
def self.raise!(timeout)
|
20
|
+
raise(self, "invalid deadline - #{timeout}", caller(1))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnknownAttribute < ArgumentError
|
25
|
+
def self.raise!(attribute)
|
26
|
+
raise(self, "unknown attribute - #{attribute}", caller(1))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class NotAnException < TypeError
|
31
|
+
def self.raise!(object)
|
32
|
+
raise(self, "not a valid exception class - #{object.inspect}", caller(1))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class NotConnected < SocketError
|
37
|
+
def self.raise!
|
38
|
+
raise(self, 'client not connected', caller(1))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
TimeoutError = Class.new(IOError)
|
43
|
+
ConnectTimeoutError = Class.new(TimeoutError)
|
44
|
+
ReadTimeoutError = Class.new(TimeoutError)
|
45
|
+
WriteTimeoutError = Class.new(TimeoutError)
|
46
|
+
|
47
|
+
Timeout = TimeoutError # backward compatibility
|
48
|
+
deprecate_constant(:Timeout)
|
49
|
+
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# I keep this file for backward compatibility.
|
2
|
+
# This may be removed in one of the next versions.
|
3
|
+
# Let me know if you like to use it elsewhere...
|
4
|
+
|
1
5
|
IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
|
2
6
|
|
3
7
|
module IOTimeoutMixin
|
@@ -10,6 +14,24 @@ module IOTimeoutMixin
|
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
17
|
+
def read_with_deadline(nbytes, deadline, exception)
|
18
|
+
result = ''.b
|
19
|
+
return result if nbytes.zero?
|
20
|
+
loop do
|
21
|
+
junk_size = nbytes - result.bytesize
|
22
|
+
read =
|
23
|
+
with_deadline(deadline, exception) do
|
24
|
+
read_nonblock(junk_size, exception: false)
|
25
|
+
end
|
26
|
+
unless read
|
27
|
+
close
|
28
|
+
return result
|
29
|
+
end
|
30
|
+
result += read
|
31
|
+
return result if result.bytesize >= nbytes
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
13
35
|
def read(nbytes, timeout: nil, exception: IOTimeoutError)
|
14
36
|
timeout = timeout.to_f
|
15
37
|
return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
|
@@ -35,8 +57,8 @@ module IOTimeoutMixin
|
|
35
57
|
private
|
36
58
|
|
37
59
|
def read_all(nbytes)
|
38
|
-
return '' if nbytes.zero?
|
39
|
-
result = ''
|
60
|
+
return ''.b if nbytes.zero?
|
61
|
+
result = ''.b
|
40
62
|
loop do
|
41
63
|
unless read = yield(nbytes - result.bytesize)
|
42
64
|
close
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module IOWithDeadlineMixin
|
2
|
+
def self.included(mod)
|
3
|
+
im = mod.instance_methods
|
4
|
+
if im.index(:wait_writable) && im.index(:wait_readable)
|
5
|
+
mod.include(ViaWaitMethod)
|
6
|
+
else
|
7
|
+
mod.include(ViaSelect)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_with_deadline(nbytes, deadline, exclass)
|
12
|
+
raise(exclass) if Time.now > deadline
|
13
|
+
result = ''.b
|
14
|
+
return result if nbytes.zero?
|
15
|
+
loop do
|
16
|
+
read =
|
17
|
+
with_deadline(deadline, exclass) do
|
18
|
+
read_nonblock(nbytes - result.bytesize, exception: false)
|
19
|
+
end
|
20
|
+
unless read
|
21
|
+
close
|
22
|
+
return result
|
23
|
+
end
|
24
|
+
result += read
|
25
|
+
return result if result.bytesize >= nbytes
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_with_deadline(data, deadline, exclass)
|
30
|
+
raise(exclass) if Time.now > deadline
|
31
|
+
return 0 if (size = data.bytesize).zero?
|
32
|
+
result = 0
|
33
|
+
loop do
|
34
|
+
written =
|
35
|
+
with_deadline(deadline, exclass) do
|
36
|
+
write_nonblock(data, exception: false)
|
37
|
+
end
|
38
|
+
result += written
|
39
|
+
return result if result >= size
|
40
|
+
data = data.byteslice(written, data.bytesize - written)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ViaWaitMethod
|
45
|
+
private def with_deadline(deadline, exclass)
|
46
|
+
loop do
|
47
|
+
case ret = yield
|
48
|
+
when :wait_writable
|
49
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
50
|
+
raise(exclass) if wait_writable(remaining_time).nil?
|
51
|
+
when :wait_readable
|
52
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
53
|
+
raise(exclass) if wait_readable(remaining_time).nil?
|
54
|
+
else
|
55
|
+
return ret
|
56
|
+
end
|
57
|
+
end
|
58
|
+
rescue Errno::ETIMEDOUT
|
59
|
+
raise(exclass)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ViaSelect
|
64
|
+
private def with_deadline(deadline, exclass)
|
65
|
+
loop do
|
66
|
+
case ret = yield
|
67
|
+
when :wait_writable
|
68
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
69
|
+
raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
70
|
+
when :wait_readable
|
71
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
72
|
+
raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
|
73
|
+
else
|
74
|
+
return ret
|
75
|
+
end
|
76
|
+
end
|
77
|
+
rescue Errno::ETIMEDOUT
|
78
|
+
raise(exclass)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private_constant(:ViaWaitMethod, :ViaSelect)
|
83
|
+
end
|
@@ -4,11 +4,11 @@ rescue LoadError
|
|
4
4
|
return
|
5
5
|
end
|
6
6
|
|
7
|
-
require_relative 'mixin/
|
7
|
+
require_relative 'mixin/io_with_deadline'
|
8
8
|
|
9
9
|
class TCPClient
|
10
10
|
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
11
|
-
include
|
11
|
+
include IOWithDeadlineMixin
|
12
12
|
|
13
13
|
def initialize(socket, address, configuration, exception)
|
14
14
|
ssl_params = Hash[configuration.ssl_params]
|
@@ -31,12 +31,13 @@ class TCPClient
|
|
31
31
|
|
32
32
|
def connect_to(address, check, timeout, exception)
|
33
33
|
self.hostname = address.hostname
|
34
|
-
|
34
|
+
timeout = timeout.to_f
|
35
|
+
if timeout.zero?
|
36
|
+
connect
|
37
|
+
else
|
35
38
|
with_deadline(Time.now + timeout, exception) do
|
36
39
|
connect_nonblock(exception: false)
|
37
40
|
end
|
38
|
-
else
|
39
|
-
connect
|
40
41
|
end
|
41
42
|
post_connection_check(address.hostname) if check
|
42
43
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'socket'
|
2
|
-
require_relative 'mixin/
|
2
|
+
require_relative 'mixin/io_with_deadline'
|
3
3
|
|
4
4
|
class TCPClient
|
5
5
|
class TCPSocket < ::Socket
|
6
|
-
include
|
6
|
+
include IOWithDeadlineMixin
|
7
7
|
|
8
8
|
def initialize(address, configuration, exception)
|
9
9
|
super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
|
@@ -19,7 +19,8 @@ class TCPClient
|
|
19
19
|
address.addrinfo.ip_port,
|
20
20
|
address.addrinfo.ip_address
|
21
21
|
)
|
22
|
-
|
22
|
+
timeout = timeout.to_f
|
23
|
+
return connect(addr) if timeout.zero?
|
23
24
|
with_deadline(Time.now + timeout, exception) do
|
24
25
|
connect_nonblock(addr, exception: false)
|
25
26
|
end
|
data/lib/tcp-client/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -6,12 +6,12 @@ require 'bundler/gem_tasks'
|
|
6
6
|
|
7
7
|
$stdout.sync = $stderr.sync = true
|
8
8
|
|
9
|
-
task(:default) { exec('rake --tasks') }
|
10
|
-
|
11
9
|
CLOBBER << 'prj'
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
task(:default) { exec('rake --tasks') }
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test) do |task|
|
14
|
+
task.test_files = FileList['test/**/*_test.rb']
|
15
|
+
task.ruby_opts = %w[-w]
|
16
|
+
task.verbose = true
|
17
17
|
end
|
data/sample/google_ssl.rb
CHANGED
@@ -2,20 +2,16 @@ require_relative '../lib/tcp-client'
|
|
2
2
|
|
3
3
|
TCPClient.configure do |cfg|
|
4
4
|
cfg.connect_timeout = 1 # second to connect the server
|
5
|
-
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
6
|
-
cfg.read_timeout = 0.5 # seconds to read some bytes
|
7
5
|
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
8
6
|
end
|
9
7
|
|
10
|
-
# the following request sequence is not allowed to last longer than 2 seconds:
|
11
|
-
# 1 second to connect (incl. SSL handshake etc.)
|
12
|
-
# + 0.25 seconds to write data
|
13
|
-
# + 0.5 seconds to read a response
|
14
|
-
|
15
8
|
TCPClient.open('www.google.com:443') do |client|
|
16
|
-
#
|
17
|
-
|
9
|
+
# query should not last longer than 0.5 seconds
|
10
|
+
client.with_deadline(0.5) do
|
11
|
+
# simple HTTP get request
|
12
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
18
13
|
|
19
|
-
|
20
|
-
|
14
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
15
|
+
pp client.read(12)
|
16
|
+
end
|
21
17
|
end
|
@@ -13,7 +13,7 @@ class AddressTest < MiniTest::Test
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_create_from_addrinfo
|
16
|
-
addrinfo = Addrinfo.tcp('
|
16
|
+
addrinfo = Addrinfo.tcp('localhost', 42)
|
17
17
|
subject = TCPClient::Address.new(addrinfo)
|
18
18
|
assert_equal(addrinfo.getnameinfo[0], subject.hostname)
|
19
19
|
assert_equal(addrinfo, subject.addrinfo)
|
@@ -62,5 +62,4 @@ class AddressTest < MiniTest::Test
|
|
62
62
|
assert(a == b)
|
63
63
|
assert(a === b)
|
64
64
|
end
|
65
|
-
|
66
65
|
end
|
@@ -39,7 +39,8 @@ class ConfigurationTest < MiniTest::Test
|
|
39
39
|
connect_timeout: 1,
|
40
40
|
read_timeout: 2,
|
41
41
|
write_timeout: 3,
|
42
|
-
ssl: true
|
42
|
+
ssl: true,
|
43
|
+
connect_timeout_error: IOError
|
43
44
|
)
|
44
45
|
refute(subject.buffered)
|
45
46
|
refute(subject.keep_alive)
|
@@ -48,6 +49,8 @@ class ConfigurationTest < MiniTest::Test
|
|
48
49
|
assert_same(2, subject.read_timeout)
|
49
50
|
assert_same(3, subject.write_timeout)
|
50
51
|
assert(subject.ssl?)
|
52
|
+
assert_same(IOError, subject.connect_timeout_error)
|
53
|
+
assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
|
51
54
|
end
|
52
55
|
|
53
56
|
def test_invalid_option
|
@@ -88,6 +91,18 @@ class ConfigurationTest < MiniTest::Test
|
|
88
91
|
assert_same(42, subject.write_timeout)
|
89
92
|
end
|
90
93
|
|
94
|
+
def test_timeout_error_overwrite
|
95
|
+
subject = TCPClient::Configuration.new
|
96
|
+
assert_same(TCPClient::ConnectTimeoutError, subject.connect_timeout_error)
|
97
|
+
assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
|
98
|
+
assert_same(TCPClient::WriteTimeoutError, subject.write_timeout_error)
|
99
|
+
|
100
|
+
subject.timeout_error = IOError
|
101
|
+
assert_same(IOError, subject.connect_timeout_error)
|
102
|
+
assert_same(IOError, subject.read_timeout_error)
|
103
|
+
assert_same(IOError, subject.write_timeout_error)
|
104
|
+
end
|
105
|
+
|
91
106
|
def test_compare
|
92
107
|
a = TCPClient::Configuration.new
|
93
108
|
b = TCPClient::Configuration.new
|
@@ -95,4 +110,32 @@ class ConfigurationTest < MiniTest::Test
|
|
95
110
|
assert(a == b)
|
96
111
|
assert(a === b)
|
97
112
|
end
|
113
|
+
|
114
|
+
def test_dup
|
115
|
+
source =
|
116
|
+
TCPClient::Configuration.new(
|
117
|
+
buffered: false,
|
118
|
+
keep_alive: false,
|
119
|
+
reverse_lookup: false,
|
120
|
+
connect_timeout: 1,
|
121
|
+
read_timeout: 2,
|
122
|
+
write_timeout: 3,
|
123
|
+
ssl: {
|
124
|
+
ssl_version: :TLSv1_2
|
125
|
+
}
|
126
|
+
)
|
127
|
+
shadow = source.dup.freeze
|
128
|
+
|
129
|
+
# some changes
|
130
|
+
source.buffered = true
|
131
|
+
source.write_timeout = 5
|
132
|
+
source.ssl_params[:err] = true
|
133
|
+
source.timeout_error = IOError
|
134
|
+
|
135
|
+
refute_equal(source.__id__, shadow.__id__)
|
136
|
+
refute(shadow.buffered)
|
137
|
+
assert_equal(3, shadow.write_timeout)
|
138
|
+
assert_equal({ ssl_version: :TLSv1_2 }, shadow.ssl_params)
|
139
|
+
assert_same(TCPClient::ReadTimeoutError, shadow.read_timeout_error)
|
140
|
+
end
|
98
141
|
end
|
data/test/tcp_client_test.rb
CHANGED
@@ -3,6 +3,8 @@ require_relative 'test_helper'
|
|
3
3
|
class TCPClientTest < MiniTest::Test
|
4
4
|
parallelize_me!
|
5
5
|
|
6
|
+
HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
|
7
|
+
|
6
8
|
attr_reader :config
|
7
9
|
|
8
10
|
def setup
|
@@ -65,7 +67,7 @@ class TCPClientTest < MiniTest::Test
|
|
65
67
|
start_time = Time.now
|
66
68
|
subject.read(42, timeout: timeout)
|
67
69
|
end
|
68
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
70
|
+
assert_in_delta(timeout, Time.now - start_time, 0.15)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
@@ -81,27 +83,63 @@ class TCPClientTest < MiniTest::Test
|
|
81
83
|
start_time = nil
|
82
84
|
assert_raises(TCPClient::WriteTimeoutError) do
|
83
85
|
start_time = Time.now
|
84
|
-
|
85
|
-
# send 1MB to avoid any TCP stack buffering
|
86
|
-
args = Array.new(2024, '?' * 1024)
|
87
|
-
subject.write(*args, timeout: timeout)
|
86
|
+
subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
|
88
87
|
end
|
89
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
88
|
+
assert_in_delta(timeout, Time.now - start_time, 0.15)
|
90
89
|
end
|
91
90
|
end
|
92
91
|
|
93
92
|
def test_write_timeout
|
94
|
-
check_write_timeout(0.
|
93
|
+
check_write_timeout(0.01)
|
95
94
|
check_write_timeout(0.25)
|
96
95
|
end
|
97
96
|
|
97
|
+
def test_write_deadline
|
98
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
99
|
+
refute(subject.closed?)
|
100
|
+
assert_raises(TCPClient::WriteTimeoutError) do
|
101
|
+
subject.with_deadline(0.25) do |*args|
|
102
|
+
assert_equal([subject], args)
|
103
|
+
loop { subject.write('some data here') }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_read_deadline
|
110
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
111
|
+
refute(subject.closed?)
|
112
|
+
assert_raises(TCPClient::ReadTimeoutError) do
|
113
|
+
subject.with_deadline(0.25) do |*args|
|
114
|
+
assert_equal([subject], args)
|
115
|
+
loop { subject.read(0) }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def xtest_read_write_deadline
|
122
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
123
|
+
refute(subject.closed?)
|
124
|
+
assert_raises(TCPClient::TimeoutError) do
|
125
|
+
subject.with_deadline(0.25) do |*args|
|
126
|
+
assert_equal([subject], args)
|
127
|
+
loop do
|
128
|
+
subject.write('HUGE_AMOUNT_OF_DATA')
|
129
|
+
subject.read(0)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
98
136
|
def check_connect_timeout(ssl_config)
|
99
137
|
start_time = nil
|
100
138
|
assert_raises(TCPClient::ConnectTimeoutError) do
|
101
139
|
start_time = Time.now
|
102
140
|
TCPClient.new.connect('localhost:1234', ssl_config)
|
103
141
|
end
|
104
|
-
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.
|
142
|
+
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
|
105
143
|
end
|
106
144
|
|
107
145
|
def test_connect_ssl_timeout
|
data/test/test_helper.rb
CHANGED
@@ -4,8 +4,6 @@ require 'minitest/autorun'
|
|
4
4
|
require 'minitest/parallel'
|
5
5
|
require_relative '../lib/tcp-client'
|
6
6
|
|
7
|
-
$stdout.sync = $stderr.sync = true
|
8
|
-
|
9
7
|
# this pseudo-server never reads or writes anything
|
10
|
-
|
11
|
-
Minitest.after_run {
|
8
|
+
PseudoServer = TCPServer.new('localhost', 1234)
|
9
|
+
Minitest.after_run { PseudoServer.close }
|
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.2.3
|
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-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -71,7 +71,9 @@ files:
|
|
71
71
|
- lib/tcp-client/address.rb
|
72
72
|
- lib/tcp-client/configuration.rb
|
73
73
|
- lib/tcp-client/default_configuration.rb
|
74
|
+
- lib/tcp-client/errors.rb
|
74
75
|
- lib/tcp-client/mixin/io_timeout.rb
|
76
|
+
- lib/tcp-client/mixin/io_with_deadline.rb
|
75
77
|
- lib/tcp-client/ssl_socket.rb
|
76
78
|
- lib/tcp-client/tcp_socket.rb
|
77
79
|
- lib/tcp-client/version.rb
|
@@ -106,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
108
|
- !ruby/object:Gem::Version
|
107
109
|
version: '0'
|
108
110
|
requirements: []
|
109
|
-
rubygems_version: 3.2.
|
111
|
+
rubygems_version: 3.2.15
|
110
112
|
signing_key:
|
111
113
|
specification_version: 4
|
112
114
|
summary: A TCP client implementation with working timeout support.
|