tcp-client 0.0.10 → 0.1.4
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 +13 -11
- data/gems.rb +1 -1
- data/lib/tcp-client.rb +43 -13
- data/lib/tcp-client/address.rb +21 -11
- data/lib/tcp-client/configuration.rb +43 -14
- data/lib/tcp-client/default_configuration.rb +21 -0
- data/lib/tcp-client/mixin/io_timeout.rb +5 -9
- data/lib/tcp-client/tcp_socket.rb +5 -3
- data/lib/tcp-client/version.rb +1 -1
- data/rakefile.rb +3 -5
- data/sample/google.rb +15 -10
- data/sample/google_ssl.rb +11 -11
- data/tcp-client.gemspec +9 -15
- data/test/tcp-client/address_test.rb +11 -1
- data/test/tcp-client/configuration_test.rb +52 -11
- data/test/tcp-client/default_configuration_test.rb +59 -0
- data/test/tcp-client/version_test.rb +3 -1
- data/test/tcp_client_test.rb +104 -62
- data/test/test_helper.rb +5 -3
- metadata +12 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a122629b36ac22b95c2ee7ed0d520391cfc00136982e9c0c4a8c7d0ea4dd5b47
|
4
|
+
data.tar.gz: a63612520461f6d84abffbe71c750158b2d7d0c5227d4ae9de16b9cac5071540
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31d0760aa3f16564d171ba27dc0c3a919490356319265aa099762ef7f1e9ff57a20e968315d0d70beb291d62405040e01ec76aa4dfd78a229f3d91c0a7db029b
|
7
|
+
data.tar.gz: 265a8cda03d5c8f62138311f05a40756dd7e052c8b55d6e43412b3cf51dcea91e98c6ba38943bedf4b41de7af6b0d3dc86700197f7eafd4919093abd59d15351
|
data/README.md
CHANGED
@@ -8,20 +8,22 @@ This gem implements a TCP client with (optional) SSL support. The motivation of
|
|
8
8
|
## Sample
|
9
9
|
|
10
10
|
```ruby
|
11
|
-
|
11
|
+
require 'tcp-client'
|
12
|
+
|
13
|
+
TCPClient.configure do |cfg|
|
12
14
|
cfg.connect_timeout = 1 # second to connect the server
|
13
|
-
cfg.
|
14
|
-
cfg.read_timeout = 0.5 # seconds to read some bytes
|
15
|
-
cfg.ssl_params = {ssl_version: :TLSv1_2} # use TLS 1.2
|
15
|
+
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
#
|
20
|
-
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
TCPClient.open('www.google.com:443') do |client|
|
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
|
25
27
|
end
|
26
28
|
```
|
27
29
|
|
data/gems.rb
CHANGED
data/lib/tcp-client.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative 'tcp-client/address'
|
|
4
4
|
require_relative 'tcp-client/tcp_socket'
|
5
5
|
require_relative 'tcp-client/ssl_socket'
|
6
6
|
require_relative 'tcp-client/configuration'
|
7
|
+
require_relative 'tcp-client/default_configuration'
|
7
8
|
require_relative 'tcp-client/version'
|
8
9
|
|
9
10
|
class TCPClient
|
@@ -14,14 +15,20 @@ class TCPClient
|
|
14
15
|
end
|
15
16
|
|
16
17
|
class NotConnected < SocketError
|
17
|
-
def self.raise!(
|
18
|
-
raise(self, "client not connected - #{
|
18
|
+
def self.raise!(reason)
|
19
|
+
raise(self, "client not connected - #{reason}", caller(1))
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
22
|
-
|
23
|
+
TimeoutError = Class.new(IOError)
|
24
|
+
ConnectTimeoutError = Class.new(TimeoutError)
|
25
|
+
ReadTimeoutError = Class.new(TimeoutError)
|
26
|
+
WriteTimeoutError = Class.new(TimeoutError)
|
23
27
|
|
24
|
-
|
28
|
+
Timeout = TimeoutError # backward compatibility
|
29
|
+
deprecate_constant(:Timeout)
|
30
|
+
|
31
|
+
def self.open(addr, configuration = Configuration.default)
|
25
32
|
addr = Address.new(addr)
|
26
33
|
client = new
|
27
34
|
client.connect(addr, configuration)
|
@@ -34,20 +41,20 @@ class TCPClient
|
|
34
41
|
|
35
42
|
def initialize
|
36
43
|
@socket = @address = @write_timeout = @read_timeout = nil
|
44
|
+
@deadline = nil
|
37
45
|
end
|
38
46
|
|
39
47
|
def to_s
|
40
48
|
@address ? @address.to_s : ''
|
41
49
|
end
|
42
50
|
|
43
|
-
def connect(addr, configuration)
|
51
|
+
def connect(addr, configuration, exception: ConnectTimeoutError)
|
44
52
|
close
|
45
53
|
NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
|
46
54
|
@address = Address.new(addr)
|
47
|
-
@socket = TCPSocket.new(@address, configuration,
|
48
|
-
configuration.ssl? &&
|
49
|
-
@socket, @address, configuration,
|
50
|
-
)
|
55
|
+
@socket = TCPSocket.new(@address, configuration, exception)
|
56
|
+
configuration.ssl? &&
|
57
|
+
@socket = SSLSocket.new(@socket, @address, configuration, exception)
|
51
58
|
@write_timeout = configuration.write_timeout
|
52
59
|
@read_timeout = configuration.read_timeout
|
53
60
|
self
|
@@ -59,24 +66,47 @@ class TCPClient
|
|
59
66
|
self
|
60
67
|
rescue IOError
|
61
68
|
self
|
69
|
+
ensure
|
70
|
+
@deadline = nil
|
62
71
|
end
|
63
72
|
|
64
73
|
def closed?
|
65
74
|
@socket.nil? || @socket.closed?
|
66
75
|
end
|
67
76
|
|
68
|
-
def
|
77
|
+
def with_deadline(timeout)
|
78
|
+
raise('no block given') unless block_given?
|
79
|
+
raise('deadline already used') if @deadline
|
80
|
+
tm = timeout&.to_f
|
81
|
+
raise(ArgumentError, "invalid deadline - #{timeout}") unless tm.positive?
|
82
|
+
@deadline = Time.now + tm
|
83
|
+
yield(self)
|
84
|
+
ensure
|
85
|
+
@deadline = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def read(nbytes, timeout: nil, exception: ReadTimeoutError)
|
69
89
|
NotConnected.raise!(self) if closed?
|
70
|
-
|
90
|
+
time = timeout || remaining_time(exception) || @read_timeout
|
91
|
+
@socket.read(nbytes, timeout: time, exception: exception)
|
71
92
|
end
|
72
93
|
|
73
|
-
def write(*msg, timeout:
|
94
|
+
def write(*msg, timeout: nil, exception: WriteTimeoutError)
|
74
95
|
NotConnected.raise!(self) if closed?
|
75
|
-
|
96
|
+
time = timeout || remaining_time(exception) || @write_timeout
|
97
|
+
@socket.write(*msg, timeout: time, exception: exception)
|
76
98
|
end
|
77
99
|
|
78
100
|
def flush
|
79
101
|
@socket.flush unless closed?
|
80
102
|
self
|
81
103
|
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def remaining_time(exception)
|
108
|
+
return unless @deadline
|
109
|
+
remaining_time = @deadline - Time.now
|
110
|
+
0 < remaining_time ? remaining_time : raise(exception)
|
111
|
+
end
|
82
112
|
end
|
data/lib/tcp-client/address.rb
CHANGED
@@ -4,7 +4,7 @@ require 'socket'
|
|
4
4
|
|
5
5
|
class TCPClient
|
6
6
|
class Address
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :hostname, :addrinfo
|
8
8
|
|
9
9
|
def initialize(addr)
|
10
10
|
case addr
|
@@ -20,17 +20,33 @@ class TCPClient
|
|
20
20
|
@addrinfo.freeze
|
21
21
|
end
|
22
22
|
|
23
|
+
def to_s
|
24
|
+
return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
|
25
|
+
"#{@hostname}:#{@addrinfo.ip_port}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
{ host: @hostname, port: @addrinfo.ip_port }
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
to_h == other.to_h
|
34
|
+
end
|
35
|
+
alias eql? ==
|
36
|
+
|
37
|
+
def equal?(other)
|
38
|
+
self.class == other.class && self == other
|
39
|
+
end
|
40
|
+
|
23
41
|
private
|
24
42
|
|
25
43
|
def init_from_selfclass(address)
|
26
|
-
@to_s = address.to_s
|
27
44
|
@hostname = address.hostname
|
28
45
|
@addrinfo = address.addrinfo
|
29
46
|
end
|
30
47
|
|
31
48
|
def init_from_addrinfo(addrinfo)
|
32
|
-
@hostname,
|
33
|
-
@to_s = "#{@hostname}:#{port}"
|
49
|
+
@hostname, _port = addrinfo.getnameinfo(Socket::NI_NUMERICSERV)
|
34
50
|
@addrinfo = addrinfo
|
35
51
|
end
|
36
52
|
|
@@ -38,19 +54,13 @@ class TCPClient
|
|
38
54
|
@hostname, port = from_string(str.to_s)
|
39
55
|
return init_from_addrinfo(Addrinfo.tcp(nil, port)) unless @hostname
|
40
56
|
@addrinfo = Addrinfo.tcp(@hostname, port)
|
41
|
-
@to_s = as_str(@hostname, port)
|
42
57
|
end
|
43
58
|
|
44
59
|
def from_string(str)
|
45
|
-
|
60
|
+
idx = str.rindex(':') or return nil, str.to_i
|
46
61
|
name = str[0, idx]
|
47
62
|
name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
|
48
63
|
[name, str[idx + 1, str.size - idx].to_i]
|
49
64
|
end
|
50
|
-
|
51
|
-
def as_str(hostname, port)
|
52
|
-
return "[#{hostname}]:#{port}" if hostname.index(':') # IP6
|
53
|
-
"#{hostname}:#{port}"
|
54
|
-
end
|
55
65
|
end
|
56
66
|
end
|
@@ -1,39 +1,40 @@
|
|
1
1
|
class TCPClient
|
2
2
|
class Configuration
|
3
|
-
def self.create
|
4
|
-
ret = new
|
3
|
+
def self.create(options = {})
|
4
|
+
ret = new(options)
|
5
5
|
yield(ret) if block_given?
|
6
6
|
ret
|
7
7
|
end
|
8
8
|
|
9
|
-
attr_reader :buffered, :keep_alive, :reverse_lookup
|
9
|
+
attr_reader :buffered, :keep_alive, :reverse_lookup, :timeout
|
10
10
|
attr_accessor :ssl_params
|
11
11
|
|
12
|
-
def initialize
|
12
|
+
def initialize(options = {})
|
13
13
|
@buffered = @keep_alive = @reverse_lookup = true
|
14
14
|
self.timeout = @ssl_params = nil
|
15
|
+
options.each_pair { |attribute, value| set(attribute, value) }
|
15
16
|
end
|
16
17
|
|
17
18
|
def ssl?
|
18
19
|
@ssl_params ? true : false
|
19
20
|
end
|
20
21
|
|
21
|
-
def ssl=(
|
22
|
-
return @ssl_params = nil unless
|
23
|
-
return @ssl_params =
|
22
|
+
def ssl=(value)
|
23
|
+
return @ssl_params = nil unless value
|
24
|
+
return @ssl_params = value.dup if Hash === value
|
24
25
|
@ssl_params ||= {}
|
25
26
|
end
|
26
27
|
|
27
|
-
def buffered=(
|
28
|
-
@buffered =
|
28
|
+
def buffered=(value)
|
29
|
+
@buffered = value ? true : false
|
29
30
|
end
|
30
31
|
|
31
|
-
def keep_alive=(
|
32
|
-
@keep_alive =
|
32
|
+
def keep_alive=(value)
|
33
|
+
@keep_alive = value ? true : false
|
33
34
|
end
|
34
35
|
|
35
|
-
def reverse_lookup=(
|
36
|
-
@reverse_lookup =
|
36
|
+
def reverse_lookup=(value)
|
37
|
+
@reverse_lookup = value ? true : false
|
37
38
|
end
|
38
39
|
|
39
40
|
def timeout=(seconds)
|
@@ -65,10 +66,38 @@ class TCPClient
|
|
65
66
|
@read_timeout = seconds(seconds)
|
66
67
|
end
|
67
68
|
|
69
|
+
def to_h
|
70
|
+
{
|
71
|
+
buffered: @buffered,
|
72
|
+
keep_alive: @keep_alive,
|
73
|
+
reverse_lookup: @reverse_lookup,
|
74
|
+
timeout: @timeout,
|
75
|
+
connect_timeout: @connect_timeout,
|
76
|
+
read_timeout: @read_timeout,
|
77
|
+
write_timeout: @write_timeout,
|
78
|
+
ssl_params: @ssl_params
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def ==(other)
|
83
|
+
to_h == other.to_h
|
84
|
+
end
|
85
|
+
alias eql? ==
|
86
|
+
|
87
|
+
def equal?(other)
|
88
|
+
self.class == other.class && self == other
|
89
|
+
end
|
90
|
+
|
68
91
|
private
|
69
92
|
|
93
|
+
def set(attribute, value)
|
94
|
+
public_send("#{attribute}=", value)
|
95
|
+
rescue NoMethodError
|
96
|
+
raise(ArgumentError, "unknown attribute - #{attribute}")
|
97
|
+
end
|
98
|
+
|
70
99
|
def seconds(value)
|
71
|
-
value&.positive? ? value : nil
|
100
|
+
value&.to_f&.positive? ? value : nil
|
72
101
|
end
|
73
102
|
end
|
74
103
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'configuration'
|
2
|
+
|
3
|
+
class TCPClient
|
4
|
+
@default_configuration = Configuration.new
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :default_configuration
|
8
|
+
|
9
|
+
def configure(options = {})
|
10
|
+
cfg = Configuration.new(options)
|
11
|
+
yield(cfg) if block_given?
|
12
|
+
@default_configuration = cfg
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Configuration
|
17
|
+
def self.default
|
18
|
+
TCPClient.default_configuration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -12,7 +12,7 @@ module IOTimeoutMixin
|
|
12
12
|
|
13
13
|
def read(nbytes, timeout: nil, exception: IOTimeoutError)
|
14
14
|
timeout = timeout.to_f
|
15
|
-
return read_all(nbytes){ |junk_size| super(junk_size) } if timeout <= 0
|
15
|
+
return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
|
16
16
|
deadline = Time.now + timeout
|
17
17
|
read_all(nbytes) do |junk_size|
|
18
18
|
with_deadline(deadline, exception) do
|
@@ -23,9 +23,9 @@ module IOTimeoutMixin
|
|
23
23
|
|
24
24
|
def write(*msgs, timeout: nil, exception: IOTimeoutError)
|
25
25
|
timeout = timeout.to_f
|
26
|
-
return write_all(msgs.join){ |junk| super(junk) } if timeout <= 0
|
26
|
+
return write_all(msgs.join.b) { |junk| super(junk) } if timeout <= 0
|
27
27
|
deadline = Time.now + timeout
|
28
|
-
write_all(msgs.join) do |junk|
|
28
|
+
write_all(msgs.join.b) do |junk|
|
29
29
|
with_deadline(deadline, exception) do
|
30
30
|
write_nonblock(junk, exception: false)
|
31
31
|
end
|
@@ -59,9 +59,7 @@ module IOTimeoutMixin
|
|
59
59
|
end
|
60
60
|
|
61
61
|
module DeadlineMethods
|
62
|
-
private
|
63
|
-
|
64
|
-
def with_deadline(deadline, exclass)
|
62
|
+
private def with_deadline(deadline, exclass)
|
65
63
|
loop do
|
66
64
|
case ret = yield
|
67
65
|
when :wait_writable
|
@@ -78,9 +76,7 @@ module IOTimeoutMixin
|
|
78
76
|
end
|
79
77
|
|
80
78
|
module DeadlineIO
|
81
|
-
private
|
82
|
-
|
83
|
-
def with_deadline(deadline, exclass)
|
79
|
+
private def with_deadline(deadline, exclass)
|
84
80
|
loop do
|
85
81
|
case ret = yield
|
86
82
|
when :wait_writable
|
@@ -14,9 +14,11 @@ class TCPClient
|
|
14
14
|
private
|
15
15
|
|
16
16
|
def connect_to(address, timeout, exception)
|
17
|
-
addr =
|
18
|
-
|
19
|
-
|
17
|
+
addr =
|
18
|
+
::Socket.pack_sockaddr_in(
|
19
|
+
address.addrinfo.ip_port,
|
20
|
+
address.addrinfo.ip_address
|
21
|
+
)
|
20
22
|
return connect(addr) unless timeout
|
21
23
|
with_deadline(Time.now + timeout, exception) do
|
22
24
|
connect_nonblock(addr, exception: false)
|
data/lib/tcp-client/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -4,7 +4,9 @@ require 'rake/clean'
|
|
4
4
|
require 'rake/testtask'
|
5
5
|
require 'bundler/gem_tasks'
|
6
6
|
|
7
|
-
|
7
|
+
$stdout.sync = $stderr.sync = true
|
8
|
+
|
9
|
+
task(:default) { exec('rake --tasks') }
|
8
10
|
|
9
11
|
CLOBBER << 'prj'
|
10
12
|
|
@@ -13,7 +15,3 @@ Rake::TestTask.new(:test) do |t|
|
|
13
15
|
t.verbose = true
|
14
16
|
t.test_files = FileList['test/**/*_test.rb']
|
15
17
|
end
|
16
|
-
|
17
|
-
task :default do
|
18
|
-
exec("#{$PROGRAM_NAME} --tasks")
|
19
|
-
end
|
data/sample/google.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
require_relative '../lib/tcp-client'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
TCPClient.configure(
|
4
|
+
connect_timeout: 0.5, # seconds to connect the server
|
5
|
+
write_timeout: 0.25, # seconds to write a single data junk
|
6
|
+
read_timeout: 0.5 # seconds to read some bytes
|
7
|
+
)
|
8
8
|
|
9
|
-
# the following request sequence is not allowed
|
10
|
-
#
|
9
|
+
# the following request sequence is not allowed
|
10
|
+
# to last longer than 1.25 seconds:
|
11
|
+
# 0.5 seconds to connect
|
11
12
|
# + 0.25 seconds to write data
|
12
13
|
# + 0.5 seconds to read a response
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
|
15
|
+
TCPClient.open('www.google.com:80') do |client|
|
16
|
+
# simple HTTP get request
|
17
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
18
|
+
|
19
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
20
|
+
pp client.read(12)
|
16
21
|
end
|
data/sample/google_ssl.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
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.
|
6
|
-
cfg.read_timeout = 0.5 # seconds to read some bytes
|
7
|
-
cfg.ssl_params = {ssl_version: :TLSv1_2} # use TLS 1.2
|
5
|
+
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
8
6
|
end
|
9
7
|
|
10
|
-
|
11
|
-
#
|
12
|
-
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
TCPClient.open('www.google.com:443') do |client|
|
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")
|
13
|
+
|
14
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
15
|
+
pp client.read(12)
|
16
|
+
end
|
17
17
|
end
|
data/tcp-client.gemspec
CHANGED
@@ -5,6 +5,10 @@ require_relative './lib/tcp-client/version'
|
|
5
5
|
GemSpec = Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'tcp-client'
|
7
7
|
spec.version = TCPClient::VERSION
|
8
|
+
spec.author = 'Mike Blumtritt'
|
9
|
+
|
10
|
+
spec.required_ruby_version = '>= 2.7.0'
|
11
|
+
|
8
12
|
spec.summary = 'A TCP client implementation with working timeout support.'
|
9
13
|
spec.description = <<~DESCRIPTION
|
10
14
|
This gem implements a TCP client with (optional) SSL support. The
|
@@ -13,27 +17,17 @@ GemSpec = Gem::Specification.new do |spec|
|
|
13
17
|
other implementations this client respects given/configurable time
|
14
18
|
limits for each method (`connect`, `read`, `write`).
|
15
19
|
DESCRIPTION
|
16
|
-
spec.author = 'Mike Blumtritt'
|
17
|
-
spec.email = 'mike.blumtritt@pm.me'
|
18
20
|
spec.homepage = 'https://github.com/mblumtritt/tcp-client'
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
}
|
23
|
-
spec.rubyforge_project = spec.name
|
21
|
+
|
22
|
+
spec.metadata['source_code_uri'] = 'https://github.com/mblumtritt/tcp-client'
|
23
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/mblumtritt/tcp-client/issues'
|
24
24
|
|
25
25
|
spec.add_development_dependency 'bundler'
|
26
26
|
spec.add_development_dependency 'minitest'
|
27
27
|
spec.add_development_dependency 'rake'
|
28
28
|
|
29
|
-
|
30
|
-
spec.
|
31
|
-
spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
|
32
|
-
|
33
|
-
spec.require_paths = %w[lib]
|
34
|
-
|
35
|
-
all_files = %x(git ls-files -z).split(0.chr)
|
36
|
-
spec.test_files = all_files.grep(%r{^(spec|test)/})
|
29
|
+
all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
|
30
|
+
spec.test_files = all_files.grep(%r{^test/})
|
37
31
|
spec.files = all_files - spec.test_files
|
38
32
|
|
39
33
|
spec.extra_rdoc_files = %w[README.md]
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require_relative '../test_helper'
|
2
2
|
|
3
|
-
class AddressTest < Test
|
3
|
+
class AddressTest < MiniTest::Test
|
4
|
+
parallelize_me!
|
5
|
+
|
4
6
|
def test_create_from_integer
|
5
7
|
subject = TCPClient::Address.new(42)
|
6
8
|
assert_equal('localhost:42', subject.to_s)
|
@@ -52,4 +54,12 @@ class AddressTest < Test
|
|
52
54
|
assert(subject.addrinfo.ip?)
|
53
55
|
assert(subject.addrinfo.ipv6?)
|
54
56
|
end
|
57
|
+
|
58
|
+
def test_compare
|
59
|
+
a = TCPClient::Address.new('localhost:42')
|
60
|
+
b = TCPClient::Address.new('localhost:42')
|
61
|
+
assert_equal(a, b)
|
62
|
+
assert(a == b)
|
63
|
+
assert(a === b)
|
64
|
+
end
|
55
65
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require_relative '../test_helper'
|
2
2
|
|
3
|
-
class ConfigurationTest < Test
|
3
|
+
class ConfigurationTest < MiniTest::Test
|
4
|
+
parallelize_me!
|
5
|
+
|
4
6
|
def test_defaults
|
5
7
|
subject = TCPClient::Configuration.new
|
6
8
|
assert(subject.buffered)
|
@@ -13,11 +15,12 @@ class ConfigurationTest < Test
|
|
13
15
|
end
|
14
16
|
|
15
17
|
def test_configure
|
16
|
-
subject =
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
subject =
|
19
|
+
TCPClient::Configuration.create do |cfg|
|
20
|
+
cfg.buffered = cfg.keep_alive = cfg.reverse_lookup = false
|
21
|
+
cfg.timeout = 42
|
22
|
+
cfg.ssl = true
|
23
|
+
end
|
21
24
|
refute(subject.buffered)
|
22
25
|
refute(subject.keep_alive)
|
23
26
|
refute(subject.reverse_lookup)
|
@@ -27,6 +30,35 @@ class ConfigurationTest < Test
|
|
27
30
|
assert(subject.ssl?)
|
28
31
|
end
|
29
32
|
|
33
|
+
def test_options
|
34
|
+
subject =
|
35
|
+
TCPClient::Configuration.new(
|
36
|
+
buffered: false,
|
37
|
+
keep_alive: false,
|
38
|
+
reverse_lookup: false,
|
39
|
+
connect_timeout: 1,
|
40
|
+
read_timeout: 2,
|
41
|
+
write_timeout: 3,
|
42
|
+
ssl: true
|
43
|
+
)
|
44
|
+
refute(subject.buffered)
|
45
|
+
refute(subject.keep_alive)
|
46
|
+
refute(subject.reverse_lookup)
|
47
|
+
assert_same(1, subject.connect_timeout)
|
48
|
+
assert_same(2, subject.read_timeout)
|
49
|
+
assert_same(3, subject.write_timeout)
|
50
|
+
assert(subject.ssl?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_invalid_option
|
54
|
+
err =
|
55
|
+
assert_raises(ArgumentError) do
|
56
|
+
TCPClient::Configuration.new(unknown_attr: :argument)
|
57
|
+
end
|
58
|
+
assert_includes(err.message, 'attribute')
|
59
|
+
assert_includes(err.message, 'unknown_attr')
|
60
|
+
end
|
61
|
+
|
30
62
|
def test_ssl_params
|
31
63
|
subject = TCPClient::Configuration.new
|
32
64
|
refute(subject.ssl?)
|
@@ -40,11 +72,12 @@ class ConfigurationTest < Test
|
|
40
72
|
end
|
41
73
|
|
42
74
|
def test_timeout_overwrite
|
43
|
-
subject =
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
75
|
+
subject =
|
76
|
+
TCPClient::Configuration.create do |cfg|
|
77
|
+
cfg.connect_timeout = 1
|
78
|
+
cfg.read_timeout = 2
|
79
|
+
cfg.write_timeout = 3
|
80
|
+
end
|
48
81
|
assert_same(1, subject.connect_timeout)
|
49
82
|
assert_same(2, subject.read_timeout)
|
50
83
|
assert_same(3, subject.write_timeout)
|
@@ -54,4 +87,12 @@ class ConfigurationTest < Test
|
|
54
87
|
assert_same(42, subject.read_timeout)
|
55
88
|
assert_same(42, subject.write_timeout)
|
56
89
|
end
|
90
|
+
|
91
|
+
def test_compare
|
92
|
+
a = TCPClient::Configuration.new
|
93
|
+
b = TCPClient::Configuration.new
|
94
|
+
assert_equal(a, b)
|
95
|
+
assert(a == b)
|
96
|
+
assert(a === b)
|
97
|
+
end
|
57
98
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class DefauktConfigurationTest < MiniTest::Test
|
4
|
+
def test_default
|
5
|
+
subject = TCPClient.configure # reset to defaults
|
6
|
+
|
7
|
+
assert_same(
|
8
|
+
TCPClient.default_configuration,
|
9
|
+
TCPClient::Configuration.default
|
10
|
+
)
|
11
|
+
assert(subject.buffered)
|
12
|
+
assert(subject.keep_alive)
|
13
|
+
assert(subject.reverse_lookup)
|
14
|
+
refute(subject.ssl?)
|
15
|
+
assert_nil(subject.connect_timeout)
|
16
|
+
assert_nil(subject.read_timeout)
|
17
|
+
assert_nil(subject.write_timeout)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_configure_options
|
21
|
+
TCPClient.configure(
|
22
|
+
buffered: false,
|
23
|
+
keep_alive: false,
|
24
|
+
reverse_lookup: false,
|
25
|
+
ssl: true,
|
26
|
+
connect_timeout: 1,
|
27
|
+
read_timeout: 2,
|
28
|
+
write_timeout: 3
|
29
|
+
)
|
30
|
+
subject = TCPClient.default_configuration
|
31
|
+
refute(subject.buffered)
|
32
|
+
refute(subject.keep_alive)
|
33
|
+
refute(subject.reverse_lookup)
|
34
|
+
assert(subject.ssl?)
|
35
|
+
assert_same(1, subject.connect_timeout)
|
36
|
+
assert_same(2, subject.read_timeout)
|
37
|
+
assert_same(3, subject.write_timeout)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_configure_block
|
41
|
+
TCPClient.configure do |cfg|
|
42
|
+
cfg.buffered = false
|
43
|
+
cfg.keep_alive = false
|
44
|
+
cfg.reverse_lookup = false
|
45
|
+
cfg.ssl = true
|
46
|
+
cfg.connect_timeout = 1
|
47
|
+
cfg.read_timeout = 2
|
48
|
+
cfg.write_timeout = 3
|
49
|
+
end
|
50
|
+
subject = TCPClient.default_configuration
|
51
|
+
refute(subject.buffered)
|
52
|
+
refute(subject.keep_alive)
|
53
|
+
refute(subject.reverse_lookup)
|
54
|
+
assert(subject.ssl?)
|
55
|
+
assert_same(1, subject.connect_timeout)
|
56
|
+
assert_same(2, subject.read_timeout)
|
57
|
+
assert_same(3, subject.write_timeout)
|
58
|
+
end
|
59
|
+
end
|
data/test/tcp_client_test.rb
CHANGED
@@ -1,26 +1,32 @@
|
|
1
1
|
require_relative 'test_helper'
|
2
2
|
|
3
|
-
class TCPClientTest < Test
|
3
|
+
class TCPClientTest < MiniTest::Test
|
4
|
+
parallelize_me!
|
5
|
+
|
6
|
+
HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
|
7
|
+
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@config = TCPClient::Configuration.create(buffered: false)
|
12
|
+
end
|
13
|
+
|
4
14
|
def test_defaults
|
5
15
|
subject = TCPClient.new
|
6
16
|
assert(subject.closed?)
|
7
17
|
assert_equal('', subject.to_s)
|
8
18
|
assert_nil(subject.address)
|
9
19
|
subject.close
|
10
|
-
assert_raises(TCPClient::NotConnected)
|
11
|
-
|
12
|
-
end
|
13
|
-
assert_raises(TCPClient::NotConnected) do
|
14
|
-
subject.read(42)
|
15
|
-
end
|
20
|
+
assert_raises(TCPClient::NotConnected) { subject.write('hello world!') }
|
21
|
+
assert_raises(TCPClient::NotConnected) { subject.read(42) }
|
16
22
|
end
|
17
23
|
|
18
24
|
def create_nonconnected_client
|
19
25
|
client = TCPClient.new
|
20
|
-
client.connect('',
|
26
|
+
client.connect('', config)
|
27
|
+
client
|
21
28
|
rescue Errno::EADDRNOTAVAIL
|
22
|
-
|
23
|
-
return client
|
29
|
+
client
|
24
30
|
end
|
25
31
|
|
26
32
|
def test_failed_state
|
@@ -32,84 +38,120 @@ class TCPClientTest < Test
|
|
32
38
|
assert_equal('localhost', subject.address.hostname)
|
33
39
|
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
34
40
|
assert_same(0, subject.address.addrinfo.ip_port)
|
35
|
-
assert_raises(TCPClient::NotConnected)
|
36
|
-
|
37
|
-
end
|
38
|
-
assert_raises(TCPClient::NotConnected) do
|
39
|
-
subject.read(42)
|
40
|
-
end
|
41
|
+
assert_raises(TCPClient::NotConnected) { subject.write('hello world!') }
|
42
|
+
assert_raises(TCPClient::NotConnected) { subject.read(42) }
|
41
43
|
end
|
42
44
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
def test_connected_state
|
46
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
47
|
+
refute(subject.closed?)
|
48
|
+
assert_equal('localhost:1234', subject.to_s)
|
49
|
+
refute_nil(subject.address)
|
50
|
+
address_when_opened = subject.address
|
51
|
+
assert_equal('localhost:1234', subject.address.to_s)
|
52
|
+
assert_equal('localhost', subject.address.hostname)
|
53
|
+
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
54
|
+
assert_same(1234, subject.address.addrinfo.ip_port)
|
55
|
+
|
56
|
+
subject.close
|
57
|
+
assert(subject.closed?)
|
58
|
+
assert_same(address_when_opened, subject.address)
|
59
|
+
end
|
49
60
|
end
|
50
61
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
assert_equal('localhost:1234', subject.address.to_s)
|
59
|
-
assert_equal('localhost', subject.address.hostname)
|
60
|
-
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
61
|
-
assert_same(1234, subject.address.addrinfo.ip_port)
|
62
|
-
|
63
|
-
subject.close
|
64
|
-
assert(subject.closed?)
|
65
|
-
assert_same(address_when_opened, subject.address)
|
62
|
+
def check_read_timeout(timeout)
|
63
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
64
|
+
refute(subject.closed?)
|
65
|
+
start_time = nil
|
66
|
+
assert_raises(TCPClient::ReadTimeoutError) do
|
67
|
+
start_time = Time.now
|
68
|
+
subject.read(42, timeout: timeout)
|
66
69
|
end
|
70
|
+
assert_in_delta(timeout, Time.now - start_time, 0.11)
|
67
71
|
end
|
68
72
|
end
|
69
73
|
|
70
|
-
def
|
71
|
-
|
74
|
+
def test_read_timeout
|
75
|
+
check_read_timeout(0.5)
|
76
|
+
check_read_timeout(1)
|
77
|
+
check_read_timeout(1.5)
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_write_timeout(timeout)
|
81
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
72
82
|
refute(subject.closed?)
|
73
83
|
start_time = nil
|
74
|
-
assert_raises(TCPClient::
|
84
|
+
assert_raises(TCPClient::WriteTimeoutError) do
|
75
85
|
start_time = Time.now
|
76
|
-
|
77
|
-
subject.write('?' * (1024 * 1024), timeout: timeout)
|
86
|
+
subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
|
78
87
|
end
|
79
88
|
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
80
|
-
|
81
|
-
|
82
|
-
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_write_timeout
|
93
|
+
check_write_timeout(0.01)
|
94
|
+
check_write_timeout(0.25)
|
95
|
+
end
|
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
|
83
117
|
end
|
84
|
-
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
85
118
|
end
|
86
119
|
end
|
87
120
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
121
|
+
def test_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
|
92
132
|
end
|
93
133
|
end
|
94
134
|
end
|
95
135
|
|
96
|
-
def check_connect_timeout(
|
136
|
+
def check_connect_timeout(ssl_config)
|
97
137
|
start_time = nil
|
98
|
-
assert_raises(TCPClient::
|
138
|
+
assert_raises(TCPClient::ConnectTimeoutError) do
|
99
139
|
start_time = Time.now
|
100
|
-
TCPClient.new.connect(
|
140
|
+
TCPClient.new.connect('localhost:1234', ssl_config)
|
101
141
|
end
|
102
|
-
assert_in_delta(
|
142
|
+
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
|
103
143
|
end
|
104
144
|
|
105
145
|
def test_connect_ssl_timeout
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
146
|
+
ssl_config = TCPClient::Configuration.new(ssl: true)
|
147
|
+
|
148
|
+
ssl_config.connect_timeout = 0.5
|
149
|
+
check_connect_timeout(ssl_config)
|
150
|
+
|
151
|
+
ssl_config.connect_timeout = 1
|
152
|
+
check_connect_timeout(ssl_config)
|
153
|
+
|
154
|
+
ssl_config.connect_timeout = 1.5
|
155
|
+
check_connect_timeout(ssl_config)
|
114
156
|
end
|
115
157
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'minitest/autorun'
|
2
4
|
require 'minitest/parallel'
|
3
5
|
require_relative '../lib/tcp-client'
|
4
6
|
|
5
7
|
$stdout.sync = $stderr.sync = true
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
# this pseudo-server never reads or writes anything
|
10
|
+
DummyServer = TCPServer.new('localhost', 1234)
|
11
|
+
Minitest.after_run { DummyServer.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.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -58,7 +58,7 @@ description: |
|
|
58
58
|
easy to use client which can handle time limits correctly. Unlike
|
59
59
|
other implementations this client respects given/configurable time
|
60
60
|
limits for each method (`connect`, `read`, `write`).
|
61
|
-
email:
|
61
|
+
email:
|
62
62
|
executables: []
|
63
63
|
extensions: []
|
64
64
|
extra_rdoc_files:
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/tcp-client.rb
|
71
71
|
- lib/tcp-client/address.rb
|
72
72
|
- lib/tcp-client/configuration.rb
|
73
|
+
- lib/tcp-client/default_configuration.rb
|
73
74
|
- lib/tcp-client/mixin/io_timeout.rb
|
74
75
|
- lib/tcp-client/ssl_socket.rb
|
75
76
|
- lib/tcp-client/tcp_socket.rb
|
@@ -81,6 +82,7 @@ files:
|
|
81
82
|
- tcp-client.gemspec
|
82
83
|
- test/tcp-client/address_test.rb
|
83
84
|
- test/tcp-client/configuration_test.rb
|
85
|
+
- test/tcp-client/default_configuration_test.rb
|
84
86
|
- test/tcp-client/version_test.rb
|
85
87
|
- test/tcp_client_test.rb
|
86
88
|
- test/test_helper.rb
|
@@ -89,7 +91,7 @@ licenses: []
|
|
89
91
|
metadata:
|
90
92
|
source_code_uri: https://github.com/mblumtritt/tcp-client
|
91
93
|
bug_tracker_uri: https://github.com/mblumtritt/tcp-client/issues
|
92
|
-
post_install_message:
|
94
|
+
post_install_message:
|
93
95
|
rdoc_options: []
|
94
96
|
require_paths:
|
95
97
|
- lib
|
@@ -97,20 +99,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
99
|
requirements:
|
98
100
|
- - ">="
|
99
101
|
- !ruby/object:Gem::Version
|
100
|
-
version: 2.
|
102
|
+
version: 2.7.0
|
101
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
104
|
requirements:
|
103
105
|
- - ">="
|
104
106
|
- !ruby/object:Gem::Version
|
105
|
-
version:
|
107
|
+
version: '0'
|
106
108
|
requirements: []
|
107
|
-
rubygems_version: 3.
|
108
|
-
signing_key:
|
109
|
+
rubygems_version: 3.2.9
|
110
|
+
signing_key:
|
109
111
|
specification_version: 4
|
110
112
|
summary: A TCP client implementation with working timeout support.
|
111
113
|
test_files:
|
112
114
|
- test/tcp-client/address_test.rb
|
113
115
|
- test/tcp-client/configuration_test.rb
|
116
|
+
- test/tcp-client/default_configuration_test.rb
|
114
117
|
- test/tcp-client/version_test.rb
|
115
118
|
- test/tcp_client_test.rb
|
116
119
|
- test/test_helper.rb
|