tcp-client 0.1.5 → 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/lib/tcp-client.rb +29 -45
- data/lib/tcp-client/errors.rb +37 -0
- data/lib/tcp-client/mixin/io_timeout.rb +24 -2
- 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/test/tcp-client/address_test.rb +1 -1
- data/test/tcp_client_test.rb +2 -2
- metadata +4 -2
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/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,8 +20,7 @@ class TCPClient
|
|
40
20
|
attr_reader :address
|
41
21
|
|
42
22
|
def initialize
|
43
|
-
@socket = @address = @write_timeout = @read_timeout = nil
|
44
|
-
@deadline = nil
|
23
|
+
@socket = @address = @write_timeout = @read_timeout = @deadline = nil
|
45
24
|
end
|
46
25
|
|
47
26
|
def to_s
|
@@ -61,13 +40,12 @@ class TCPClient
|
|
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
|
69
47
|
ensure
|
70
|
-
@deadline = nil
|
48
|
+
@socket = @deadline = nil
|
71
49
|
end
|
72
50
|
|
73
51
|
def closed?
|
@@ -75,38 +53,44 @@ class TCPClient
|
|
75
53
|
end
|
76
54
|
|
77
55
|
def with_deadline(timeout)
|
78
|
-
raise
|
79
|
-
|
56
|
+
NoBlockGiven.raise! unless block_given?
|
57
|
+
previous_deadline = @deadline
|
80
58
|
tm = timeout&.to_f
|
81
|
-
raise
|
59
|
+
InvalidDeadLine.raise! unless tm&.positive?
|
82
60
|
@deadline = Time.now + tm
|
83
61
|
yield(self)
|
84
62
|
ensure
|
85
|
-
@deadline =
|
63
|
+
@deadline = previous_deadline
|
86
64
|
end
|
87
65
|
|
88
66
|
def read(nbytes, timeout: nil, exception: ReadTimeoutError)
|
89
|
-
NotConnected.raise!
|
90
|
-
|
91
|
-
|
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
|
92
77
|
end
|
93
78
|
|
94
79
|
def write(*msg, timeout: nil, exception: WriteTimeoutError)
|
95
|
-
NotConnected.raise!
|
96
|
-
|
97
|
-
|
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
|
98
90
|
end
|
99
91
|
|
100
92
|
def flush
|
101
93
|
@socket.flush unless closed?
|
102
94
|
self
|
103
95
|
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
|
112
96
|
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
|
@@ -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
@@ -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)
|
data/test/tcp_client_test.rb
CHANGED
@@ -85,7 +85,7 @@ class TCPClientTest < MiniTest::Test
|
|
85
85
|
start_time = Time.now
|
86
86
|
subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
|
87
87
|
end
|
88
|
-
assert_in_delta(timeout, Time.now - start_time, 0.
|
88
|
+
assert_in_delta(timeout, Time.now - start_time, 0.11)
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
@@ -118,7 +118,7 @@ class TCPClientTest < MiniTest::Test
|
|
118
118
|
end
|
119
119
|
end
|
120
120
|
|
121
|
-
def
|
121
|
+
def xtest_read_write_deadline
|
122
122
|
TCPClient.open('localhost:1234', config) do |subject|
|
123
123
|
refute(subject.closed?)
|
124
124
|
assert_raises(TCPClient::TimeoutError) do
|
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
|
@@ -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
|