tcp-client 0.2.3 → 0.4.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/.gitignore +1 -1
- data/LICENSE +28 -0
- data/README.md +5 -4
- data/lib/tcp-client.rb +17 -22
- data/lib/tcp-client/address.rb +3 -4
- data/lib/tcp-client/configuration.rb +14 -12
- data/lib/tcp-client/deadline.rb +32 -0
- data/lib/tcp-client/default_configuration.rb +2 -0
- data/lib/tcp-client/errors.rb +41 -20
- data/lib/tcp-client/mixin/io_with_deadline.rb +28 -29
- data/lib/tcp-client/ssl_socket.rb +17 -20
- data/lib/tcp-client/tcp_socket.rb +8 -5
- data/lib/tcp-client/version.rb +3 -1
- data/rakefile.rb +2 -3
- data/sample/google.rb +4 -3
- data/sample/google_ssl.rb +5 -3
- data/tcp-client.gemspec +7 -5
- data/test/tcp-client/address_test.rb +2 -0
- data/test/tcp-client/configuration_test.rb +2 -0
- data/test/tcp-client/deadline_test.rb +26 -0
- data/test/tcp-client/version_test.rb +2 -0
- data/test/tcp_client_test.rb +18 -12
- data/test/test_helper.rb +1 -1
- metadata +14 -9
- data/lib/tcp-client/mixin/io_timeout.rb +0 -118
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a9fe010bf08d26652d93212beee9ffe55275b3b051809a3093d2ff066719372
|
4
|
+
data.tar.gz: bf23f4441be6f32d581dfb069afb09bfbb8a9ca78b90f3e0f2332f16abed89e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c929de839e7335c4e8e950d0b96fb6028c74ba1ae6a1acec3bb4a572e54a6564d799a8ee8fdef51b77273a83a2af4c5cc29d66a4ed92c3647e1185e103d82fda
|
7
|
+
data.tar.gz: 00dacdc8d09584e1f4421c7ca62786ab617eaed1005db955ca36da4c1294738591820159d282d27e24fe3194d84e9cd5cb02b47966f231af5d5a26336bcfa366
|
data/.gitignore
CHANGED
data/LICENSE
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
BSD 3-Clause License
|
2
|
+
|
3
|
+
Copyright (c) 2017-2021, Mike Blumtritt. All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
13
|
+
and/or other materials provided with the distribution.
|
14
|
+
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
16
|
+
contributors may be used to endorse or promote products derived from
|
17
|
+
this software without specific prior written permission.
|
18
|
+
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
A TCP client implementation with working timeout support.
|
4
4
|
|
5
5
|
## Description
|
6
|
-
|
6
|
+
|
7
|
+
This Gem implements a TCP client with (optional) SSL support. It is an easy to use, versatile configurable client that can correctly handle time limits. Unlike other implementations, this client respects predefined/configurable time limits for each method (`connect`, `read`, `write`). Deadlines for a sequence of read/write actions can also be monitored.
|
7
8
|
|
8
9
|
## Sample
|
9
10
|
|
@@ -11,15 +12,15 @@ This gem implements a TCP client with (optional) SSL support. The motivation of
|
|
11
12
|
require 'tcp-client'
|
12
13
|
|
13
14
|
TCPClient.configure do |cfg|
|
14
|
-
cfg.connect_timeout = 1 #
|
15
|
+
cfg.connect_timeout = 1 # limit connect time the server to 1 second
|
15
16
|
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
16
17
|
end
|
17
18
|
|
18
19
|
TCPClient.open('www.google.com:443') do |client|
|
19
|
-
#
|
20
|
+
# next sequence should not last longer than 0.5 seconds
|
20
21
|
client.with_deadline(0.5) do
|
21
22
|
# simple HTTP get request
|
22
|
-
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
23
|
+
pp client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
|
23
24
|
|
24
25
|
# read "HTTP/1.1 " + 3 byte HTTP status code
|
25
26
|
pp client.read(12)
|
data/lib/tcp-client.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'tcp-client/errors'
|
4
4
|
require_relative 'tcp-client/address'
|
5
|
+
require_relative 'tcp-client/deadline'
|
5
6
|
require_relative 'tcp-client/tcp_socket'
|
6
7
|
require_relative 'tcp-client/ssl_socket'
|
7
8
|
require_relative 'tcp-client/configuration'
|
@@ -28,8 +29,8 @@ class TCPClient
|
|
28
29
|
end
|
29
30
|
|
30
31
|
def connect(addr, configuration, exception: nil)
|
32
|
+
raise(NoOpenSSL) if configuration.ssl? && !defined?(SSLSocket)
|
31
33
|
close
|
32
|
-
NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
|
33
34
|
@address = Address.new(addr)
|
34
35
|
@cfg = configuration.dup
|
35
36
|
exception ||= configuration.connect_timeout_error
|
@@ -54,31 +55,30 @@ class TCPClient
|
|
54
55
|
|
55
56
|
def with_deadline(timeout)
|
56
57
|
previous_deadline = @deadline
|
57
|
-
NoBlockGiven
|
58
|
-
|
59
|
-
InvalidDeadLine
|
60
|
-
@deadline = Time.now + tm
|
58
|
+
raise(NoBlockGiven) unless block_given?
|
59
|
+
@deadline = Deadline.new(timeout)
|
60
|
+
raise(InvalidDeadLine, timeout) unless @deadline.valid?
|
61
61
|
yield(self)
|
62
62
|
ensure
|
63
63
|
@deadline = previous_deadline
|
64
64
|
end
|
65
65
|
|
66
66
|
def read(nbytes, timeout: nil, exception: nil)
|
67
|
-
NotConnected
|
67
|
+
raise(NotConnected) if closed?
|
68
68
|
timeout.nil? && @deadline and
|
69
69
|
return read_with_deadline(nbytes, @deadline, exception)
|
70
|
-
|
71
|
-
return @socket.read(nbytes) unless
|
72
|
-
read_with_deadline(nbytes,
|
70
|
+
deadline = Deadline.new(timeout || @cfg.read_timeout)
|
71
|
+
return @socket.read(nbytes) unless deadline.valid?
|
72
|
+
read_with_deadline(nbytes, deadline, exception)
|
73
73
|
end
|
74
74
|
|
75
75
|
def write(*msg, timeout: nil, exception: nil)
|
76
|
-
NotConnected
|
76
|
+
raise(NotConnected) if closed?
|
77
77
|
timeout.nil? && @deadline and
|
78
78
|
return write_with_deadline(msg, @deadline, exception)
|
79
|
-
|
80
|
-
return @socket.write(*msg) unless
|
81
|
-
write_with_deadline(msg,
|
79
|
+
deadline = Deadline.new(timeout || @cfg.read_timeout)
|
80
|
+
return @socket.write(*msg) unless deadline.valid?
|
81
|
+
write_with_deadline(msg, deadline, exception)
|
82
82
|
end
|
83
83
|
|
84
84
|
def flush
|
@@ -89,19 +89,14 @@ class TCPClient
|
|
89
89
|
private
|
90
90
|
|
91
91
|
def read_with_deadline(nbytes, deadline, exception)
|
92
|
-
@
|
93
|
-
|
94
|
-
deadline,
|
95
|
-
exception || @cfg.read_timeout_error
|
96
|
-
)
|
92
|
+
exception ||= @cfg.read_timeout_error
|
93
|
+
@socket.read_with_deadline(nbytes, deadline, exception)
|
97
94
|
end
|
98
95
|
|
99
96
|
def write_with_deadline(msg, deadline, exception)
|
100
97
|
exception ||= @cfg.write_timeout_error
|
101
|
-
|
102
|
-
|
103
|
-
result += @socket.write_with_deadline(chunk.b, deadline, exception)
|
98
|
+
msg.sum do |chunk|
|
99
|
+
@socket.write_with_deadline(chunk.b, deadline, exception)
|
104
100
|
end
|
105
|
-
result
|
106
101
|
end
|
107
102
|
end
|
data/lib/tcp-client/address.rb
CHANGED
@@ -10,10 +10,10 @@ class TCPClient
|
|
10
10
|
case addr
|
11
11
|
when self.class
|
12
12
|
init_from_selfclass(addr)
|
13
|
-
when Integer
|
14
|
-
init_from_addrinfo(Addrinfo.tcp(nil, addr))
|
15
13
|
when Addrinfo
|
16
14
|
init_from_addrinfo(addr)
|
15
|
+
when Integer
|
16
|
+
init_from_addrinfo(Addrinfo.tcp(nil, addr))
|
17
17
|
else
|
18
18
|
init_from_string(addr)
|
19
19
|
end
|
@@ -58,8 +58,7 @@ class TCPClient
|
|
58
58
|
|
59
59
|
def from_string(str)
|
60
60
|
idx = str.rindex(':') or return nil, str.to_i
|
61
|
-
name = str[0, idx]
|
62
|
-
name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
|
61
|
+
name = str[0, idx].delete_prefix('[').delete_suffix(']')
|
63
62
|
[name, str[idx + 1, str.size - idx].to_i]
|
64
63
|
end
|
65
64
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'errors'
|
2
4
|
|
3
5
|
class TCPClient
|
@@ -11,7 +13,6 @@ class TCPClient
|
|
11
13
|
attr_reader :buffered,
|
12
14
|
:keep_alive,
|
13
15
|
:reverse_lookup,
|
14
|
-
:timeout,
|
15
16
|
:connect_timeout,
|
16
17
|
:read_timeout,
|
17
18
|
:write_timeout,
|
@@ -45,9 +46,12 @@ class TCPClient
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def ssl=(value)
|
48
|
-
|
49
|
-
|
50
|
-
|
49
|
+
@ssl_params =
|
50
|
+
if Hash === value
|
51
|
+
value.dup
|
52
|
+
else
|
53
|
+
value ? {} : nil
|
54
|
+
end
|
51
55
|
end
|
52
56
|
|
53
57
|
def buffered=(value)
|
@@ -63,8 +67,7 @@ class TCPClient
|
|
63
67
|
end
|
64
68
|
|
65
69
|
def timeout=(seconds)
|
66
|
-
@
|
67
|
-
@connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
|
70
|
+
@connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
|
68
71
|
end
|
69
72
|
|
70
73
|
def connect_timeout=(seconds)
|
@@ -80,23 +83,23 @@ class TCPClient
|
|
80
83
|
end
|
81
84
|
|
82
85
|
def timeout_error=(exception)
|
83
|
-
|
86
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
84
87
|
@connect_timeout_error =
|
85
88
|
@read_timeout_error = @write_timeout_error = exception
|
86
89
|
end
|
87
90
|
|
88
91
|
def connect_timeout_error=(exception)
|
89
|
-
|
92
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
90
93
|
@connect_timeout_error = exception
|
91
94
|
end
|
92
95
|
|
93
96
|
def read_timeout_error=(exception)
|
94
|
-
|
97
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
95
98
|
@read_timeout_error = exception
|
96
99
|
end
|
97
100
|
|
98
101
|
def write_timeout_error=(exception)
|
99
|
-
|
102
|
+
raise(NotAnException, exception) unless exception_class?(exception)
|
100
103
|
@write_timeout_error = exception
|
101
104
|
end
|
102
105
|
|
@@ -105,7 +108,6 @@ class TCPClient
|
|
105
108
|
buffered: @buffered,
|
106
109
|
keep_alive: @keep_alive,
|
107
110
|
reverse_lookup: @reverse_lookup,
|
108
|
-
timeout: @timeout,
|
109
111
|
connect_timeout: @connect_timeout,
|
110
112
|
read_timeout: @read_timeout,
|
111
113
|
write_timeout: @write_timeout,
|
@@ -134,7 +136,7 @@ class TCPClient
|
|
134
136
|
def set(attribute, value)
|
135
137
|
public_send("#{attribute}=", value)
|
136
138
|
rescue NoMethodError
|
137
|
-
|
139
|
+
raise(UnknownAttribute, attribute)
|
138
140
|
end
|
139
141
|
|
140
142
|
def seconds(value)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TCPClient
|
4
|
+
class Deadline
|
5
|
+
MONOTONIC = !!defined?(Process::CLOCK_MONOTONIC)
|
6
|
+
|
7
|
+
def initialize(timeout)
|
8
|
+
timeout = timeout&.to_f
|
9
|
+
@deadline = timeout&.positive? ? now + timeout : 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
@deadline != 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def remaining_time
|
17
|
+
(@deadline != 0) && (remaining = @deadline - now) > 0 ? remaining : nil
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
if MONOTONIC
|
23
|
+
def now
|
24
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
def now
|
28
|
+
::Time.now
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/tcp-client/errors.rb
CHANGED
@@ -1,48 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'socket'
|
4
|
-
|
5
3
|
class TCPClient
|
6
4
|
class NoOpenSSL < RuntimeError
|
7
|
-
def
|
8
|
-
|
5
|
+
def initialize
|
6
|
+
super('OpenSSL is not available')
|
9
7
|
end
|
10
8
|
end
|
11
9
|
|
12
|
-
class NoBlockGiven <
|
13
|
-
def
|
14
|
-
|
10
|
+
class NoBlockGiven < ArgumentError
|
11
|
+
def initialize
|
12
|
+
super('no block given')
|
15
13
|
end
|
16
14
|
end
|
17
15
|
|
18
16
|
class InvalidDeadLine < ArgumentError
|
19
|
-
def
|
20
|
-
|
17
|
+
def initialize(timeout)
|
18
|
+
super("invalid deadline - #{timeout}")
|
21
19
|
end
|
22
20
|
end
|
23
21
|
|
24
22
|
class UnknownAttribute < ArgumentError
|
25
|
-
def
|
26
|
-
|
23
|
+
def initialize(attribute)
|
24
|
+
super("unknown attribute - #{attribute}")
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
30
28
|
class NotAnException < TypeError
|
31
|
-
def
|
32
|
-
|
29
|
+
def initialize(object)
|
30
|
+
super("exception class required - #{object.inspect}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class NotConnected < IOError
|
35
|
+
def initialize
|
36
|
+
super('client not connected')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class TimeoutError < IOError
|
41
|
+
def initialize(message = nil)
|
42
|
+
super(message || "unable to #{action} in time")
|
43
|
+
end
|
44
|
+
|
45
|
+
def action
|
46
|
+
:process
|
33
47
|
end
|
34
48
|
end
|
35
49
|
|
36
|
-
class
|
37
|
-
def
|
38
|
-
|
50
|
+
class ConnectTimeoutError < TimeoutError
|
51
|
+
def action
|
52
|
+
:connect
|
39
53
|
end
|
40
54
|
end
|
41
55
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
56
|
+
class ReadTimeoutError < TimeoutError
|
57
|
+
def action
|
58
|
+
:read
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class WriteTimeoutError < TimeoutError
|
63
|
+
def action
|
64
|
+
:write
|
65
|
+
end
|
66
|
+
end
|
46
67
|
|
47
68
|
Timeout = TimeoutError # backward compatibility
|
48
69
|
deprecate_constant(:Timeout)
|
@@ -1,38 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module IOWithDeadlineMixin
|
2
4
|
def self.included(mod)
|
3
|
-
|
4
|
-
if
|
5
|
+
methods = mod.instance_methods
|
6
|
+
if methods.index(:wait_writable) && methods.index(:wait_readable)
|
5
7
|
mod.include(ViaWaitMethod)
|
6
8
|
else
|
7
9
|
mod.include(ViaSelect)
|
8
10
|
end
|
9
11
|
end
|
10
12
|
|
11
|
-
def read_with_deadline(
|
12
|
-
raise(
|
13
|
+
def read_with_deadline(bytes_to_read, deadline, exception)
|
14
|
+
raise(exception) unless deadline.remaining_time
|
13
15
|
result = ''.b
|
14
|
-
|
15
|
-
loop do
|
16
|
+
while result.bytesize < bytes_to_read
|
16
17
|
read =
|
17
|
-
with_deadline(deadline,
|
18
|
-
read_nonblock(
|
18
|
+
with_deadline(deadline, exception) do
|
19
|
+
read_nonblock(bytes_to_read - result.bytesize, exception: false)
|
19
20
|
end
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
-
result += read
|
25
|
-
return result if result.bytesize >= nbytes
|
21
|
+
next result += read if read
|
22
|
+
close
|
23
|
+
break
|
26
24
|
end
|
25
|
+
result
|
27
26
|
end
|
28
27
|
|
29
|
-
def write_with_deadline(data, deadline,
|
30
|
-
raise(
|
28
|
+
def write_with_deadline(data, deadline, exception)
|
29
|
+
raise(exception) unless deadline.remaining_time
|
31
30
|
return 0 if (size = data.bytesize).zero?
|
32
31
|
result = 0
|
33
32
|
loop do
|
34
33
|
written =
|
35
|
-
with_deadline(deadline,
|
34
|
+
with_deadline(deadline, exception) do
|
36
35
|
write_nonblock(data, exception: false)
|
37
36
|
end
|
38
37
|
result += written
|
@@ -42,40 +41,40 @@ module IOWithDeadlineMixin
|
|
42
41
|
end
|
43
42
|
|
44
43
|
module ViaWaitMethod
|
45
|
-
private def with_deadline(deadline,
|
44
|
+
private def with_deadline(deadline, exception)
|
46
45
|
loop do
|
47
46
|
case ret = yield
|
48
47
|
when :wait_writable
|
49
|
-
|
50
|
-
raise(
|
48
|
+
remaining_time = deadline.remaining_time or raise(exception)
|
49
|
+
raise(exception) if wait_writable(remaining_time).nil?
|
51
50
|
when :wait_readable
|
52
|
-
|
53
|
-
raise(
|
51
|
+
remaining_time = deadline.remaining_time or raise(exception)
|
52
|
+
raise(exception) if wait_readable(remaining_time).nil?
|
54
53
|
else
|
55
54
|
return ret
|
56
55
|
end
|
57
56
|
end
|
58
57
|
rescue Errno::ETIMEDOUT
|
59
|
-
raise(
|
58
|
+
raise(exception)
|
60
59
|
end
|
61
60
|
end
|
62
61
|
|
63
62
|
module ViaSelect
|
64
|
-
private def with_deadline(deadline,
|
63
|
+
private def with_deadline(deadline, exception)
|
65
64
|
loop do
|
66
65
|
case ret = yield
|
67
66
|
when :wait_writable
|
68
|
-
|
69
|
-
raise(
|
67
|
+
remaining_time = deadline.remaining_time or raise(exception)
|
68
|
+
raise(exception) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
70
69
|
when :wait_readable
|
71
|
-
|
72
|
-
raise(
|
70
|
+
remaining_time = deadline.remaining_time or raise(exception)
|
71
|
+
raise(exception) if ::IO.select([self], nil, nil, remaining_time).nil?
|
73
72
|
else
|
74
73
|
return ret
|
75
74
|
end
|
76
75
|
end
|
77
76
|
rescue Errno::ETIMEDOUT
|
78
|
-
raise(
|
77
|
+
raise(exception)
|
79
78
|
end
|
80
79
|
end
|
81
80
|
|
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
begin
|
2
4
|
require 'openssl'
|
3
5
|
rescue LoadError
|
4
6
|
return
|
5
7
|
end
|
6
8
|
|
9
|
+
require_relative 'deadline'
|
7
10
|
require_relative 'mixin/io_with_deadline'
|
8
11
|
|
9
12
|
class TCPClient
|
@@ -13,33 +16,27 @@ class TCPClient
|
|
13
16
|
def initialize(socket, address, configuration, exception)
|
14
17
|
ssl_params = Hash[configuration.ssl_params]
|
15
18
|
super(socket, create_context(ssl_params))
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
)
|
19
|
+
self.sync_close = true
|
20
|
+
self.hostname = address.hostname
|
21
|
+
deadline = Deadline.new(configuration.connect_timeout)
|
22
|
+
deadline.valid? ? connect_with_deadline(deadline, exception) : connect
|
23
|
+
post_connection_check(address.hostname) if should_verify?(ssl_params)
|
22
24
|
end
|
23
25
|
|
24
26
|
private
|
25
27
|
|
26
28
|
def create_context(ssl_params)
|
27
|
-
|
28
|
-
|
29
|
-
|
29
|
+
context = OpenSSL::SSL::SSLContext.new
|
30
|
+
context.set_params(ssl_params)
|
31
|
+
context
|
30
32
|
end
|
31
33
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
with_deadline(Time.now + timeout, exception) do
|
39
|
-
connect_nonblock(exception: false)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
post_connection_check(address.hostname) if check
|
34
|
+
def connect_with_deadline(deadline, exception)
|
35
|
+
with_deadline(deadline, exception) { connect_nonblock(exception: false) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def should_verify?(ssl_params)
|
39
|
+
ssl_params[:verify_mode] != OpenSSL::SSL::VERIFY_NONE
|
43
40
|
end
|
44
41
|
end
|
45
42
|
|
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'socket'
|
4
|
+
require_relative 'deadline'
|
2
5
|
require_relative 'mixin/io_with_deadline'
|
3
6
|
|
4
7
|
class TCPClient
|
@@ -8,20 +11,20 @@ class TCPClient
|
|
8
11
|
def initialize(address, configuration, exception)
|
9
12
|
super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
|
10
13
|
configure(configuration)
|
11
|
-
|
14
|
+
deadline = Deadline.new(configuration.connect_timeout)
|
15
|
+
connect_to(address, deadline, exception)
|
12
16
|
end
|
13
17
|
|
14
18
|
private
|
15
19
|
|
16
|
-
def connect_to(address,
|
20
|
+
def connect_to(address, deadline, exception)
|
17
21
|
addr =
|
18
22
|
::Socket.pack_sockaddr_in(
|
19
23
|
address.addrinfo.ip_port,
|
20
24
|
address.addrinfo.ip_address
|
21
25
|
)
|
22
|
-
|
23
|
-
|
24
|
-
with_deadline(Time.now + timeout, exception) do
|
26
|
+
return connect(addr) unless deadline.valid?
|
27
|
+
with_deadline(deadline, exception) do
|
25
28
|
connect_nonblock(addr, exception: false)
|
26
29
|
end
|
27
30
|
end
|
data/lib/tcp-client/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -11,7 +11,6 @@ CLOBBER << 'prj'
|
|
11
11
|
task(:default) { exec('rake --tasks') }
|
12
12
|
|
13
13
|
Rake::TestTask.new(:test) do |task|
|
14
|
-
task.
|
15
|
-
task.
|
16
|
-
task.verbose = true
|
14
|
+
task.pattern = 'test/**/*_test.rb'
|
15
|
+
task.warning = task.verbose = true
|
17
16
|
end
|
data/sample/google.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../lib/tcp-client'
|
2
4
|
|
3
5
|
TCPClient.configure(
|
@@ -6,15 +8,14 @@ TCPClient.configure(
|
|
6
8
|
read_timeout: 0.5 # seconds to read some bytes
|
7
9
|
)
|
8
10
|
|
9
|
-
# the following
|
10
|
-
# to last longer than 1.25 seconds:
|
11
|
+
# the following sequence is not allowed to last longer than 1.25 seconds:
|
11
12
|
# 0.5 seconds to connect
|
12
13
|
# + 0.25 seconds to write data
|
13
14
|
# + 0.5 seconds to read a response
|
14
15
|
|
15
16
|
TCPClient.open('www.google.com:80') do |client|
|
16
17
|
# simple HTTP get request
|
17
|
-
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
18
|
+
pp client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
|
18
19
|
|
19
20
|
# read "HTTP/1.1 " + 3 byte HTTP status code
|
20
21
|
pp client.read(12)
|
data/sample/google_ssl.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../lib/tcp-client'
|
2
4
|
|
3
5
|
TCPClient.configure do |cfg|
|
4
|
-
cfg.connect_timeout = 1 #
|
6
|
+
cfg.connect_timeout = 1 # limit connect time the server to 1 second
|
5
7
|
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
6
8
|
end
|
7
9
|
|
8
10
|
TCPClient.open('www.google.com:443') do |client|
|
9
|
-
#
|
11
|
+
# next sequence should not last longer than 0.5 seconds
|
10
12
|
client.with_deadline(0.5) do
|
11
13
|
# simple HTTP get request
|
12
|
-
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
14
|
+
pp client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
|
13
15
|
|
14
16
|
# read "HTTP/1.1 " + 3 byte HTTP status code
|
15
17
|
pp client.read(12)
|
data/tcp-client.gemspec
CHANGED
@@ -11,11 +11,13 @@ GemSpec = Gem::Specification.new do |spec|
|
|
11
11
|
|
12
12
|
spec.summary = 'A TCP client implementation with working timeout support.'
|
13
13
|
spec.description = <<~DESCRIPTION
|
14
|
-
This
|
15
|
-
|
16
|
-
|
17
|
-
other implementations this client respects
|
18
|
-
limits for each method
|
14
|
+
This Gem implements a TCP client with (optional) SSL support.
|
15
|
+
It is an easy to use, versatile configurable client that can correctly
|
16
|
+
handle time limits.
|
17
|
+
Unlike other implementations, this client respects
|
18
|
+
predefined/configurable time limits for each method
|
19
|
+
(`connect`, `read`, `write`). Deadlines for a sequence of read/write
|
20
|
+
actions can also be monitored.
|
19
21
|
DESCRIPTION
|
20
22
|
spec.homepage = 'https://github.com/mblumtritt/tcp-client'
|
21
23
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../test_helper'
|
4
|
+
|
5
|
+
class Deadlineest < MiniTest::Test
|
6
|
+
parallelize_me!
|
7
|
+
|
8
|
+
def test_validity
|
9
|
+
assert(TCPClient::Deadline.new(1).valid?)
|
10
|
+
assert(TCPClient::Deadline.new(0.0001).valid?)
|
11
|
+
|
12
|
+
refute(TCPClient::Deadline.new(0).valid?)
|
13
|
+
refute(TCPClient::Deadline.new(nil).valid?)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_remaining_time
|
17
|
+
assert(TCPClient::Deadline.new(1).remaining_time > 0)
|
18
|
+
|
19
|
+
assert_nil(TCPClient::Deadline.new(0).remaining_time)
|
20
|
+
assert_nil(TCPClient::Deadline.new(nil).remaining_time)
|
21
|
+
|
22
|
+
deadline = TCPClient::Deadline.new(0.2)
|
23
|
+
sleep(0.2)
|
24
|
+
assert_nil(deadline.remaining_time)
|
25
|
+
end
|
26
|
+
end
|
data/test/tcp_client_test.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'test_helper'
|
2
4
|
|
3
5
|
class TCPClientTest < MiniTest::Test
|
@@ -11,6 +13,10 @@ class TCPClientTest < MiniTest::Test
|
|
11
13
|
@config = TCPClient::Configuration.create(buffered: false)
|
12
14
|
end
|
13
15
|
|
16
|
+
def port
|
17
|
+
PseudoServer.local_address.ip_port
|
18
|
+
end
|
19
|
+
|
14
20
|
def test_defaults
|
15
21
|
subject = TCPClient.new
|
16
22
|
assert(subject.closed?)
|
@@ -43,15 +49,15 @@ class TCPClientTest < MiniTest::Test
|
|
43
49
|
end
|
44
50
|
|
45
51
|
def test_connected_state
|
46
|
-
TCPClient.open(
|
52
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
47
53
|
refute(subject.closed?)
|
48
|
-
assert_equal(
|
54
|
+
assert_equal("localhost:#{port}", subject.to_s)
|
49
55
|
refute_nil(subject.address)
|
50
56
|
address_when_opened = subject.address
|
51
|
-
assert_equal(
|
57
|
+
assert_equal("localhost:#{port}", subject.address.to_s)
|
52
58
|
assert_equal('localhost', subject.address.hostname)
|
53
59
|
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
54
|
-
assert_same(
|
60
|
+
assert_same(port, subject.address.addrinfo.ip_port)
|
55
61
|
|
56
62
|
subject.close
|
57
63
|
assert(subject.closed?)
|
@@ -60,7 +66,7 @@ class TCPClientTest < MiniTest::Test
|
|
60
66
|
end
|
61
67
|
|
62
68
|
def check_read_timeout(timeout)
|
63
|
-
TCPClient.open(
|
69
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
64
70
|
refute(subject.closed?)
|
65
71
|
start_time = nil
|
66
72
|
assert_raises(TCPClient::ReadTimeoutError) do
|
@@ -78,7 +84,7 @@ class TCPClientTest < MiniTest::Test
|
|
78
84
|
end
|
79
85
|
|
80
86
|
def check_write_timeout(timeout)
|
81
|
-
TCPClient.open(
|
87
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
82
88
|
refute(subject.closed?)
|
83
89
|
start_time = nil
|
84
90
|
assert_raises(TCPClient::WriteTimeoutError) do
|
@@ -95,7 +101,7 @@ class TCPClientTest < MiniTest::Test
|
|
95
101
|
end
|
96
102
|
|
97
103
|
def test_write_deadline
|
98
|
-
TCPClient.open(
|
104
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
99
105
|
refute(subject.closed?)
|
100
106
|
assert_raises(TCPClient::WriteTimeoutError) do
|
101
107
|
subject.with_deadline(0.25) do |*args|
|
@@ -107,7 +113,7 @@ class TCPClientTest < MiniTest::Test
|
|
107
113
|
end
|
108
114
|
|
109
115
|
def test_read_deadline
|
110
|
-
TCPClient.open(
|
116
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
111
117
|
refute(subject.closed?)
|
112
118
|
assert_raises(TCPClient::ReadTimeoutError) do
|
113
119
|
subject.with_deadline(0.25) do |*args|
|
@@ -118,8 +124,8 @@ class TCPClientTest < MiniTest::Test
|
|
118
124
|
end
|
119
125
|
end
|
120
126
|
|
121
|
-
def
|
122
|
-
TCPClient.open(
|
127
|
+
def test_read_write_deadline
|
128
|
+
TCPClient.open("localhost:#{port}", config) do |subject|
|
123
129
|
refute(subject.closed?)
|
124
130
|
assert_raises(TCPClient::TimeoutError) do
|
125
131
|
subject.with_deadline(0.25) do |*args|
|
@@ -137,9 +143,9 @@ class TCPClientTest < MiniTest::Test
|
|
137
143
|
start_time = nil
|
138
144
|
assert_raises(TCPClient::ConnectTimeoutError) do
|
139
145
|
start_time = Time.now
|
140
|
-
TCPClient.new.connect(
|
146
|
+
TCPClient.new.connect("localhost:#{port}", ssl_config)
|
141
147
|
end
|
142
|
-
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.
|
148
|
+
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.25)
|
143
149
|
end
|
144
150
|
|
145
151
|
def test_connect_ssl_timeout
|
data/test/test_helper.rb
CHANGED
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.4.1
|
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-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -53,11 +53,13 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
description: |
|
56
|
-
This
|
57
|
-
|
58
|
-
|
59
|
-
other implementations this client respects
|
60
|
-
limits for each method
|
56
|
+
This Gem implements a TCP client with (optional) SSL support.
|
57
|
+
It is an easy to use, versatile configurable client that can correctly
|
58
|
+
handle time limits.
|
59
|
+
Unlike other implementations, this client respects
|
60
|
+
predefined/configurable time limits for each method
|
61
|
+
(`connect`, `read`, `write`). Deadlines for a sequence of read/write
|
62
|
+
actions can also be monitored.
|
61
63
|
email:
|
62
64
|
executables: []
|
63
65
|
extensions: []
|
@@ -65,14 +67,15 @@ extra_rdoc_files:
|
|
65
67
|
- README.md
|
66
68
|
files:
|
67
69
|
- ".gitignore"
|
70
|
+
- LICENSE
|
68
71
|
- README.md
|
69
72
|
- gems.rb
|
70
73
|
- lib/tcp-client.rb
|
71
74
|
- lib/tcp-client/address.rb
|
72
75
|
- lib/tcp-client/configuration.rb
|
76
|
+
- lib/tcp-client/deadline.rb
|
73
77
|
- lib/tcp-client/default_configuration.rb
|
74
78
|
- lib/tcp-client/errors.rb
|
75
|
-
- lib/tcp-client/mixin/io_timeout.rb
|
76
79
|
- lib/tcp-client/mixin/io_with_deadline.rb
|
77
80
|
- lib/tcp-client/ssl_socket.rb
|
78
81
|
- lib/tcp-client/tcp_socket.rb
|
@@ -84,6 +87,7 @@ files:
|
|
84
87
|
- tcp-client.gemspec
|
85
88
|
- test/tcp-client/address_test.rb
|
86
89
|
- test/tcp-client/configuration_test.rb
|
90
|
+
- test/tcp-client/deadline_test.rb
|
87
91
|
- test/tcp-client/default_configuration_test.rb
|
88
92
|
- test/tcp-client/version_test.rb
|
89
93
|
- test/tcp_client_test.rb
|
@@ -108,13 +112,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
112
|
- !ruby/object:Gem::Version
|
109
113
|
version: '0'
|
110
114
|
requirements: []
|
111
|
-
rubygems_version: 3.2.
|
115
|
+
rubygems_version: 3.2.22
|
112
116
|
signing_key:
|
113
117
|
specification_version: 4
|
114
118
|
summary: A TCP client implementation with working timeout support.
|
115
119
|
test_files:
|
116
120
|
- test/tcp-client/address_test.rb
|
117
121
|
- test/tcp-client/configuration_test.rb
|
122
|
+
- test/tcp-client/deadline_test.rb
|
118
123
|
- test/tcp-client/default_configuration_test.rb
|
119
124
|
- test/tcp-client/version_test.rb
|
120
125
|
- test/tcp_client_test.rb
|
@@ -1,118 +0,0 @@
|
|
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
|
-
|
5
|
-
IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
|
6
|
-
|
7
|
-
module IOTimeoutMixin
|
8
|
-
def self.included(mod)
|
9
|
-
im = mod.instance_methods
|
10
|
-
if im.index(:wait_writable) && im.index(:wait_readable)
|
11
|
-
mod.include(DeadlineMethods)
|
12
|
-
else
|
13
|
-
mod.include(DeadlineIO)
|
14
|
-
end
|
15
|
-
end
|
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
|
-
|
35
|
-
def read(nbytes, timeout: nil, exception: IOTimeoutError)
|
36
|
-
timeout = timeout.to_f
|
37
|
-
return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
|
38
|
-
deadline = Time.now + timeout
|
39
|
-
read_all(nbytes) do |junk_size|
|
40
|
-
with_deadline(deadline, exception) do
|
41
|
-
read_nonblock(junk_size, exception: false)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def write(*msgs, timeout: nil, exception: IOTimeoutError)
|
47
|
-
timeout = timeout.to_f
|
48
|
-
return write_all(msgs.join.b) { |junk| super(junk) } if timeout <= 0
|
49
|
-
deadline = Time.now + timeout
|
50
|
-
write_all(msgs.join.b) do |junk|
|
51
|
-
with_deadline(deadline, exception) do
|
52
|
-
write_nonblock(junk, exception: false)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
58
|
-
|
59
|
-
def read_all(nbytes)
|
60
|
-
return ''.b if nbytes.zero?
|
61
|
-
result = ''.b
|
62
|
-
loop do
|
63
|
-
unless read = yield(nbytes - result.bytesize)
|
64
|
-
close
|
65
|
-
return result
|
66
|
-
end
|
67
|
-
result += read
|
68
|
-
return result if result.bytesize >= nbytes
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def write_all(data)
|
73
|
-
return 0 if (size = data.bytesize).zero?
|
74
|
-
result = 0
|
75
|
-
loop do
|
76
|
-
written = yield(data)
|
77
|
-
result += written
|
78
|
-
return result if result >= size
|
79
|
-
data = data.byteslice(written, data.bytesize - written)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
module DeadlineMethods
|
84
|
-
private def with_deadline(deadline, exclass)
|
85
|
-
loop do
|
86
|
-
case ret = yield
|
87
|
-
when :wait_writable
|
88
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
89
|
-
raise(exclass) if wait_writable(remaining_time).nil?
|
90
|
-
when :wait_readable
|
91
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
92
|
-
raise(exclass) if wait_readable(remaining_time).nil?
|
93
|
-
else
|
94
|
-
return ret
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
module DeadlineIO
|
101
|
-
private def with_deadline(deadline, exclass)
|
102
|
-
loop do
|
103
|
-
case ret = yield
|
104
|
-
when :wait_writable
|
105
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
106
|
-
raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
107
|
-
when :wait_readable
|
108
|
-
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
109
|
-
raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
|
110
|
-
else
|
111
|
-
return ret
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
private_constant(:DeadlineMethods, :DeadlineIO)
|
118
|
-
end
|