tcp-client 0.2.3 → 0.4.1

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