tcp-client 0.0.2
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 +7 -0
- data/.gitignore +4 -0
- data/README.md +55 -0
- data/gems.rb +3 -0
- data/lib/tcp-client.rb +74 -0
- data/lib/tcp-client/address.rb +50 -0
- data/lib/tcp-client/configuration.rb +68 -0
- data/lib/tcp-client/mixin/io_timeout.rb +92 -0
- data/lib/tcp-client/ssl_socket.rb +35 -0
- data/lib/tcp-client/tcp_socket.rb +32 -0
- data/lib/tcp-client/version.rb +3 -0
- data/lib/tcp_client.rb +1 -0
- data/rakefile.rb +12 -0
- data/sample/google.rb +17 -0
- data/sample/google_ssl.rb +18 -0
- data/tcp-client.gemspec +38 -0
- data/test/tcp-client/configuration_test.rb +45 -0
- data/test/tcp-client/version_test.rb +7 -0
- data/test/tcp_client_test.rb +111 -0
- data/test/test_helper.rb +9 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c78407e818a50dbd68dcee0fe5b5cebcb24c05e8067a85e58d041708d7f17d6d
|
4
|
+
data.tar.gz: 02d404e555b0aeab3536654171563a90c33d6f38a9d00a4931e44915d099667f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 564d3dd03cf6a63997e58cc2cee355f285682af61efda74caed413bfa09b672331de4cd29f1a8434403d128f45175ffbc9ca21818c6031e3a4fb5d15e19476f3
|
7
|
+
data.tar.gz: b563240e1fed9791c4f0714e1fe9d5711441488b482e17e2a37de2e6e0828bf004d15a24db69b7b4ffc1ef2bdc79c936f3de87a4213f9ae4832c1985aa820988
|
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# TCPClient
|
2
|
+
|
3
|
+
A TCP client implementation with working timeout support.
|
4
|
+
|
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`).
|
7
|
+
|
8
|
+
## Sample
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
configuration = TCPClient::Configuration.create do |cfg|
|
12
|
+
cfg.connect_timeout = 1 # second to connect the server
|
13
|
+
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
14
|
+
cfg.read_timeout = 0.25 # seconds to read some bytes
|
15
|
+
cfg.ssl_params = {} # use SSL, but without any specific parameters
|
16
|
+
end
|
17
|
+
|
18
|
+
# the following request sequence is not allowed to last longer than 1.5 seconds:
|
19
|
+
# 1 second to connect (incl. SSL handshake etc.)
|
20
|
+
# + 0.25 seconds to write data
|
21
|
+
# + 0.25 seconds to read
|
22
|
+
# a response
|
23
|
+
TCPClient.open('www.google.com:443', configuration) do |client|
|
24
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n") # simple HTTP get request
|
25
|
+
pp client.read(12) # "HTTP/1.1 " + 3 byte HTTP status code
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### Installation
|
30
|
+
|
31
|
+
Use [Bundler](http://gembundler.com/) to use TCPClient in your own project:
|
32
|
+
|
33
|
+
Add to your `Gemfile`:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gem 'tcp-client'
|
37
|
+
```
|
38
|
+
|
39
|
+
and install it by running Bundler:
|
40
|
+
|
41
|
+
```bash
|
42
|
+
$ bundle
|
43
|
+
```
|
44
|
+
|
45
|
+
To install the gem globally use:
|
46
|
+
|
47
|
+
```bash
|
48
|
+
$ gem install tcp-client
|
49
|
+
```
|
50
|
+
|
51
|
+
After that you need only a single line of code in your project code to have all tools on board:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
require 'tcp-client'
|
55
|
+
```
|
data/gems.rb
ADDED
data/lib/tcp-client.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'tcp-client/address'
|
4
|
+
require_relative 'tcp-client/tcp_socket'
|
5
|
+
require_relative 'tcp-client/ssl_socket'
|
6
|
+
require_relative 'tcp-client/configuration'
|
7
|
+
require_relative 'tcp-client/version'
|
8
|
+
|
9
|
+
class TCPClient
|
10
|
+
class NoOpenSSL < RuntimeError
|
11
|
+
def self.raise!
|
12
|
+
raise(self, 'OpenSSL is not avail', caller(1))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class NotConnected < SocketError
|
17
|
+
def self.raise!(which)
|
18
|
+
raise(self, format('client not connected - %s', which), caller(1))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.open(addr, configuration = Configuration.new)
|
23
|
+
addr = Address.new(addr)
|
24
|
+
client = new
|
25
|
+
client.connect(addr, configuration)
|
26
|
+
return yield(client) if block_given?
|
27
|
+
client, ret = nil, client
|
28
|
+
ret
|
29
|
+
ensure
|
30
|
+
client.close if client
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :address
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@socket = @address = @write_timeout = @read_timeout = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
@address ? @address.to_s : ''
|
41
|
+
end
|
42
|
+
|
43
|
+
def connect(addr, configuration)
|
44
|
+
close
|
45
|
+
NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
|
46
|
+
@address = Address.new(addr)
|
47
|
+
@socket = TCPSocket.new(@address, configuration)
|
48
|
+
@socket = SSLSocket.new(@socket, @address, configuration) if configuration.ssl?
|
49
|
+
@write_timeout = configuration.write_timeout
|
50
|
+
@read_timeout = configuration.read_timeout
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def close
|
55
|
+
socket, @socket = @socket, nil
|
56
|
+
socket.close if socket
|
57
|
+
self
|
58
|
+
rescue IOError
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def closed?
|
63
|
+
@socket.nil? || @socket.closed?
|
64
|
+
end
|
65
|
+
|
66
|
+
def read(nbytes, timeout: @read_timeout)
|
67
|
+
closed? ? NotConnected.raise!(self) : @socket.read(nbytes, timeout: timeout)
|
68
|
+
end
|
69
|
+
|
70
|
+
def write(*args, timeout: @write_timeout)
|
71
|
+
closed? ? NotConnected.raise!(self) : @socket.write(*args, timeout: timeout)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class TCPClient
|
6
|
+
class Address
|
7
|
+
attr_reader :to_s, :hostname, :addrinfo
|
8
|
+
|
9
|
+
def initialize(addr)
|
10
|
+
case addr
|
11
|
+
when self.class
|
12
|
+
init_from_selfclass(addr)
|
13
|
+
when Integer
|
14
|
+
init_from_addrinfo(Addrinfo.tcp(nil, addr))
|
15
|
+
when Addrinfo
|
16
|
+
init_from_addrinfo(add)
|
17
|
+
else
|
18
|
+
init_from_string(addr)
|
19
|
+
end
|
20
|
+
@addrinfo.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def init_from_string(str)
|
26
|
+
@hostname, port = from_string(str.to_s)
|
27
|
+
@addrinfo = Addrinfo.tcp(@hostname, port)
|
28
|
+
@to_s = "#{@hostname}:#{port}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def init_from_selfclass(address)
|
32
|
+
@to_s = address.to_s
|
33
|
+
@hostname = address.hostname
|
34
|
+
@addrinfo = address.addrinfo
|
35
|
+
end
|
36
|
+
|
37
|
+
def init_from_addrinfo(addrinfo)
|
38
|
+
@hostname, port = addrinfo.getnameinfo(Socket::NI_NUMERICSERV)
|
39
|
+
@to_s = "#{@hostname}:#{port}"
|
40
|
+
@addrinfo = addrinfo
|
41
|
+
end
|
42
|
+
|
43
|
+
def from_string(str)
|
44
|
+
return [nil, str.to_i] unless idx = str.rindex(':')
|
45
|
+
name = str[0, idx]
|
46
|
+
name = name[1, name.size - 2] if name[0] == '[' && name[-1] == ']'
|
47
|
+
[name, str[idx + 1, str.size - idx].to_i]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class TCPClient
|
2
|
+
class Configuration
|
3
|
+
def self.create
|
4
|
+
ret = new
|
5
|
+
yield(ret) if block_given?
|
6
|
+
ret
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :buffered, :keep_alive, :reverse_lookup
|
10
|
+
attr_accessor :ssl_params
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@buffered = @keep_alive = @reverse_lookup = true
|
14
|
+
self.timeout = @ssl_params = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def ssl?
|
18
|
+
@ssl_params ? true : false
|
19
|
+
end
|
20
|
+
|
21
|
+
def buffered=(yn)
|
22
|
+
@buffered = yn ? true : false
|
23
|
+
end
|
24
|
+
|
25
|
+
def keep_alive=(yn)
|
26
|
+
@keep_alive = yn ? true : false
|
27
|
+
end
|
28
|
+
|
29
|
+
def reverse_lookup=(yn)
|
30
|
+
@reverse_lookup = yn ? true : false
|
31
|
+
end
|
32
|
+
|
33
|
+
def timeout=(seconds)
|
34
|
+
@timeout = seconds(seconds)
|
35
|
+
@connect_timeout = @write_timeout = @read_timeout = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def connect_timeout
|
39
|
+
@connect_timeout || @timeout
|
40
|
+
end
|
41
|
+
|
42
|
+
def connect_timeout=(seconds)
|
43
|
+
@connect_timeout = seconds(seconds)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_timeout
|
47
|
+
@write_timeout || @timeout
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_timeout=(seconds)
|
51
|
+
@write_timeout = seconds(seconds)
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_timeout
|
55
|
+
@read_timeout || @timeout
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_timeout=(seconds)
|
59
|
+
@read_timeout = seconds(seconds)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def seconds(value)
|
65
|
+
value && value > 0 ? value : nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
|
2
|
+
|
3
|
+
module IOTimeoutMixin
|
4
|
+
def self.included(mod)
|
5
|
+
im = mod.instance_methods
|
6
|
+
mod.include(im.index(:wait_writable) && im.index(:wait_readable) ? WithDeadlineMethods : WidthDeadlineIO)
|
7
|
+
end
|
8
|
+
|
9
|
+
def read(nbytes, timeout: nil)
|
10
|
+
timeout = timeout.to_f
|
11
|
+
return read_all(nbytes){ |junk_size| super(junk_size) } if timeout <= 0
|
12
|
+
deadline = Time.now + timeout
|
13
|
+
read_all(nbytes) do |junk_size|
|
14
|
+
with_deadline(deadline){ read_nonblock(junk_size, exception: false) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(*args, timeout: nil)
|
19
|
+
timeout = timeout.to_f
|
20
|
+
return write_all(args.join){ |junk| super(junk) } if timeout <= 0
|
21
|
+
deadline = Time.now + timeout
|
22
|
+
write_all(args.join) do |junk|
|
23
|
+
with_deadline(deadline){ write_nonblock(junk, exception: false) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def read_all(nbytes)
|
30
|
+
return '' if nbytes == 0
|
31
|
+
result = ''
|
32
|
+
loop do
|
33
|
+
unless read = yield(nbytes - result.bytesize)
|
34
|
+
close
|
35
|
+
return result
|
36
|
+
end
|
37
|
+
result += read
|
38
|
+
return result if result.bytesize >= nbytes
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_all(data)
|
43
|
+
return 0 if 0 == (size = data.bytesize)
|
44
|
+
result = 0
|
45
|
+
loop do
|
46
|
+
written = yield(data)
|
47
|
+
result += written
|
48
|
+
return result if result >= size
|
49
|
+
data = data.byteslice(written, data.bytesize - written)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module WithDeadlineMethods
|
54
|
+
private
|
55
|
+
|
56
|
+
def with_deadline(deadline)
|
57
|
+
loop do
|
58
|
+
case ret = yield
|
59
|
+
when :wait_writable
|
60
|
+
remaining_time = deadline - Time.now
|
61
|
+
raise(IOTimeoutError) if remaining_time <= 0 || wait_writable(remaining_time).nil?
|
62
|
+
when :wait_readable
|
63
|
+
remaining_time = deadline - Time.now
|
64
|
+
raise(IOTimeoutError) if remaining_time <= 0 || wait_readable(remaining_time).nil?
|
65
|
+
else
|
66
|
+
return ret
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module WidthDeadlineIO
|
73
|
+
private
|
74
|
+
|
75
|
+
def with_deadline(deadline)
|
76
|
+
loop do
|
77
|
+
case ret = yield
|
78
|
+
when :wait_writable
|
79
|
+
remaining_time = deadline - Time.now
|
80
|
+
raise(IOTimeoutError) if remaining_time <= 0 || ::IO.select(nil, [self], nil, remaining_time).nil?
|
81
|
+
when :wait_readable
|
82
|
+
remaining_time = deadline - Time.now
|
83
|
+
raise(IOTimeoutError) if remaining_time <= 0 || ::IO.select([self], nil, nil, remaining_time).nil?
|
84
|
+
else
|
85
|
+
return ret
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private_constant :WithDeadlineMethods, :WidthDeadlineIO
|
92
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
begin
|
2
|
+
require 'openssl'
|
3
|
+
rescue LoadError
|
4
|
+
return
|
5
|
+
end
|
6
|
+
|
7
|
+
require_relative 'mixin/io_timeout'
|
8
|
+
|
9
|
+
class TCPClient
|
10
|
+
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
11
|
+
include IOTimeoutMixin
|
12
|
+
|
13
|
+
def initialize(socket, address, configuration)
|
14
|
+
ssl_params = Hash[configuration.ssl_params]
|
15
|
+
super(socket, create_context(ssl_params))
|
16
|
+
connect_to(address, configuration.connect_timeout)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_context(ssl_params)
|
22
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
23
|
+
ctx.set_params(ssl_params)
|
24
|
+
ctx
|
25
|
+
end
|
26
|
+
|
27
|
+
def connect_to(address, timeout)
|
28
|
+
self.hostname = address.hostname
|
29
|
+
timeout ? with_deadline(Time.now + timeout){ connect_nonblock(exception: false) } : connect
|
30
|
+
post_connection_check(address.hostname)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private_constant :SSLSocket
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'mixin/io_timeout'
|
3
|
+
|
4
|
+
class TCPClient
|
5
|
+
class TCPSocket < ::Socket
|
6
|
+
include IOTimeoutMixin
|
7
|
+
|
8
|
+
def initialize(address, configuration)
|
9
|
+
super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
|
10
|
+
configure(configuration)
|
11
|
+
connect_to(address, configuration.connect_timeout)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def connect_to(address, timeout)
|
17
|
+
addr = ::Socket.pack_sockaddr_in(address.addrinfo.ip_port, address.addrinfo.ip_address)
|
18
|
+
timeout ? with_deadline(Time.now + timeout){ connect_nonblock(addr, exception: false) } : connect(addr)
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure(configuration)
|
22
|
+
unless configuration.buffered
|
23
|
+
self.sync = true
|
24
|
+
setsockopt(:TCP, :NODELAY, 1)
|
25
|
+
end
|
26
|
+
setsockopt(:SOCKET, :KEEPALIVE, configuration.keep_alive ? 1 : 0)
|
27
|
+
self.do_not_reverse_lookup = configuration.reverse_lookup
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private_constant :TCPSocket
|
32
|
+
end
|
data/lib/tcp_client.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'tcp-client'
|
data/rakefile.rb
ADDED
data/sample/google.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative '../lib/tcp-client'
|
2
|
+
|
3
|
+
configuration = TCPClient::Configuration.create do |cfg|
|
4
|
+
cfg.connect_timeout = 0.5 # seconds to connect the server
|
5
|
+
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
6
|
+
cfg.read_timeout = 0.25 # seconds to read some bytes
|
7
|
+
end
|
8
|
+
|
9
|
+
# the following request sequence is not allowed to last longer than 0.75 seconds:
|
10
|
+
# 0.5 seconds to connect
|
11
|
+
# + 0.25 seconds to write data
|
12
|
+
# + 0.25 seconds to read
|
13
|
+
# a response
|
14
|
+
TCPClient.open('www.google.com:80', configuration) do |client|
|
15
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n") # simple HTTP get request
|
16
|
+
pp client.read(12) # "HTTP/1.1 " + 3 byte HTTP status code
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../lib/tcp-client'
|
2
|
+
|
3
|
+
configuration = TCPClient::Configuration.create do |cfg|
|
4
|
+
cfg.connect_timeout = 1 # second to connect the server
|
5
|
+
cfg.write_timeout = 0.25 # seconds to write a single data junk
|
6
|
+
cfg.read_timeout = 0.25 # seconds to read some bytes
|
7
|
+
cfg.ssl_params = {} # use SSL, but without any specific parameters
|
8
|
+
end
|
9
|
+
|
10
|
+
# the following request sequence is not allowed to last longer than 1.5 seconds:
|
11
|
+
# 1 second to connect (incl. SSL handshake etc.)
|
12
|
+
# + 0.25 seconds to write data
|
13
|
+
# + 0.25 seconds to read
|
14
|
+
# a response
|
15
|
+
TCPClient.open('www.google.com:443', configuration) do |client|
|
16
|
+
pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n") # simple HTTP get request
|
17
|
+
pp client.read(12) # "HTTP/1.1 " + 3 byte HTTP status code
|
18
|
+
end
|
data/tcp-client.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../lib/tcp-client/version', __FILE__)
|
4
|
+
|
5
|
+
GemSpec = Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'tcp-client'
|
7
|
+
spec.version = TCPClient::VERSION
|
8
|
+
spec.summary = 'A TCP client implementation with working timeout support.'
|
9
|
+
spec.description = <<~EOS
|
10
|
+
This gem implements a TCP client with (optional) SSL support. The
|
11
|
+
motivation of this project is the need to have a _really working_
|
12
|
+
easy to use client which can handle time limits correctly. Unlike
|
13
|
+
other implementations this client respects given/configurable time
|
14
|
+
limits for each method (`connect`, `read`, `write`).
|
15
|
+
EOS
|
16
|
+
spec.author = 'Mike Blumtritt'
|
17
|
+
spec.email = 'mike.blumtritt@invision.de'
|
18
|
+
spec.homepage = 'https://github.com/mblumtritt/tcp-client'
|
19
|
+
spec.metadata = {'issue_tracker' => 'https://github.com/mblumtritt/tcp-client/issues'}
|
20
|
+
spec.rubyforge_project = spec.name
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'minitest'
|
25
|
+
|
26
|
+
spec.platform = Gem::Platform::RUBY
|
27
|
+
spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
|
28
|
+
spec.required_ruby_version = '>= 2.0.0'
|
29
|
+
|
30
|
+
spec.require_paths = %w[lib]
|
31
|
+
|
32
|
+
all_files = %x(git ls-files -z).split(0.chr)
|
33
|
+
spec.test_files = all_files.grep(%r{^(spec|test)/})
|
34
|
+
spec.files = all_files - spec.test_files
|
35
|
+
|
36
|
+
spec.has_rdoc = false
|
37
|
+
spec.extra_rdoc_files = %w[README.md]
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
|
3
|
+
class ConfigurationTest < Test
|
4
|
+
def test_defaults
|
5
|
+
subject = TCPClient::Configuration.new
|
6
|
+
assert(subject.buffered)
|
7
|
+
assert(subject.keep_alive)
|
8
|
+
assert(subject.reverse_lookup)
|
9
|
+
refute(subject.ssl?)
|
10
|
+
assert_nil(subject.connect_timeout)
|
11
|
+
assert_nil(subject.read_timeout)
|
12
|
+
assert_nil(subject.write_timeout)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_configure
|
16
|
+
subject = TCPClient::Configuration.create do |cfg|
|
17
|
+
cfg.buffered = cfg.keep_alive = cfg.reverse_lookup = false
|
18
|
+
cfg.timeout = 42
|
19
|
+
cfg.ssl_params = {}
|
20
|
+
end
|
21
|
+
refute(subject.buffered)
|
22
|
+
refute(subject.keep_alive)
|
23
|
+
refute(subject.reverse_lookup)
|
24
|
+
assert_same(42, subject.connect_timeout)
|
25
|
+
assert_same(42, subject.read_timeout)
|
26
|
+
assert_same(42, subject.write_timeout)
|
27
|
+
assert(subject.ssl?)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_timeout_overwrite
|
31
|
+
subject = TCPClient::Configuration.create do |cfg|
|
32
|
+
cfg.connect_timeout = 1
|
33
|
+
cfg.read_timeout = 2
|
34
|
+
cfg.write_timeout = 3
|
35
|
+
end
|
36
|
+
assert_same(1, subject.connect_timeout)
|
37
|
+
assert_same(2, subject.read_timeout)
|
38
|
+
assert_same(3, subject.write_timeout)
|
39
|
+
|
40
|
+
subject.timeout = 42
|
41
|
+
assert_same(42, subject.connect_timeout)
|
42
|
+
assert_same(42, subject.read_timeout)
|
43
|
+
assert_same(42, subject.write_timeout)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class TCPClientTest < Test
|
4
|
+
def test_defaults
|
5
|
+
subject = TCPClient.new
|
6
|
+
assert(subject.closed?)
|
7
|
+
assert_equal('', subject.to_s)
|
8
|
+
assert_nil(subject.address)
|
9
|
+
subject.close
|
10
|
+
assert_raises(TCPClient::NotConnected) do
|
11
|
+
subject.write('hello world!')
|
12
|
+
end
|
13
|
+
assert_raises(TCPClient::NotConnected) do
|
14
|
+
subject.read(42)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_nonconnected_client
|
19
|
+
client = TCPClient.new
|
20
|
+
client.connect('', TCPClient::Configuration.new)
|
21
|
+
rescue Errno::EADDRNOTAVAIL
|
22
|
+
ensure
|
23
|
+
return client
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_failed_state
|
27
|
+
subject = create_nonconnected_client
|
28
|
+
assert(subject.closed?)
|
29
|
+
assert_equal(':0', subject.to_s)
|
30
|
+
refute_nil(subject.address)
|
31
|
+
assert_equal(':0', subject.address.to_s)
|
32
|
+
assert_nil(subject.address.hostname)
|
33
|
+
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
34
|
+
assert_same(0, subject.address.addrinfo.ip_port)
|
35
|
+
assert_raises(TCPClient::NotConnected) do
|
36
|
+
subject.write('hello world!')
|
37
|
+
end
|
38
|
+
assert_raises(TCPClient::NotConnected) do
|
39
|
+
subject.read(42)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_connected_state
|
44
|
+
server = TCPServer.new(1234)
|
45
|
+
TCPClient.open('localhost:1234', TCPClient::Configuration.new) do |subject|
|
46
|
+
refute(subject.closed?)
|
47
|
+
assert_equal('localhost:1234', subject.to_s)
|
48
|
+
refute_nil(subject.address)
|
49
|
+
address_when_opened = subject.address
|
50
|
+
assert_equal('localhost:1234', subject.address.to_s)
|
51
|
+
assert_equal('localhost', subject.address.hostname)
|
52
|
+
assert_instance_of(Addrinfo, subject.address.addrinfo)
|
53
|
+
assert_same(1234, subject.address.addrinfo.ip_port)
|
54
|
+
|
55
|
+
subject.close
|
56
|
+
assert(subject.closed?)
|
57
|
+
assert_same(address_when_opened, subject.address)
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
server.close if server
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_read_write_timeout(addr, timeout)
|
64
|
+
TCPClient.open(addr) do |subject|
|
65
|
+
refute(subject.closed?)
|
66
|
+
start_time = nil
|
67
|
+
assert_raises(IOTimeoutError) do
|
68
|
+
start_time = Time.now
|
69
|
+
# we need to send 1MB to avoid any TCP stack buffering
|
70
|
+
subject.write('?' * (1024 * 1024), timeout: timeout)
|
71
|
+
end
|
72
|
+
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
73
|
+
assert_raises(IOTimeoutError) do
|
74
|
+
start_time = Time.now
|
75
|
+
subject.read(42, timeout: timeout)
|
76
|
+
end
|
77
|
+
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_read_write_timeout
|
82
|
+
server = TCPServer.new(1235) # this server will never read/write client data
|
83
|
+
[0.5, 1, 1.5].each do |timeout|
|
84
|
+
check_read_write_timeout(':1235', timeout)
|
85
|
+
end
|
86
|
+
ensure
|
87
|
+
server.close if server
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_connect_timeout(addr, config, timeout)
|
91
|
+
start_time = nil
|
92
|
+
assert_raises(IOTimeoutError) do
|
93
|
+
start_time = Time.now
|
94
|
+
TCPClient.new.connect(addr, config)
|
95
|
+
end
|
96
|
+
assert_in_delta(timeout, Time.now - start_time, 0.02)
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_connect_ssl_timeout
|
100
|
+
server = TCPServer.new(1236)
|
101
|
+
config = TCPClient::Configuration.create do |cfg|
|
102
|
+
cfg.ssl_params = {}
|
103
|
+
end
|
104
|
+
[0.5, 1, 1.5].each do |timeout|
|
105
|
+
config.timeout = timeout
|
106
|
+
check_connect_timeout('localhost:1236', config, timeout)
|
107
|
+
end
|
108
|
+
ensure
|
109
|
+
server.close if server
|
110
|
+
end
|
111
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tcp-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Blumtritt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
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`).
|
61
|
+
email: mike.blumtritt@invision.de
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files:
|
65
|
+
- README.md
|
66
|
+
files:
|
67
|
+
- ".gitignore"
|
68
|
+
- README.md
|
69
|
+
- gems.rb
|
70
|
+
- lib/tcp-client.rb
|
71
|
+
- lib/tcp-client/address.rb
|
72
|
+
- lib/tcp-client/configuration.rb
|
73
|
+
- lib/tcp-client/mixin/io_timeout.rb
|
74
|
+
- lib/tcp-client/ssl_socket.rb
|
75
|
+
- lib/tcp-client/tcp_socket.rb
|
76
|
+
- lib/tcp-client/version.rb
|
77
|
+
- lib/tcp_client.rb
|
78
|
+
- rakefile.rb
|
79
|
+
- sample/google.rb
|
80
|
+
- sample/google_ssl.rb
|
81
|
+
- tcp-client.gemspec
|
82
|
+
- test/tcp-client/configuration_test.rb
|
83
|
+
- test/tcp-client/version_test.rb
|
84
|
+
- test/tcp_client_test.rb
|
85
|
+
- test/test_helper.rb
|
86
|
+
homepage: https://github.com/mblumtritt/tcp-client
|
87
|
+
licenses: []
|
88
|
+
metadata:
|
89
|
+
issue_tracker: https://github.com/mblumtritt/tcp-client/issues
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 2.0.0
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.3.6
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project: tcp-client
|
106
|
+
rubygems_version: 2.7.3
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: A TCP client implementation with working timeout support.
|
110
|
+
test_files:
|
111
|
+
- test/tcp-client/configuration_test.rb
|
112
|
+
- test/tcp-client/version_test.rb
|
113
|
+
- test/tcp_client_test.rb
|
114
|
+
- test/test_helper.rb
|