tcp-client 0.1.0 → 0.2.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 +8 -12
- data/lib/tcp-client.rb +42 -35
- data/lib/tcp-client/address.rb +14 -1
- data/lib/tcp-client/configuration.rb +33 -11
- data/lib/tcp-client/errors.rb +37 -0
- data/lib/tcp-client/mixin/io_timeout.rb +26 -8
- data/lib/tcp-client/mixin/io_with_deadline.rb +79 -0
- data/lib/tcp-client/ssl_socket.rb +6 -5
- data/lib/tcp-client/tcp_socket.rb +4 -3
- data/lib/tcp-client/version.rb +1 -1
- data/rakefile.rb +3 -5
- data/sample/google_ssl.rb +7 -11
- data/tcp-client.gemspec +9 -15
- data/test/tcp-client/address_test.rb +9 -1
- data/test/tcp-client/configuration_test.rb +8 -0
- data/test/tcp_client_test.rb +49 -11
- data/test/test_helper.rb +2 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4738c2c07c1163d9a991fa08978d148ebdee182193a90103ecba277cd783f4b3
|
4
|
+
data.tar.gz: a69ecf05054b5260ab9a324226d5d0a592e337a09a68865c4702e37f4a948a4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c348cb8696ab9fec32321499b4fa62f169ce5b62e4d703df009a7762cd40e9c6e9cbe22c52affce4a493f4d473907f6b051a74e2a46e9de9ff5fc13d24bdd24
|
7
|
+
data.tar.gz: 40d45b9e44f4f7d6fd8e9514ae0e507c39e7177df8cca986b13f461753d00de579bae04578db49a0b5a67b86e6d9527e35cc8db87eb5ff4646b972c9ac4a6b90
|
data/README.md
CHANGED
@@ -12,22 +12,18 @@ require 'tcp-client'
|
|
12
12
|
|
13
13
|
TCPClient.configure do |cfg|
|
14
14
|
cfg.connect_timeout = 1 # second to connect the server
|
15
|
-
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
16
|
-
cfg.read_timeout = 0.5 # seconds to read some bytes
|
17
15
|
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
18
16
|
end
|
19
17
|
|
20
|
-
# the following request sequence is not allowed to last longer than 2 seconds:
|
21
|
-
# 1 second to connect (incl. SSL handshake etc.)
|
22
|
-
# + 0.25 seconds to write data
|
23
|
-
# + 0.5 seconds to read a response
|
24
|
-
|
25
18
|
TCPClient.open('www.google.com:443') do |client|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
19
|
+
# query should not last longer than 0.5 seconds
|
20
|
+
client.with_deadline(0.5) do
|
21
|
+
# simple HTTP get request
|
22
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
23
|
+
|
24
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
25
|
+
pp client.read(12)
|
26
|
+
end
|
31
27
|
end
|
32
28
|
```
|
33
29
|
|
data/lib/tcp-client.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'tcp-client/errors'
|
3
4
|
require_relative 'tcp-client/address'
|
4
5
|
require_relative 'tcp-client/tcp_socket'
|
5
6
|
require_relative 'tcp-client/ssl_socket'
|
@@ -8,30 +9,9 @@ require_relative 'tcp-client/default_configuration'
|
|
8
9
|
require_relative 'tcp-client/version'
|
9
10
|
|
10
11
|
class TCPClient
|
11
|
-
class NoOpenSSL < RuntimeError
|
12
|
-
def self.raise!
|
13
|
-
raise(self, 'OpenSSL is not avail', caller(1))
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class NotConnected < SocketError
|
18
|
-
def self.raise!(reason)
|
19
|
-
raise(self, "client not connected - #{reason}", caller(1))
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
TimeoutError = Class.new(IOError)
|
24
|
-
ConnectTimeoutError = Class.new(TimeoutError)
|
25
|
-
ReadTimeoutError = Class.new(TimeoutError)
|
26
|
-
WriteTimeoutError = Class.new(TimeoutError)
|
27
|
-
|
28
|
-
Timeout = TimeoutError # backward compatibility
|
29
|
-
deprecate_constant(:Timeout)
|
30
|
-
|
31
12
|
def self.open(addr, configuration = Configuration.default)
|
32
|
-
addr = Address.new(addr)
|
33
13
|
client = new
|
34
|
-
client.connect(addr, configuration)
|
14
|
+
client.connect(Address.new(addr), configuration)
|
35
15
|
block_given? ? yield(client) : client
|
36
16
|
ensure
|
37
17
|
client&.close if block_given?
|
@@ -40,46 +20,73 @@ class TCPClient
|
|
40
20
|
attr_reader :address
|
41
21
|
|
42
22
|
def initialize
|
43
|
-
@socket = @address = @write_timeout = @read_timeout = nil
|
23
|
+
@socket = @address = @write_timeout = @read_timeout = @deadline = nil
|
44
24
|
end
|
45
25
|
|
46
26
|
def to_s
|
47
27
|
@address ? @address.to_s : ''
|
48
28
|
end
|
49
29
|
|
50
|
-
def connect(addr, configuration)
|
30
|
+
def connect(addr, configuration, exception: ConnectTimeoutError)
|
51
31
|
close
|
52
32
|
NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
|
53
33
|
@address = Address.new(addr)
|
54
|
-
@socket = TCPSocket.new(@address, configuration,
|
34
|
+
@socket = TCPSocket.new(@address, configuration, exception)
|
55
35
|
configuration.ssl? &&
|
56
|
-
@socket =
|
57
|
-
SSLSocket.new(@socket, @address, configuration, ConnectTimeoutError)
|
36
|
+
@socket = SSLSocket.new(@socket, @address, configuration, exception)
|
58
37
|
@write_timeout = configuration.write_timeout
|
59
38
|
@read_timeout = configuration.read_timeout
|
60
39
|
self
|
61
40
|
end
|
62
41
|
|
63
42
|
def close
|
64
|
-
|
65
|
-
socket&.close
|
43
|
+
@socket&.close
|
66
44
|
self
|
67
45
|
rescue IOError
|
68
46
|
self
|
47
|
+
ensure
|
48
|
+
@socket = @deadline = nil
|
69
49
|
end
|
70
50
|
|
71
51
|
def closed?
|
72
52
|
@socket.nil? || @socket.closed?
|
73
53
|
end
|
74
54
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
55
|
+
def with_deadline(timeout)
|
56
|
+
NoBlockGiven.raise! unless block_given?
|
57
|
+
previous_deadline = @deadline
|
58
|
+
tm = timeout&.to_f
|
59
|
+
InvalidDeadLine.raise! unless tm&.positive?
|
60
|
+
@deadline = Time.now + tm
|
61
|
+
yield(self)
|
62
|
+
ensure
|
63
|
+
@deadline = previous_deadline
|
64
|
+
end
|
65
|
+
|
66
|
+
def read(nbytes, timeout: nil, exception: ReadTimeoutError)
|
67
|
+
NotConnected.raise! if closed?
|
68
|
+
if timeout.nil? && @deadline
|
69
|
+
return @socket.read_with_deadline(nbytes, @deadline, exception)
|
70
|
+
end
|
71
|
+
timeout = (timeout || @read_timeout).to_f
|
72
|
+
if timeout.positive?
|
73
|
+
@socket.read_with_deadline(nbytes, Time.now + timeout, exception)
|
74
|
+
else
|
75
|
+
@socket.read(nbytes)
|
76
|
+
end
|
78
77
|
end
|
79
78
|
|
80
|
-
def write(*msg, timeout:
|
81
|
-
NotConnected.raise!
|
82
|
-
|
79
|
+
def write(*msg, timeout: nil, exception: WriteTimeoutError)
|
80
|
+
NotConnected.raise! if closed?
|
81
|
+
if timeout.nil? && @deadline
|
82
|
+
return @socket.write_with_deadline(msg.join.b, @deadline, exception)
|
83
|
+
end
|
84
|
+
timeout = (timeout || @read_timeout).to_f
|
85
|
+
if timeout.positive?
|
86
|
+
@socket.write_with_deadline(msg.join.b, Time.now + timeout, exception)
|
87
|
+
else
|
88
|
+
@socket.write(*msg)
|
89
|
+
end
|
83
90
|
end
|
84
91
|
|
85
92
|
def flush
|
data/lib/tcp-client/address.rb
CHANGED
@@ -25,6 +25,19 @@ class TCPClient
|
|
25
25
|
"#{@hostname}:#{@addrinfo.ip_port}"
|
26
26
|
end
|
27
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
|
+
|
28
41
|
private
|
29
42
|
|
30
43
|
def init_from_selfclass(address)
|
@@ -44,7 +57,7 @@ class TCPClient
|
|
44
57
|
end
|
45
58
|
|
46
59
|
def from_string(str)
|
47
|
-
|
60
|
+
idx = str.rindex(':') or return nil, str.to_i
|
48
61
|
name = str[0, idx]
|
49
62
|
name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
|
50
63
|
[name, str[idx + 1, str.size - idx].to_i]
|
@@ -6,7 +6,7 @@ class TCPClient
|
|
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
12
|
def initialize(options = {})
|
@@ -19,22 +19,22 @@ class TCPClient
|
|
19
19
|
@ssl_params ? true : false
|
20
20
|
end
|
21
21
|
|
22
|
-
def ssl=(
|
23
|
-
return @ssl_params = nil unless
|
24
|
-
return @ssl_params =
|
22
|
+
def ssl=(value)
|
23
|
+
return @ssl_params = nil unless value
|
24
|
+
return @ssl_params = value.dup if Hash === value
|
25
25
|
@ssl_params ||= {}
|
26
26
|
end
|
27
27
|
|
28
|
-
def buffered=(
|
29
|
-
@buffered =
|
28
|
+
def buffered=(value)
|
29
|
+
@buffered = value ? true : false
|
30
30
|
end
|
31
31
|
|
32
|
-
def keep_alive=(
|
33
|
-
@keep_alive =
|
32
|
+
def keep_alive=(value)
|
33
|
+
@keep_alive = value ? true : false
|
34
34
|
end
|
35
35
|
|
36
|
-
def reverse_lookup=(
|
37
|
-
@reverse_lookup =
|
36
|
+
def reverse_lookup=(value)
|
37
|
+
@reverse_lookup = value ? true : false
|
38
38
|
end
|
39
39
|
|
40
40
|
def timeout=(seconds)
|
@@ -66,6 +66,28 @@ class TCPClient
|
|
66
66
|
@read_timeout = seconds(seconds)
|
67
67
|
end
|
68
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
|
+
|
69
91
|
private
|
70
92
|
|
71
93
|
def set(attribute, value)
|
@@ -75,7 +97,7 @@ class TCPClient
|
|
75
97
|
end
|
76
98
|
|
77
99
|
def seconds(value)
|
78
|
-
value&.positive? ? value : nil
|
100
|
+
value&.to_f&.positive? ? value : nil
|
79
101
|
end
|
80
102
|
end
|
81
103
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class TCPClient
|
6
|
+
class NoOpenSSL < RuntimeError
|
7
|
+
def self.raise!
|
8
|
+
raise(self, 'OpenSSL is not avail', caller(1))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoBlockGiven < RuntimeError
|
13
|
+
def self.raise!
|
14
|
+
raise(self, 'no block given', caller(1))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidDeadLine < ArgumentError
|
19
|
+
def self.raise!(timeout)
|
20
|
+
raise(self, "invalid deadline - #{timeout}", caller(1))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class NotConnected < SocketError
|
25
|
+
def self.raise!
|
26
|
+
raise(self, 'client not connected', caller(1))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
TimeoutError = Class.new(IOError)
|
31
|
+
ConnectTimeoutError = Class.new(TimeoutError)
|
32
|
+
ReadTimeoutError = Class.new(TimeoutError)
|
33
|
+
WriteTimeoutError = Class.new(TimeoutError)
|
34
|
+
|
35
|
+
Timeout = TimeoutError # backward compatibility
|
36
|
+
deprecate_constant(:Timeout)
|
37
|
+
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# I keep this file for backward compatibility.
|
2
|
+
# This may be removed in one of the next versions.
|
3
|
+
# Let me know if you like to use it elsewhere...
|
4
|
+
|
1
5
|
IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
|
2
6
|
|
3
7
|
module IOTimeoutMixin
|
@@ -10,6 +14,24 @@ module IOTimeoutMixin
|
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
17
|
+
def read_with_deadline(nbytes, deadline, exception)
|
18
|
+
result = ''.b
|
19
|
+
return result if nbytes.zero?
|
20
|
+
loop do
|
21
|
+
junk_size = nbytes - result.bytesize
|
22
|
+
read =
|
23
|
+
with_deadline(deadline, exception) do
|
24
|
+
read_nonblock(junk_size, exception: false)
|
25
|
+
end
|
26
|
+
unless read
|
27
|
+
close
|
28
|
+
return result
|
29
|
+
end
|
30
|
+
result += read
|
31
|
+
return result if result.bytesize >= nbytes
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
13
35
|
def read(nbytes, timeout: nil, exception: IOTimeoutError)
|
14
36
|
timeout = timeout.to_f
|
15
37
|
return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
|
@@ -35,8 +57,8 @@ module IOTimeoutMixin
|
|
35
57
|
private
|
36
58
|
|
37
59
|
def read_all(nbytes)
|
38
|
-
return '' if nbytes.zero?
|
39
|
-
result = ''
|
60
|
+
return ''.b if nbytes.zero?
|
61
|
+
result = ''.b
|
40
62
|
loop do
|
41
63
|
unless read = yield(nbytes - result.bytesize)
|
42
64
|
close
|
@@ -59,9 +81,7 @@ module IOTimeoutMixin
|
|
59
81
|
end
|
60
82
|
|
61
83
|
module DeadlineMethods
|
62
|
-
private
|
63
|
-
|
64
|
-
def with_deadline(deadline, exclass)
|
84
|
+
private def with_deadline(deadline, exclass)
|
65
85
|
loop do
|
66
86
|
case ret = yield
|
67
87
|
when :wait_writable
|
@@ -78,9 +98,7 @@ module IOTimeoutMixin
|
|
78
98
|
end
|
79
99
|
|
80
100
|
module DeadlineIO
|
81
|
-
private
|
82
|
-
|
83
|
-
def with_deadline(deadline, exclass)
|
101
|
+
private def with_deadline(deadline, exclass)
|
84
102
|
loop do
|
85
103
|
case ret = yield
|
86
104
|
when :wait_writable
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module IOWithDeadlineMixin
|
2
|
+
def self.included(mod)
|
3
|
+
im = mod.instance_methods
|
4
|
+
if im.index(:wait_writable) && im.index(:wait_readable)
|
5
|
+
mod.include(ViaWaitMethod)
|
6
|
+
else
|
7
|
+
mod.include(ViaSelect)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_with_deadline(nbytes, deadline, exclass)
|
12
|
+
raise(exclass) if Time.now > deadline
|
13
|
+
result = ''.b
|
14
|
+
return result if nbytes.zero?
|
15
|
+
loop do
|
16
|
+
read =
|
17
|
+
with_deadline(deadline, exclass) do
|
18
|
+
read_nonblock(nbytes - result.bytesize, exception: false)
|
19
|
+
end
|
20
|
+
unless read
|
21
|
+
close
|
22
|
+
return result
|
23
|
+
end
|
24
|
+
result += read
|
25
|
+
return result if result.bytesize >= nbytes
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def write_with_deadline(data, deadline, exclass)
|
30
|
+
raise(exclass) if Time.now > deadline
|
31
|
+
return 0 if (size = data.bytesize).zero?
|
32
|
+
result = 0
|
33
|
+
loop do
|
34
|
+
written =
|
35
|
+
with_deadline(deadline, exclass) do
|
36
|
+
write_nonblock(data, exception: false)
|
37
|
+
end
|
38
|
+
result += written
|
39
|
+
return result if result >= size
|
40
|
+
data = data.byteslice(written, data.bytesize - written)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ViaWaitMethod
|
45
|
+
private def with_deadline(deadline, exclass)
|
46
|
+
loop do
|
47
|
+
case ret = yield
|
48
|
+
when :wait_writable
|
49
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
50
|
+
raise(exclass) if wait_writable(remaining_time).nil?
|
51
|
+
when :wait_readable
|
52
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
53
|
+
raise(exclass) if wait_readable(remaining_time).nil?
|
54
|
+
else
|
55
|
+
return ret
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ViaSelect
|
62
|
+
private def with_deadline(deadline, exclass)
|
63
|
+
loop do
|
64
|
+
case ret = yield
|
65
|
+
when :wait_writable
|
66
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
67
|
+
raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
|
68
|
+
when :wait_readable
|
69
|
+
raise(exclass) if (remaining_time = deadline - Time.now) <= 0
|
70
|
+
raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
|
71
|
+
else
|
72
|
+
return ret
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private_constant(:ViaWaitMethod, :ViaSelect)
|
79
|
+
end
|
@@ -4,11 +4,11 @@ rescue LoadError
|
|
4
4
|
return
|
5
5
|
end
|
6
6
|
|
7
|
-
require_relative 'mixin/
|
7
|
+
require_relative 'mixin/io_with_deadline'
|
8
8
|
|
9
9
|
class TCPClient
|
10
10
|
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
11
|
-
include
|
11
|
+
include IOWithDeadlineMixin
|
12
12
|
|
13
13
|
def initialize(socket, address, configuration, exception)
|
14
14
|
ssl_params = Hash[configuration.ssl_params]
|
@@ -31,12 +31,13 @@ class TCPClient
|
|
31
31
|
|
32
32
|
def connect_to(address, check, timeout, exception)
|
33
33
|
self.hostname = address.hostname
|
34
|
-
|
34
|
+
timeout = timeout.to_f
|
35
|
+
if timeout.zero?
|
36
|
+
connect
|
37
|
+
else
|
35
38
|
with_deadline(Time.now + timeout, exception) do
|
36
39
|
connect_nonblock(exception: false)
|
37
40
|
end
|
38
|
-
else
|
39
|
-
connect
|
40
41
|
end
|
41
42
|
post_connection_check(address.hostname) if check
|
42
43
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'socket'
|
2
|
-
require_relative 'mixin/
|
2
|
+
require_relative 'mixin/io_with_deadline'
|
3
3
|
|
4
4
|
class TCPClient
|
5
5
|
class TCPSocket < ::Socket
|
6
|
-
include
|
6
|
+
include IOWithDeadlineMixin
|
7
7
|
|
8
8
|
def initialize(address, configuration, exception)
|
9
9
|
super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
|
@@ -19,7 +19,8 @@ class TCPClient
|
|
19
19
|
address.addrinfo.ip_port,
|
20
20
|
address.addrinfo.ip_address
|
21
21
|
)
|
22
|
-
|
22
|
+
timeout = timeout.to_f
|
23
|
+
return connect(addr) if timeout.zero?
|
23
24
|
with_deadline(Time.now + timeout, exception) do
|
24
25
|
connect_nonblock(addr, exception: false)
|
25
26
|
end
|
data/lib/tcp-client/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -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_ssl.rb
CHANGED
@@ -2,20 +2,16 @@ require_relative '../lib/tcp-client'
|
|
2
2
|
|
3
3
|
TCPClient.configure do |cfg|
|
4
4
|
cfg.connect_timeout = 1 # second to connect the server
|
5
|
-
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
6
|
-
cfg.read_timeout = 0.5 # seconds to read some bytes
|
7
5
|
cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
|
8
6
|
end
|
9
7
|
|
10
|
-
# the following request sequence is not allowed to last longer than 2 seconds:
|
11
|
-
# 1 second to connect (incl. SSL handshake etc.)
|
12
|
-
# + 0.25 seconds to write data
|
13
|
-
# + 0.5 seconds to read a response
|
14
|
-
|
15
8
|
TCPClient.open('www.google.com:443') do |client|
|
16
|
-
#
|
17
|
-
|
9
|
+
# query should not last longer than 0.5 seconds
|
10
|
+
client.with_deadline(0.5) do
|
11
|
+
# simple HTTP get request
|
12
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
|
18
13
|
|
19
|
-
|
20
|
-
|
14
|
+
# read "HTTP/1.1 " + 3 byte HTTP status code
|
15
|
+
pp client.read(12)
|
16
|
+
end
|
21
17
|
end
|
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]
|
@@ -13,7 +13,7 @@ class AddressTest < MiniTest::Test
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_create_from_addrinfo
|
16
|
-
addrinfo = Addrinfo.tcp('
|
16
|
+
addrinfo = Addrinfo.tcp('localhost', 42)
|
17
17
|
subject = TCPClient::Address.new(addrinfo)
|
18
18
|
assert_equal(addrinfo.getnameinfo[0], subject.hostname)
|
19
19
|
assert_equal(addrinfo, subject.addrinfo)
|
@@ -54,4 +54,12 @@ class AddressTest < MiniTest::Test
|
|
54
54
|
assert(subject.addrinfo.ip?)
|
55
55
|
assert(subject.addrinfo.ipv6?)
|
56
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
|
57
65
|
end
|
@@ -87,4 +87,12 @@ class ConfigurationTest < MiniTest::Test
|
|
87
87
|
assert_same(42, subject.read_timeout)
|
88
88
|
assert_same(42, subject.write_timeout)
|
89
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
|
90
98
|
end
|
data/test/tcp_client_test.rb
CHANGED
@@ -3,6 +3,8 @@ require_relative 'test_helper'
|
|
3
3
|
class TCPClientTest < MiniTest::Test
|
4
4
|
parallelize_me!
|
5
5
|
|
6
|
+
HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
|
7
|
+
|
6
8
|
attr_reader :config
|
7
9
|
|
8
10
|
def setup
|
@@ -22,9 +24,9 @@ class TCPClientTest < MiniTest::Test
|
|
22
24
|
def create_nonconnected_client
|
23
25
|
client = TCPClient.new
|
24
26
|
client.connect('', config)
|
27
|
+
client
|
25
28
|
rescue Errno::EADDRNOTAVAIL
|
26
|
-
|
27
|
-
return client
|
29
|
+
client
|
28
30
|
end
|
29
31
|
|
30
32
|
def test_failed_state
|
@@ -41,7 +43,7 @@ class TCPClientTest < MiniTest::Test
|
|
41
43
|
end
|
42
44
|
|
43
45
|
def test_connected_state
|
44
|
-
TCPClient.open('localhost:1234') do |subject|
|
46
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
45
47
|
refute(subject.closed?)
|
46
48
|
assert_equal('localhost:1234', subject.to_s)
|
47
49
|
refute_nil(subject.address)
|
@@ -65,7 +67,7 @@ class TCPClientTest < MiniTest::Test
|
|
65
67
|
start_time = Time.now
|
66
68
|
subject.read(42, timeout: timeout)
|
67
69
|
end
|
68
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
70
|
+
assert_in_delta(timeout, Time.now - start_time, 0.11)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
@@ -81,27 +83,63 @@ class TCPClientTest < MiniTest::Test
|
|
81
83
|
start_time = nil
|
82
84
|
assert_raises(TCPClient::WriteTimeoutError) do
|
83
85
|
start_time = Time.now
|
84
|
-
|
85
|
-
# send 1MB to avoid any TCP stack buffering
|
86
|
-
args = Array.new(2024, '?' * 1024)
|
87
|
-
subject.write(*args, timeout: timeout)
|
86
|
+
subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
|
88
87
|
end
|
89
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
88
|
+
assert_in_delta(timeout, Time.now - start_time, 0.11)
|
90
89
|
end
|
91
90
|
end
|
92
91
|
|
93
92
|
def test_write_timeout
|
94
|
-
check_write_timeout(0.
|
93
|
+
check_write_timeout(0.01)
|
95
94
|
check_write_timeout(0.25)
|
96
95
|
end
|
97
96
|
|
97
|
+
def test_write_deadline
|
98
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
99
|
+
refute(subject.closed?)
|
100
|
+
assert_raises(TCPClient::WriteTimeoutError) do
|
101
|
+
subject.with_deadline(0.25) do |*args|
|
102
|
+
assert_equal([subject], args)
|
103
|
+
loop { subject.write('some data here') }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_read_deadline
|
110
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
111
|
+
refute(subject.closed?)
|
112
|
+
assert_raises(TCPClient::ReadTimeoutError) do
|
113
|
+
subject.with_deadline(0.25) do |*args|
|
114
|
+
assert_equal([subject], args)
|
115
|
+
loop { subject.read(0) }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def xtest_read_write_deadline
|
122
|
+
TCPClient.open('localhost:1234', config) do |subject|
|
123
|
+
refute(subject.closed?)
|
124
|
+
assert_raises(TCPClient::TimeoutError) do
|
125
|
+
subject.with_deadline(0.25) do |*args|
|
126
|
+
assert_equal([subject], args)
|
127
|
+
loop do
|
128
|
+
subject.write('HUGE_AMOUNT_OF_DATA')
|
129
|
+
subject.read(0)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
98
136
|
def check_connect_timeout(ssl_config)
|
99
137
|
start_time = nil
|
100
138
|
assert_raises(TCPClient::ConnectTimeoutError) do
|
101
139
|
start_time = Time.now
|
102
140
|
TCPClient.new.connect('localhost:1234', ssl_config)
|
103
141
|
end
|
104
|
-
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.
|
142
|
+
assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
|
105
143
|
end
|
106
144
|
|
107
145
|
def test_connect_ssl_timeout
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
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-02-
|
11
|
+
date: 2021-02-22 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:
|
@@ -71,7 +71,9 @@ files:
|
|
71
71
|
- lib/tcp-client/address.rb
|
72
72
|
- lib/tcp-client/configuration.rb
|
73
73
|
- lib/tcp-client/default_configuration.rb
|
74
|
+
- lib/tcp-client/errors.rb
|
74
75
|
- lib/tcp-client/mixin/io_timeout.rb
|
76
|
+
- lib/tcp-client/mixin/io_with_deadline.rb
|
75
77
|
- lib/tcp-client/ssl_socket.rb
|
76
78
|
- lib/tcp-client/tcp_socket.rb
|
77
79
|
- lib/tcp-client/version.rb
|
@@ -99,12 +101,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
99
101
|
requirements:
|
100
102
|
- - ">="
|
101
103
|
- !ruby/object:Gem::Version
|
102
|
-
version: 2.
|
104
|
+
version: 2.7.0
|
103
105
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
106
|
requirements:
|
105
107
|
- - ">="
|
106
108
|
- !ruby/object:Gem::Version
|
107
|
-
version:
|
109
|
+
version: '0'
|
108
110
|
requirements: []
|
109
111
|
rubygems_version: 3.2.9
|
110
112
|
signing_key:
|