tcp-client 0.5.1 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f36e18e6865766292ff44adf5cd46244e8944049f524553ba4168725454a2d0b
4
- data.tar.gz: abd4ec63f48c1be4e73922aaed7702bdacebe398e6cda13887fcd2efaa62818c
3
+ metadata.gz: 30592a849daf948c39b138e5f2d634761d59328f7a6eab11d45a9b069b8a747d
4
+ data.tar.gz: 39d0b9afe676700627a9b671c974ab5465cdb72e5ed6ec5c009add638a2b02b3
5
5
  SHA512:
6
- metadata.gz: 11b5302d3da2d0d46f35fb2370177916b1044b659b1f62bdbf0a30c39581844dd425487a6c94300b4a7afcf25eafef5828a6ef04cb0ff9bcae4fff01a9a312bb
7
- data.tar.gz: 54b3505c1eb90565e0a80dab5c92a394883b74cf3bb975ed83df4c1affbad15e9acbfcee7ec3d0a5bb79c1486fa46fbfbd958fc9e43106b9a97dc22411c84335
6
+ metadata.gz: d6a44381c618b1a89128499f7256bd9559dd4ea1efc5f34b82b681cfcca29f1c51b7ab47b13b513977e3276e298ec5ad02f6619f5ed36320f724e9cd192f925e
7
+ data.tar.gz: dfbcb8fb0e4b77770023023afbaee354e71ba4e41fe89daff9e197ce7d7a8a785e7891dbb4801651ae4702b56324d3431695e27acc781fafae9e7336db6c2c52
@@ -29,4 +29,6 @@ class TCPClient
29
29
  end
30
30
  end
31
31
  end
32
+
33
+ private_constant(:Deadline)
32
34
  end
@@ -12,6 +12,13 @@ module IOWithDeadlineMixin
12
12
 
13
13
  def read_with_deadline(bytes_to_read, deadline, exception)
14
14
  raise(exception) unless deadline.remaining_time
15
+ if bytes_to_read.nil?
16
+ return(
17
+ with_deadline(deadline, exception) do
18
+ read_nonblock(65_536, exception: false)
19
+ end
20
+ )
21
+ end
15
22
  result = ''.b
16
23
  while result.bytesize < bytes_to_read
17
24
  read =
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
- VERSION = '0.5.1'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/tcp-client.rb CHANGED
@@ -13,12 +13,25 @@ class TCPClient
13
13
  def self.open(address, configuration = Configuration.default)
14
14
  client = new
15
15
  client.connect(Address.new(address), configuration)
16
- return client unless block_given?
17
- begin
18
- yield(client)
19
- ensure
20
- client.close
16
+ block_given? ? yield(client) : client
17
+ ensure
18
+ client.close if block_given?
19
+ end
20
+
21
+ def self.with_deadline(
22
+ timeout,
23
+ address,
24
+ configuration = Configuration.default
25
+ )
26
+ client = nil
27
+ raise(NoBlockGiven) unless block_given?
28
+ address = Address.new(address)
29
+ client = new
30
+ client.with_deadline(timeout) do
31
+ yield(client.connect(address, configuration))
21
32
  end
33
+ ensure
34
+ client&.close
22
35
  end
23
36
 
24
37
  attr_reader :address, :configuration
@@ -31,12 +44,12 @@ class TCPClient
31
44
  @address&.to_s || ''
32
45
  end
33
46
 
34
- def connect(address, configuration, exception: nil)
47
+ def connect(address, configuration, timeout: nil, exception: nil)
48
+ close if @socket
35
49
  raise(NoOpenSSL) if configuration.ssl? && !defined?(SSLSocket)
36
- close
37
50
  @address = Address.new(address)
38
51
  @configuration = configuration.dup.freeze
39
- @socket = create_socket(exception)
52
+ @socket = create_socket(timeout, exception)
40
53
  self
41
54
  end
42
55
 
@@ -63,48 +76,40 @@ class TCPClient
63
76
  @deadline = previous_deadline
64
77
  end
65
78
 
66
- def read(nbytes, timeout: nil, exception: nil)
79
+ def read(nbytes = nil, timeout: nil, exception: nil)
67
80
  raise(NotConnected) if closed?
68
- timeout.nil? && @deadline and
69
- return read_with_deadline(nbytes, @deadline, exception)
70
- deadline = Deadline.new(timeout || @configuration.read_timeout)
81
+ deadline = create_deadline(timeout, configuration.read_timeout)
71
82
  return @socket.read(nbytes) unless deadline.valid?
72
- read_with_deadline(nbytes, deadline, exception)
83
+ exception ||= configuration.read_timeout_error
84
+ @socket.read_with_deadline(nbytes, deadline, exception)
73
85
  end
74
86
 
75
87
  def write(*msg, timeout: nil, exception: nil)
76
88
  raise(NotConnected) if closed?
77
- timeout.nil? && @deadline and
78
- return write_with_deadline(msg, @deadline, exception)
79
- deadline = Deadline.new(timeout || @configuration.read_timeout)
89
+ deadline = create_deadline(timeout, configuration.write_timeout)
80
90
  return @socket.write(*msg) unless deadline.valid?
81
- write_with_deadline(msg, deadline, exception)
91
+ exception ||= configuration.write_timeout_error
92
+ msg.sum do |chunk|
93
+ @socket.write_with_deadline(chunk.b, deadline, exception)
94
+ end
82
95
  end
83
96
 
84
97
  def flush
85
- @socket.flush unless closed?
98
+ @socket&.flush
86
99
  self
87
100
  end
88
101
 
89
102
  private
90
103
 
91
- def create_socket(exception)
92
- exception ||= @configuration.connect_timeout_error
93
- deadline = Deadline.new(@configuration.connect_timeout)
94
- @socket = TCPSocket.new(@address, @configuration, deadline, exception)
95
- return @socket unless @configuration.ssl?
96
- SSLSocket.new(@socket, @address, @configuration, deadline, exception)
97
- end
98
-
99
- def read_with_deadline(nbytes, deadline, exception)
100
- exception ||= @configuration.read_timeout_error
101
- @socket.read_with_deadline(nbytes, deadline, exception)
104
+ def create_deadline(timeout, default)
105
+ timeout.nil? && @deadline ? @deadline : Deadline.new(timeout || default)
102
106
  end
103
107
 
104
- def write_with_deadline(msg, deadline, exception)
105
- exception ||= @configuration.write_timeout_error
106
- msg.sum do |chunk|
107
- @socket.write_with_deadline(chunk.b, deadline, exception)
108
- end
108
+ def create_socket(timeout, exception)
109
+ deadline = create_deadline(timeout, configuration.connect_timeout)
110
+ exception ||= configuration.connect_timeout_error
111
+ @socket = TCPSocket.new(address, configuration, deadline, exception)
112
+ return @socket unless configuration.ssl?
113
+ SSLSocket.new(@socket, address, configuration, deadline, exception)
109
114
  end
110
115
  end
data/sample/google.rb CHANGED
@@ -2,21 +2,20 @@
2
2
 
3
3
  require_relative '../lib/tcp-client'
4
4
 
5
- TCPClient.configure(
6
- connect_timeout: 0.5, # seconds to connect the server
7
- write_timeout: 0.25, # seconds to write a single data junk
8
- read_timeout: 0.5 # seconds to read some bytes
9
- )
10
-
11
- # the following sequence is not allowed to last longer than 1.25 seconds:
12
- # 0.5 seconds to connect
13
- # + 0.25 seconds to write data
14
- # + 0.5 seconds to read a response
5
+ # global configuration.
6
+ # - 0.5 seconds to connect the server
7
+ # - 0.25 seconds to write a single data junk
8
+ # - 0.25 seconds to read some bytes
9
+ TCPClient.configure do |cfg|
10
+ cfg.connect_timeout = 0.5
11
+ cfg.write_timeout = 0.25
12
+ cfg.read_timeout = 0.25
13
+ end
15
14
 
15
+ # request to Google:
16
+ # - send a simple HTTP get request
17
+ # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
16
18
  TCPClient.open('www.google.com:80') do |client|
17
- # simple HTTP get request
18
- pp client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
19
-
20
- # read "HTTP/1.1 " + 3 byte HTTP status code
21
- pp client.read(12)
19
+ p client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
20
+ p client.read(12)
22
21
  end
data/sample/google_ssl.rb CHANGED
@@ -2,18 +2,23 @@
2
2
 
3
3
  require_relative '../lib/tcp-client'
4
4
 
5
- TCPClient.configure do |cfg|
6
- cfg.connect_timeout = 1 # limit connect time the server to 1 second
7
- cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
8
- end
9
-
10
- TCPClient.open('www.google.com:443') do |client|
11
- # next sequence should not last longer than 0.5 seconds
12
- client.with_deadline(0.5) do
13
- # simple HTTP get request
14
- pp client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
5
+ # create a configuration.
6
+ # - use TLS 1.2
7
+ # - don't use internal buffering
8
+ cfg =
9
+ TCPClient::Configuration.create(
10
+ buffered: false,
11
+ ssl_params: {
12
+ ssl_version: :TLSv1_2
13
+ }
14
+ )
15
15
 
16
- # read "HTTP/1.1 " + 3 byte HTTP status code
17
- pp client.read(12)
18
- end
16
+ # request to Google:
17
+ # - limit all interactions to 0.5 seconds
18
+ # - use the Configuration cfg
19
+ # - send a simple HTTP get request
20
+ # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
21
+ TCPClient.with_deadline(0.5, 'www.google.com:443', cfg) do |client|
22
+ p client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
23
+ p client.read(12)
19
24
  end
data/test/helper.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/parallel'
5
+ require_relative '../lib/tcp-client'
6
+
7
+ # this pseudo-server never reads or writes anything
8
+ PseudoServer = TCPServer.new('localhost', 0)
9
+ Minitest.after_run { PseudoServer.close }
10
+
11
+ class Test < MiniTest::Test
12
+ parallelize_me!
13
+ end
14
+
15
+ class Timing
16
+ def initialize
17
+ @start_time = nil
18
+ end
19
+
20
+ def started?
21
+ @start_time != nil
22
+ end
23
+
24
+ def start
25
+ @start_time = now
26
+ end
27
+
28
+ def elapsed
29
+ now - @start_time
30
+ end
31
+
32
+ if defined?(Process::CLOCK_MONOTONIC)
33
+ def now
34
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
35
+ end
36
+ else
37
+ def now
38
+ ::Time.now
39
+ end
40
+ end
41
+ end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../test_helper'
4
-
5
- class AddressTest < MiniTest::Test
6
- parallelize_me!
3
+ require_relative '../helper'
7
4
 
5
+ class AddressTest < Test
8
6
  def test_create_from_integer
9
7
  subject = TCPClient::Address.new(42)
10
8
  assert_equal('localhost:42', subject.to_s)
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../test_helper'
4
-
5
- class ConfigurationTest < MiniTest::Test
6
- parallelize_me!
3
+ require_relative '../helper'
7
4
 
5
+ class ConfigurationTest < Test
8
6
  def test_defaults
9
7
  subject = TCPClient::Configuration.new
10
8
  assert(subject.buffered)
@@ -1,25 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../test_helper'
3
+ require_relative '../helper'
4
4
 
5
- class Deadlineest < MiniTest::Test
6
- parallelize_me!
5
+ class DeadlineTest < Test
6
+ Deadline = TCPClient.const_get(:Deadline)
7
7
 
8
8
  def test_validity
9
- assert(TCPClient::Deadline.new(1).valid?)
10
- assert(TCPClient::Deadline.new(0.0001).valid?)
9
+ assert(Deadline.new(1).valid?)
10
+ assert(Deadline.new(0.0001).valid?)
11
11
 
12
- refute(TCPClient::Deadline.new(0).valid?)
13
- refute(TCPClient::Deadline.new(nil).valid?)
12
+ refute(Deadline.new(0).valid?)
13
+ refute(Deadline.new(nil).valid?)
14
14
  end
15
15
 
16
16
  def test_remaining_time
17
- assert(TCPClient::Deadline.new(1).remaining_time > 0)
17
+ assert(Deadline.new(1).remaining_time > 0)
18
18
 
19
- assert_nil(TCPClient::Deadline.new(0).remaining_time)
20
- assert_nil(TCPClient::Deadline.new(nil).remaining_time)
19
+ assert_nil(Deadline.new(0).remaining_time)
20
+ assert_nil(Deadline.new(nil).remaining_time)
21
21
 
22
- deadline = TCPClient::Deadline.new(0.2)
22
+ deadline = Deadline.new(0.2)
23
23
  sleep(0.2)
24
24
  assert_nil(deadline.remaining_time)
25
25
  end
@@ -1,4 +1,4 @@
1
- require_relative '../test_helper'
1
+ require_relative '../helper'
2
2
 
3
3
  class DefauktConfigurationTest < MiniTest::Test
4
4
  def test_default
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../test_helper'
4
-
5
- class VersionTest < MiniTest::Test
6
- parallelize_me!
3
+ require_relative '../helper'
7
4
 
5
+ class VersionTest < Test
8
6
  def test_format
9
7
  assert_match(/\d+\.\d+\.\d+/, TCPClient::VERSION)
10
8
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'test_helper'
3
+ require_relative 'helper'
4
4
 
5
- class TCPClientTest < MiniTest::Test
6
- parallelize_me!
7
-
8
- HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
5
+ class TCPClientTest < Test
6
+ HUGE_AMOUNT_OF_DATA = Array.new(1024, '?' * 2048).freeze
9
7
 
10
8
  attr_reader :config
11
9
 
@@ -17,6 +15,10 @@ class TCPClientTest < MiniTest::Test
17
15
  PseudoServer.local_address.ip_port
18
16
  end
19
17
 
18
+ def address
19
+ "localhost:#{port}"
20
+ end
21
+
20
22
  def test_defaults
21
23
  subject = TCPClient.new
22
24
  assert(subject.closed?)
@@ -30,7 +32,7 @@ class TCPClientTest < MiniTest::Test
30
32
  def create_nonconnected_client
31
33
  client = TCPClient.new
32
34
  client.connect('', config)
33
- client
35
+ :you_should_not_get_this
34
36
  rescue Errno::EADDRNOTAVAIL
35
37
  client
36
38
  end
@@ -49,12 +51,12 @@ class TCPClientTest < MiniTest::Test
49
51
  end
50
52
 
51
53
  def test_connected_state
52
- TCPClient.open("localhost:#{port}", config) do |subject|
54
+ TCPClient.open(address, config) do |subject|
53
55
  refute(subject.closed?)
54
- assert_equal("localhost:#{port}", subject.to_s)
56
+ assert_equal(address, subject.to_s)
55
57
  refute_nil(subject.address)
56
58
  address_when_opened = subject.address
57
- assert_equal("localhost:#{port}", subject.address.to_s)
59
+ assert_equal(address, subject.address.to_s)
58
60
  assert_equal('localhost', subject.address.hostname)
59
61
  assert_instance_of(Addrinfo, subject.address.addrinfo)
60
62
  assert_same(port, subject.address.addrinfo.ip_port)
@@ -66,14 +68,14 @@ class TCPClientTest < MiniTest::Test
66
68
  end
67
69
 
68
70
  def check_read_timeout(timeout)
69
- TCPClient.open("localhost:#{port}", config) do |subject|
71
+ TCPClient.open(address, config) do |subject|
70
72
  refute(subject.closed?)
71
- start_time = nil
73
+ timing = Timing.new
72
74
  assert_raises(TCPClient::ReadTimeoutError) do
73
- start_time = Time.now
75
+ timing.start
74
76
  subject.read(42, timeout: timeout)
75
77
  end
76
- assert_in_delta(timeout, Time.now - start_time, 0.15)
78
+ assert_in_delta(timeout, timing.elapsed, 0.15)
77
79
  end
78
80
  end
79
81
 
@@ -84,14 +86,14 @@ class TCPClientTest < MiniTest::Test
84
86
  end
85
87
 
86
88
  def check_write_timeout(timeout)
87
- TCPClient.open("localhost:#{port}", config) do |subject|
89
+ TCPClient.open(address, config) do |subject|
88
90
  refute(subject.closed?)
89
- start_time = nil
91
+ timing = Timing.new
90
92
  assert_raises(TCPClient::WriteTimeoutError) do
91
- start_time = Time.now
93
+ timing.start
92
94
  subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
93
95
  end
94
- assert_in_delta(timeout, Time.now - start_time, 0.15)
96
+ assert_in_delta(timeout, timing.elapsed, 0.15)
95
97
  end
96
98
  end
97
99
 
@@ -101,7 +103,7 @@ class TCPClientTest < MiniTest::Test
101
103
  end
102
104
 
103
105
  def test_write_deadline
104
- TCPClient.open("localhost:#{port}", config) do |subject|
106
+ TCPClient.open(address, config) do |subject|
105
107
  refute(subject.closed?)
106
108
  assert_raises(TCPClient::WriteTimeoutError) do
107
109
  subject.with_deadline(0.25) do |*args|
@@ -113,25 +115,25 @@ class TCPClientTest < MiniTest::Test
113
115
  end
114
116
 
115
117
  def test_read_deadline
116
- TCPClient.open("localhost:#{port}", config) do |subject|
118
+ TCPClient.open(address, config) do |subject|
117
119
  refute(subject.closed?)
118
120
  assert_raises(TCPClient::ReadTimeoutError) do
119
121
  subject.with_deadline(0.25) do |*args|
120
122
  assert_equal([subject], args)
121
- loop { subject.read(0) }
123
+ loop { subject.read(42) }
122
124
  end
123
125
  end
124
126
  end
125
127
  end
126
128
 
127
129
  def test_read_write_deadline
128
- TCPClient.open("localhost:#{port}", config) do |subject|
130
+ TCPClient.open(address, config) do |subject|
129
131
  refute(subject.closed?)
130
132
  assert_raises(TCPClient::TimeoutError) do
131
133
  subject.with_deadline(0.25) do |*args|
132
134
  assert_equal([subject], args)
133
135
  loop do
134
- subject.write('HUGE_AMOUNT_OF_DATA')
136
+ subject.write('some data')
135
137
  subject.read(0)
136
138
  end
137
139
  end
@@ -140,12 +142,12 @@ class TCPClientTest < MiniTest::Test
140
142
  end
141
143
 
142
144
  def check_connect_timeout(ssl_config)
143
- start_time = nil
145
+ timing = Timing.new
144
146
  assert_raises(TCPClient::ConnectTimeoutError) do
145
- start_time = Time.now
146
- TCPClient.new.connect("localhost:#{port}", ssl_config)
147
+ timing.start
148
+ TCPClient.new.connect(address, ssl_config)
147
149
  end
148
- assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.25)
150
+ assert_in_delta(ssl_config.connect_timeout, timing.elapsed, 0.25)
149
151
  end
150
152
 
151
153
  def test_connect_ssl_timeout
@@ -160,4 +162,19 @@ class TCPClientTest < MiniTest::Test
160
162
  ssl_config.connect_timeout = 1.5
161
163
  check_connect_timeout(ssl_config)
162
164
  end
165
+
166
+ def test_deadline
167
+ assert(TCPClient.with_deadline(0.15, address, config, &:itself).closed?)
168
+ end
169
+
170
+ def test_deadline_timeout
171
+ timing = Timing.new
172
+ assert_raises(TCPClient::ReadTimeoutError) do
173
+ timing.start
174
+ TCPClient.with_deadline(0.15, address, config) do |client|
175
+ client.read(42)
176
+ end
177
+ end
178
+ assert_in_delta(0.15, timing.elapsed, 0.15)
179
+ end
163
180
  end
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.5.1
4
+ version: 0.6.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-09-10 00:00:00.000000000 Z
11
+ date: 2021-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -86,13 +86,13 @@ files:
86
86
  - sample/google.rb
87
87
  - sample/google_ssl.rb
88
88
  - tcp-client.gemspec
89
+ - test/helper.rb
89
90
  - test/tcp-client/address_test.rb
90
91
  - test/tcp-client/configuration_test.rb
91
92
  - test/tcp-client/deadline_test.rb
92
93
  - test/tcp-client/default_configuration_test.rb
93
94
  - test/tcp-client/version_test.rb
94
95
  - test/tcp_client_test.rb
95
- - test/test_helper.rb
96
96
  homepage: https://github.com/mblumtritt/tcp-client
97
97
  licenses:
98
98
  - BSD-3-Clause
@@ -114,15 +114,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
114
  - !ruby/object:Gem::Version
115
115
  version: '0'
116
116
  requirements: []
117
- rubygems_version: 3.2.22
117
+ rubygems_version: 3.2.28
118
118
  signing_key:
119
119
  specification_version: 4
120
120
  summary: A TCP client implementation with working timeout support.
121
121
  test_files:
122
+ - test/helper.rb
122
123
  - test/tcp-client/address_test.rb
123
124
  - test/tcp-client/configuration_test.rb
124
125
  - test/tcp-client/deadline_test.rb
125
126
  - test/tcp-client/default_configuration_test.rb
126
127
  - test/tcp-client/version_test.rb
127
128
  - test/tcp_client_test.rb
128
- - test/test_helper.rb
data/test/test_helper.rb DELETED
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'minitest/autorun'
4
- require 'minitest/parallel'
5
- require_relative '../lib/tcp-client'
6
-
7
- # this pseudo-server never reads or writes anything
8
- PseudoServer = TCPServer.new('localhost', 0)
9
- Minitest.after_run { PseudoServer.close }