tcp-client 0.2.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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