tcp-client 0.0.6 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +12 -6
- data/gems.rb +1 -1
- data/lib/tcp-client.rb +22 -13
- data/lib/tcp-client/address.rb +14 -12
- data/lib/tcp-client/configuration.rb +11 -4
- data/lib/tcp-client/default_configuration.rb +21 -0
- data/lib/tcp-client/mixin/io_timeout.rb +27 -19
- data/lib/tcp-client/ssl_socket.rb +8 -2
- data/lib/tcp-client/tcp_socket.rb +9 -3
- data/lib/tcp-client/version.rb +1 -1
- data/rakefile.rb +9 -2
- data/sample/google.rb +15 -10
- data/sample/google_ssl.rb +9 -5
- data/tcp-client.gemspec +10 -8
- data/test/tcp-client/address_test.rb +3 -1
- data/test/tcp-client/configuration_test.rb +44 -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 +54 -45
- data/test/test_helper.rb +3 -3
- metadata +14 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb5546419b47d6680b576f39d8c4b382c39d74f07ca43f6e782160122b6ba524
|
4
|
+
data.tar.gz: 3e563cd2db3efc765e491ece2fc37a784cb55ef05f6390eb23d1322a926be49e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 712f24a4dc3427cf37126681ba50d53f0aefc9f839920283f611ef22e923f65f53f09fc83ca66cb45930536ca3c13d630dc7b0c5a5f40c6fc6f91f82bf123c3c
|
7
|
+
data.tar.gz: e97e52fc2aa1ad3cc548d2f561df97678e00c0e7e617c08afdbb9c6537709fc2743a07f26679cc8007124348cb32f1e8481d6417b7d3e28d247b504fcc951805
|
data/README.md
CHANGED
@@ -8,20 +8,26 @@ 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
15
|
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
14
16
|
cfg.read_timeout = 0.5 # seconds to read some bytes
|
15
|
-
cfg.ssl_params = {ssl_version: :TLSv1_2} # use TLS 1.2
|
17
|
+
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
16
18
|
end
|
17
19
|
|
18
20
|
# the following request sequence is not allowed to last longer than 2 seconds:
|
19
21
|
# 1 second to connect (incl. SSL handshake etc.)
|
20
22
|
# + 0.25 seconds to write data
|
21
23
|
# + 0.5 seconds to read a response
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
|
25
|
+
TCPClient.open('www.google.com:443') do |client|
|
26
|
+
# simple HTTP get request
|
27
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
28
|
+
|
29
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
30
|
+
pp client.read(12)
|
25
31
|
end
|
26
32
|
```
|
27
33
|
|
@@ -47,7 +53,7 @@ To install the gem globally use:
|
|
47
53
|
$ gem install tcp-client
|
48
54
|
```
|
49
55
|
|
50
|
-
After that you need only a single line of code in your project
|
56
|
+
After that you need only a single line of code in your project to have all tools on board:
|
51
57
|
|
52
58
|
```ruby
|
53
59
|
require 'tcp-client'
|
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,22 +15,26 @@ class TCPClient
|
|
14
15
|
end
|
15
16
|
|
16
17
|
class NotConnected < SocketError
|
17
|
-
def self.raise!(
|
18
|
-
raise(self,
|
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)
|
28
|
-
|
29
|
-
client, ret = nil, client
|
30
|
-
ret
|
35
|
+
block_given? ? yield(client) : client
|
31
36
|
ensure
|
32
|
-
client
|
37
|
+
client&.close if block_given?
|
33
38
|
end
|
34
39
|
|
35
40
|
attr_reader :address
|
@@ -46,8 +51,10 @@ class TCPClient
|
|
46
51
|
close
|
47
52
|
NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
|
48
53
|
@address = Address.new(addr)
|
49
|
-
@socket = TCPSocket.new(@address, configuration,
|
50
|
-
|
54
|
+
@socket = TCPSocket.new(@address, configuration, ConnectTimeoutError)
|
55
|
+
configuration.ssl? &&
|
56
|
+
@socket =
|
57
|
+
SSLSocket.new(@socket, @address, configuration, ConnectTimeoutError)
|
51
58
|
@write_timeout = configuration.write_timeout
|
52
59
|
@read_timeout = configuration.read_timeout
|
53
60
|
self
|
@@ -55,7 +62,7 @@ class TCPClient
|
|
55
62
|
|
56
63
|
def close
|
57
64
|
socket, @socket = @socket, nil
|
58
|
-
socket
|
65
|
+
socket&.close
|
59
66
|
self
|
60
67
|
rescue IOError
|
61
68
|
self
|
@@ -66,11 +73,13 @@ class TCPClient
|
|
66
73
|
end
|
67
74
|
|
68
75
|
def read(nbytes, timeout: @read_timeout)
|
69
|
-
|
76
|
+
NotConnected.raise!(self) if closed?
|
77
|
+
@socket.read(nbytes, timeout: timeout, exception: ReadTimeoutError)
|
70
78
|
end
|
71
79
|
|
72
80
|
def write(*msg, timeout: @write_timeout)
|
73
|
-
|
81
|
+
NotConnected.raise!(self) if closed?
|
82
|
+
@socket.write(*msg, timeout: timeout, exception: WriteTimeoutError)
|
74
83
|
end
|
75
84
|
|
76
85
|
def flush
|
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,29 +20,31 @@ class TCPClient
|
|
20
20
|
@addrinfo.freeze
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@hostname, port = from_string(str.to_s)
|
27
|
-
return init_from_addrinfo(Addrinfo.tcp(nil, port)) unless @hostname
|
28
|
-
@addrinfo = Addrinfo.tcp(@hostname, port)
|
29
|
-
@to_s = @hostname.index(':') ? "[#{@hostname}]:#{port}" : "#{@hostname}:#{port}"
|
23
|
+
def to_s
|
24
|
+
return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
|
25
|
+
"#{@hostname}:#{@addrinfo.ip_port}"
|
30
26
|
end
|
31
27
|
|
28
|
+
private
|
29
|
+
|
32
30
|
def init_from_selfclass(address)
|
33
|
-
@to_s = address.to_s
|
34
31
|
@hostname = address.hostname
|
35
32
|
@addrinfo = address.addrinfo
|
36
33
|
end
|
37
34
|
|
38
35
|
def init_from_addrinfo(addrinfo)
|
39
|
-
@hostname,
|
40
|
-
@to_s = "#{@hostname}:#{port}"
|
36
|
+
@hostname, _port = addrinfo.getnameinfo(Socket::NI_NUMERICSERV)
|
41
37
|
@addrinfo = addrinfo
|
42
38
|
end
|
43
39
|
|
40
|
+
def init_from_string(str)
|
41
|
+
@hostname, port = from_string(str.to_s)
|
42
|
+
return init_from_addrinfo(Addrinfo.tcp(nil, port)) unless @hostname
|
43
|
+
@addrinfo = Addrinfo.tcp(@hostname, port)
|
44
|
+
end
|
45
|
+
|
44
46
|
def from_string(str)
|
45
|
-
return
|
47
|
+
return nil, str.to_i unless idx = str.rindex(':')
|
46
48
|
name = str[0, idx]
|
47
49
|
name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
|
48
50
|
[name, str[idx + 1, str.size - idx].to_i]
|
@@ -1,7 +1,7 @@
|
|
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
|
@@ -9,9 +9,10 @@ class TCPClient
|
|
9
9
|
attr_reader :buffered, :keep_alive, :reverse_lookup
|
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?
|
@@ -67,8 +68,14 @@ class TCPClient
|
|
67
68
|
|
68
69
|
private
|
69
70
|
|
71
|
+
def set(attribute, value)
|
72
|
+
public_send("#{attribute}=", value)
|
73
|
+
rescue NoMethodError
|
74
|
+
raise(ArgumentError, "unknown attribute - #{attribute}")
|
75
|
+
end
|
76
|
+
|
70
77
|
def seconds(value)
|
71
|
-
value
|
78
|
+
value&.positive? ? value : nil
|
72
79
|
end
|
73
80
|
end
|
74
81
|
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
|
@@ -3,31 +3,39 @@ IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
|
|
3
3
|
module IOTimeoutMixin
|
4
4
|
def self.included(mod)
|
5
5
|
im = mod.instance_methods
|
6
|
-
|
6
|
+
if im.index(:wait_writable) && im.index(:wait_readable)
|
7
|
+
mod.include(DeadlineMethods)
|
8
|
+
else
|
9
|
+
mod.include(DeadlineIO)
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
13
|
def read(nbytes, timeout: nil, exception: IOTimeoutError)
|
10
14
|
timeout = timeout.to_f
|
11
|
-
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
|
12
16
|
deadline = Time.now + timeout
|
13
17
|
read_all(nbytes) do |junk_size|
|
14
|
-
with_deadline(deadline, exception)
|
18
|
+
with_deadline(deadline, exception) do
|
19
|
+
read_nonblock(junk_size, exception: false)
|
20
|
+
end
|
15
21
|
end
|
16
22
|
end
|
17
23
|
|
18
24
|
def write(*msgs, timeout: nil, exception: IOTimeoutError)
|
19
25
|
timeout = timeout.to_f
|
20
|
-
return write_all(msgs.join){ |junk| super(junk) } if timeout <= 0
|
26
|
+
return write_all(msgs.join.b) { |junk| super(junk) } if timeout <= 0
|
21
27
|
deadline = Time.now + timeout
|
22
|
-
write_all(msgs.join) do |junk|
|
23
|
-
with_deadline(deadline, exception)
|
28
|
+
write_all(msgs.join.b) do |junk|
|
29
|
+
with_deadline(deadline, exception) do
|
30
|
+
write_nonblock(junk, exception: false)
|
31
|
+
end
|
24
32
|
end
|
25
33
|
end
|
26
34
|
|
27
35
|
private
|
28
36
|
|
29
37
|
def read_all(nbytes)
|
30
|
-
return '' if
|
38
|
+
return '' if nbytes.zero?
|
31
39
|
result = ''
|
32
40
|
loop do
|
33
41
|
unless read = yield(nbytes - result.bytesize)
|
@@ -40,7 +48,7 @@ module IOTimeoutMixin
|
|
40
48
|
end
|
41
49
|
|
42
50
|
def write_all(data)
|
43
|
-
return 0 if
|
51
|
+
return 0 if (size = data.bytesize).zero?
|
44
52
|
result = 0
|
45
53
|
loop do
|
46
54
|
written = yield(data)
|
@@ -50,18 +58,18 @@ module IOTimeoutMixin
|
|
50
58
|
end
|
51
59
|
end
|
52
60
|
|
53
|
-
module
|
61
|
+
module DeadlineMethods
|
54
62
|
private
|
55
63
|
|
56
64
|
def with_deadline(deadline, exclass)
|
57
65
|
loop do
|
58
66
|
case ret = yield
|
59
67
|
when :wait_writable
|
60
|
-
remaining_time = deadline - Time.now
|
61
|
-
raise(exclass) if
|
68
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
69
|
+
raise(exclass) if wait_writable(remaining_time).nil?
|
62
70
|
when :wait_readable
|
63
|
-
remaining_time = deadline - Time.now
|
64
|
-
raise(exclass) if
|
71
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
72
|
+
raise(exclass) if wait_readable(remaining_time).nil?
|
65
73
|
else
|
66
74
|
return ret
|
67
75
|
end
|
@@ -69,18 +77,18 @@ module IOTimeoutMixin
|
|
69
77
|
end
|
70
78
|
end
|
71
79
|
|
72
|
-
module
|
80
|
+
module DeadlineIO
|
73
81
|
private
|
74
82
|
|
75
83
|
def with_deadline(deadline, exclass)
|
76
84
|
loop do
|
77
85
|
case ret = yield
|
78
86
|
when :wait_writable
|
79
|
-
remaining_time = deadline - Time.now
|
80
|
-
raise(exclass) if
|
87
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
88
|
+
raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
81
89
|
when :wait_readable
|
82
|
-
remaining_time = deadline - Time.now
|
83
|
-
raise(exclass) if
|
90
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
91
|
+
raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
|
84
92
|
else
|
85
93
|
return ret
|
86
94
|
end
|
@@ -88,5 +96,5 @@ module IOTimeoutMixin
|
|
88
96
|
end
|
89
97
|
end
|
90
98
|
|
91
|
-
private_constant
|
99
|
+
private_constant(:DeadlineMethods, :DeadlineIO)
|
92
100
|
end
|
@@ -31,10 +31,16 @@ class TCPClient
|
|
31
31
|
|
32
32
|
def connect_to(address, check, timeout, exception)
|
33
33
|
self.hostname = address.hostname
|
34
|
-
|
34
|
+
if timeout
|
35
|
+
with_deadline(Time.now + timeout, exception) do
|
36
|
+
connect_nonblock(exception: false)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
connect
|
40
|
+
end
|
35
41
|
post_connection_check(address.hostname) if check
|
36
42
|
end
|
37
43
|
end
|
38
44
|
|
39
|
-
private_constant
|
45
|
+
private_constant(:SSLSocket)
|
40
46
|
end
|
@@ -14,9 +14,15 @@ class TCPClient
|
|
14
14
|
private
|
15
15
|
|
16
16
|
def connect_to(address, timeout, exception)
|
17
|
-
addr =
|
17
|
+
addr =
|
18
|
+
::Socket.pack_sockaddr_in(
|
19
|
+
address.addrinfo.ip_port,
|
20
|
+
address.addrinfo.ip_address
|
21
|
+
)
|
18
22
|
return connect(addr) unless timeout
|
19
|
-
with_deadline(Time.now + timeout, exception)
|
23
|
+
with_deadline(Time.now + timeout, exception) do
|
24
|
+
connect_nonblock(addr, exception: false)
|
25
|
+
end
|
20
26
|
end
|
21
27
|
|
22
28
|
def configure(configuration)
|
@@ -29,5 +35,5 @@ class TCPClient
|
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
32
|
-
private_constant
|
38
|
+
private_constant(:TCPSocket)
|
33
39
|
end
|
data/lib/tcp-client/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake/clean'
|
2
4
|
require 'rake/testtask'
|
5
|
+
require 'bundler/gem_tasks'
|
6
|
+
|
7
|
+
STDOUT.sync = STDERR.sync = true
|
8
|
+
|
9
|
+
CLOBBER << 'prj'
|
3
10
|
|
4
11
|
Rake::TestTask.new(:test) do |t|
|
5
12
|
t.ruby_opts = %w[-w]
|
@@ -8,5 +15,5 @@ Rake::TestTask.new(:test) do |t|
|
|
8
15
|
end
|
9
16
|
|
10
17
|
task :default do
|
11
|
-
exec(
|
18
|
+
exec("#{$PROGRAM_NAME} --tasks")
|
12
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,21 @@
|
|
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
5
|
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
6
6
|
cfg.read_timeout = 0.5 # seconds to read some bytes
|
7
|
-
cfg.ssl_params = {ssl_version: :TLSv1_2} # use TLS 1.2
|
7
|
+
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
8
8
|
end
|
9
9
|
|
10
10
|
# the following request sequence is not allowed to last longer than 2 seconds:
|
11
11
|
# 1 second to connect (incl. SSL handshake etc.)
|
12
12
|
# + 0.25 seconds to write data
|
13
13
|
# + 0.5 seconds to read a response
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
|
15
|
+
TCPClient.open('www.google.com:443') 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)
|
17
21
|
end
|
data/tcp-client.gemspec
CHANGED
@@ -1,31 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative './lib/tcp-client/version'
|
4
4
|
|
5
5
|
GemSpec = Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'tcp-client'
|
7
7
|
spec.version = TCPClient::VERSION
|
8
8
|
spec.summary = 'A TCP client implementation with working timeout support.'
|
9
|
-
spec.description = <<~
|
9
|
+
spec.description = <<~DESCRIPTION
|
10
10
|
This gem implements a TCP client with (optional) SSL support. The
|
11
11
|
motivation of this project is the need to have a _really working_
|
12
12
|
easy to use client which can handle time limits correctly. Unlike
|
13
13
|
other implementations this client respects given/configurable time
|
14
14
|
limits for each method (`connect`, `read`, `write`).
|
15
|
-
|
15
|
+
DESCRIPTION
|
16
16
|
spec.author = 'Mike Blumtritt'
|
17
|
-
spec.email = 'mike.blumtritt@
|
17
|
+
spec.email = 'mike.blumtritt@pm.me'
|
18
18
|
spec.homepage = 'https://github.com/mblumtritt/tcp-client'
|
19
|
-
spec.metadata = {
|
19
|
+
spec.metadata = {
|
20
|
+
'source_code_uri' => 'https://github.com/mblumtritt/tcp-client',
|
21
|
+
'bug_tracker_uri' => 'https://github.com/mblumtritt/tcp-client/issues'
|
22
|
+
}
|
20
23
|
spec.rubyforge_project = spec.name
|
21
24
|
|
22
25
|
spec.add_development_dependency 'bundler'
|
23
|
-
spec.add_development_dependency 'rake'
|
24
26
|
spec.add_development_dependency 'minitest'
|
27
|
+
spec.add_development_dependency 'rake'
|
25
28
|
|
26
29
|
spec.platform = Gem::Platform::RUBY
|
27
|
-
spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
|
28
30
|
spec.required_ruby_version = '>= 2.5.0'
|
31
|
+
spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
|
29
32
|
|
30
33
|
spec.require_paths = %w[lib]
|
31
34
|
|
@@ -33,6 +36,5 @@ GemSpec = Gem::Specification.new do |spec|
|
|
33
36
|
spec.test_files = all_files.grep(%r{^(spec|test)/})
|
34
37
|
spec.files = all_files - spec.test_files
|
35
38
|
|
36
|
-
spec.has_rdoc = false
|
37
39
|
spec.extra_rdoc_files = %w[README.md]
|
38
40
|
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)
|
@@ -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,23 +1,27 @@
|
|
1
1
|
require_relative 'test_helper'
|
2
2
|
|
3
|
-
class TCPClientTest < Test
|
3
|
+
class TCPClientTest < MiniTest::Test
|
4
|
+
parallelize_me!
|
5
|
+
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@config = TCPClient::Configuration.create(buffered: false)
|
10
|
+
end
|
11
|
+
|
4
12
|
def test_defaults
|
5
13
|
subject = TCPClient.new
|
6
14
|
assert(subject.closed?)
|
7
15
|
assert_equal('', subject.to_s)
|
8
16
|
assert_nil(subject.address)
|
9
17
|
subject.close
|
10
|
-
assert_raises(TCPClient::NotConnected)
|
11
|
-
|
12
|
-
end
|
13
|
-
assert_raises(TCPClient::NotConnected) do
|
14
|
-
subject.read(42)
|
15
|
-
end
|
18
|
+
assert_raises(TCPClient::NotConnected) { subject.write('hello world!') }
|
19
|
+
assert_raises(TCPClient::NotConnected) { subject.read(42) }
|
16
20
|
end
|
17
21
|
|
18
22
|
def create_nonconnected_client
|
19
23
|
client = TCPClient.new
|
20
|
-
client.connect('',
|
24
|
+
client.connect('', config)
|
21
25
|
rescue Errno::EADDRNOTAVAIL
|
22
26
|
ensure
|
23
27
|
return client
|
@@ -32,17 +36,12 @@ class TCPClientTest < Test
|
|
32
36
|
assert_equal('localhost', subject.address.hostname)
|
33
37
|
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
34
38
|
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
|
39
|
+
assert_raises(TCPClient::NotConnected) { subject.write('hello world!') }
|
40
|
+
assert_raises(TCPClient::NotConnected) { subject.read(42) }
|
41
41
|
end
|
42
42
|
|
43
43
|
def test_connected_state
|
44
|
-
|
45
|
-
TCPClient.open('localhost:1234', TCPClient::Configuration.new) do |subject|
|
44
|
+
TCPClient.open('localhost:1234') do |subject|
|
46
45
|
refute(subject.closed?)
|
47
46
|
assert_equal('localhost:1234', subject.to_s)
|
48
47
|
refute_nil(subject.address)
|
@@ -56,55 +55,65 @@ class TCPClientTest < Test
|
|
56
55
|
assert(subject.closed?)
|
57
56
|
assert_same(address_when_opened, subject.address)
|
58
57
|
end
|
59
|
-
ensure
|
60
|
-
server.close if server
|
61
58
|
end
|
62
59
|
|
63
|
-
def
|
64
|
-
TCPClient.open(
|
60
|
+
def check_read_timeout(timeout)
|
61
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
65
62
|
refute(subject.closed?)
|
66
63
|
start_time = nil
|
67
|
-
assert_raises(TCPClient::
|
64
|
+
assert_raises(TCPClient::ReadTimeoutError) do
|
68
65
|
start_time = Time.now
|
69
|
-
|
70
|
-
subject.write('?' * (1024 * 1024), timeout: timeout)
|
66
|
+
subject.read(42, timeout: timeout)
|
71
67
|
end
|
72
68
|
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
73
|
-
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_read_timeout
|
73
|
+
check_read_timeout(0.5)
|
74
|
+
check_read_timeout(1)
|
75
|
+
check_read_timeout(1.5)
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_write_timeout(timeout)
|
79
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
80
|
+
refute(subject.closed?)
|
81
|
+
start_time = nil
|
82
|
+
assert_raises(TCPClient::WriteTimeoutError) do
|
74
83
|
start_time = Time.now
|
75
|
-
|
84
|
+
|
85
|
+
# send 1MB to avoid any TCP stack buffering
|
86
|
+
args = Array.new(2024, '?' * 1024)
|
87
|
+
subject.write(*args, timeout: timeout)
|
76
88
|
end
|
77
89
|
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
78
90
|
end
|
79
91
|
end
|
80
92
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
check_read_write_timeout(':1235', timeout)
|
85
|
-
end
|
86
|
-
ensure
|
87
|
-
server.close if server
|
93
|
+
def test_write_timeout
|
94
|
+
check_write_timeout(0.1)
|
95
|
+
check_write_timeout(0.25)
|
88
96
|
end
|
89
97
|
|
90
|
-
def check_connect_timeout(
|
98
|
+
def check_connect_timeout(ssl_config)
|
91
99
|
start_time = nil
|
92
|
-
assert_raises(TCPClient::
|
100
|
+
assert_raises(TCPClient::ConnectTimeoutError) do
|
93
101
|
start_time = Time.now
|
94
|
-
TCPClient.new.connect(
|
102
|
+
TCPClient.new.connect('localhost:1234', ssl_config)
|
95
103
|
end
|
96
|
-
assert_in_delta(
|
104
|
+
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.02)
|
97
105
|
end
|
98
106
|
|
99
107
|
def test_connect_ssl_timeout
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
108
|
+
ssl_config = TCPClient::Configuration.new(ssl: true)
|
109
|
+
|
110
|
+
ssl_config.connect_timeout = 0.5
|
111
|
+
check_connect_timeout(ssl_config)
|
112
|
+
|
113
|
+
ssl_config.connect_timeout = 1
|
114
|
+
check_connect_timeout(ssl_config)
|
115
|
+
|
116
|
+
ssl_config.connect_timeout = 1.5
|
117
|
+
check_connect_timeout(ssl_config)
|
109
118
|
end
|
110
119
|
end
|
data/test/test_helper.rb
CHANGED
@@ -4,6 +4,6 @@ require_relative '../lib/tcp-client'
|
|
4
4
|
|
5
5
|
$stdout.sync = $stderr.sync = true
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
# this pseudo-server never reads or writes anything
|
8
|
+
DummyServer = TCPServer.new('localhost', 1234)
|
9
|
+
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.0
|
4
|
+
version: 0.1.0
|
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-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: minitest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -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: mike.blumtritt@
|
61
|
+
email: mike.blumtritt@pm.me
|
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,14 +82,16 @@ 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
|
87
89
|
homepage: https://github.com/mblumtritt/tcp-client
|
88
90
|
licenses: []
|
89
91
|
metadata:
|
90
|
-
|
91
|
-
|
92
|
+
source_code_uri: https://github.com/mblumtritt/tcp-client
|
93
|
+
bug_tracker_uri: https://github.com/mblumtritt/tcp-client/issues
|
94
|
+
post_install_message:
|
92
95
|
rdoc_options: []
|
93
96
|
require_paths:
|
94
97
|
- lib
|
@@ -103,14 +106,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
106
|
- !ruby/object:Gem::Version
|
104
107
|
version: 1.3.6
|
105
108
|
requirements: []
|
106
|
-
|
107
|
-
|
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
|