tcp-client 0.5.1 → 0.9.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.
@@ -1,70 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
- class NoOpenSSL < RuntimeError
4
+ #
5
+ # Raised when a SSL connection should be establshed but the OpenSSL gem is not available.
6
+ #
7
+ class NoOpenSSLError < RuntimeError
5
8
  def initialize
6
9
  super('OpenSSL is not available')
7
10
  end
8
11
  end
9
12
 
10
- class NoBlockGiven < ArgumentError
13
+ #
14
+ # Raised when a method requires a callback block but no such block is specified.
15
+ #
16
+ class NoBlockGivenError < ArgumentError
11
17
  def initialize
12
18
  super('no block given')
13
19
  end
14
20
  end
15
21
 
16
- class InvalidDeadLine < ArgumentError
22
+ #
23
+ # Raised when a an invalid timeout value was specified.
24
+ #
25
+ class InvalidDeadLineError < ArgumentError
17
26
  def initialize(timeout)
18
27
  super("invalid deadline - #{timeout}")
19
28
  end
20
29
  end
21
30
 
22
- class UnknownAttribute < ArgumentError
31
+ #
32
+ # Raised by {Configuration} when an undefined attribute should be set.
33
+ #
34
+ class UnknownAttributeError < ArgumentError
23
35
  def initialize(attribute)
24
36
  super("unknown attribute - #{attribute}")
25
37
  end
26
38
  end
27
39
 
28
- class NotAnException < TypeError
40
+ #
41
+ # Raised when a given timeout exception parameter is not an exception class.
42
+ #
43
+ class NotAnExceptionError < TypeError
29
44
  def initialize(object)
30
45
  super("exception class required - #{object.inspect}")
31
46
  end
32
47
  end
33
48
 
34
- class NotConnected < IOError
49
+ #
50
+ # Base exception class for all network related errors.
51
+ #
52
+ # Will be raised for any system level network error when {Configuration.normalize_network_errors} is configured.
53
+ #
54
+ # You should catch this exception class when you like to handle any relevant {TCPClient} error.
55
+ #
56
+ class NetworkError < StandardError
57
+ end
58
+
59
+ #
60
+ # Raised when a {TCPClient} instance should read/write from/to the network but is not connected.
61
+ #
62
+ class NotConnectedError < NetworkError
35
63
  def initialize
36
64
  super('client not connected')
37
65
  end
38
66
  end
39
67
 
40
- class TimeoutError < IOError
68
+ #
69
+ # Base exception class for a detected timeout.
70
+ #
71
+ # You should catch this exception class when you like to handle any timeout error.
72
+ #
73
+ class TimeoutError < NetworkError
74
+ #
75
+ # Initializes the instance with an optional message.
76
+ #
77
+ # the message will be generated from {#action} when not specified.
78
+ # @overload initialize
79
+ # @overload initialize(message)
80
+ #
81
+ # @param message [String, #to_s] the error message
82
+ #
41
83
  def initialize(message = nil)
42
84
  super(message || "unable to #{action} in time")
43
85
  end
44
86
 
87
+ #
88
+ # @return [Symbol] the action which timed out
89
+ #
45
90
  def action
46
91
  :process
47
92
  end
48
93
  end
49
94
 
95
+ #
96
+ # Raised by default whenever a {TCPClient.connect} timed out.
97
+ #
50
98
  class ConnectTimeoutError < TimeoutError
99
+ #
100
+ # @return [Symbol] the action which timed out: +:connect+
101
+ #
51
102
  def action
52
103
  :connect
53
104
  end
54
105
  end
55
106
 
107
+ #
108
+ # Raised by default whenever a {TCPClient.read} timed out.
109
+ #
56
110
  class ReadTimeoutError < TimeoutError
111
+ #
112
+ # @return [Symbol] the action which timed out: +:read+
113
+ #
57
114
  def action
58
115
  :read
59
116
  end
60
117
  end
61
118
 
119
+ #
120
+ # Raised by default whenever a {TCPClient.write} timed out.
121
+ #
62
122
  class WriteTimeoutError < TimeoutError
123
+ #
124
+ # @return [Symbol] the action which timed out: +:write+
125
+ #
63
126
  def action
64
127
  :write
65
128
  end
66
129
  end
67
130
 
68
- Timeout = TimeoutError # backward compatibility
69
- deprecate_constant(:Timeout)
131
+ NoOpenSSL = NoOpenSSLError # @!visibility private
132
+ NoBlockGiven = NoBlockGivenError # @!visibility private
133
+ InvalidDeadLine = InvalidDeadLineError # @!visibility private
134
+ UnknownAttribute = UnknownAttributeError # @!visibility private
135
+ NotAnException = NotAnExceptionError # @!visibility private
136
+ NotConnected = NotConnectedError # @!visibility private
137
+ deprecate_constant(
138
+ :NoOpenSSL,
139
+ :NoBlockGiven,
140
+ :InvalidDeadLine,
141
+ :UnknownAttribute,
142
+ :NotAnException,
143
+ :NotConnected
144
+ )
70
145
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module IOWithDeadlineMixin
3
+ # @!visibility private
4
+ module IOWithDeadlineMixin # :nodoc:
4
5
  def self.included(mod)
5
6
  methods = mod.instance_methods
6
7
  if methods.index(:wait_writable) && methods.index(:wait_readable)
7
8
  mod.include(ViaWaitMethod)
9
+ elsif methods.index(:to_io)
10
+ mod.include(ViaIOWaitMethod)
8
11
  else
9
12
  mod.include(ViaSelect)
10
13
  end
@@ -12,6 +15,13 @@ module IOWithDeadlineMixin
12
15
 
13
16
  def read_with_deadline(bytes_to_read, deadline, exception)
14
17
  raise(exception) unless deadline.remaining_time
18
+ if bytes_to_read.nil?
19
+ return(
20
+ with_deadline(deadline, exception) do
21
+ read_nonblock(65_536, exception: false)
22
+ end
23
+ )
24
+ end
15
25
  result = ''.b
16
26
  while result.bytesize < bytes_to_read
17
27
  read =
@@ -59,6 +69,25 @@ module IOWithDeadlineMixin
59
69
  end
60
70
  end
61
71
 
72
+ module ViaIOWaitMethod
73
+ private def with_deadline(deadline, exception)
74
+ loop do
75
+ case ret = yield
76
+ when :wait_writable
77
+ remaining_time = deadline.remaining_time or raise(exception)
78
+ raise(exception) if to_io.wait_writable(remaining_time).nil?
79
+ when :wait_readable
80
+ remaining_time = deadline.remaining_time or raise(exception)
81
+ raise(exception) if to_io.wait_readable(remaining_time).nil?
82
+ else
83
+ return ret
84
+ end
85
+ end
86
+ rescue Errno::ETIMEDOUT
87
+ raise(exception)
88
+ end
89
+ end
90
+
62
91
  module ViaSelect
63
92
  private def with_deadline(deadline, exception)
64
93
  loop do
@@ -78,5 +107,5 @@ module IOWithDeadlineMixin
78
107
  end
79
108
  end
80
109
 
81
- private_constant(:ViaWaitMethod, :ViaSelect)
110
+ private_constant(:ViaWaitMethod, :ViaIOWaitMethod, :ViaSelect)
82
111
  end
@@ -18,6 +18,7 @@ class TCPClient
18
18
  super(socket, create_context(ssl_params))
19
19
  self.sync_close = true
20
20
  self.hostname = address.hostname
21
+ check_new_session if @new_session
21
22
  deadline.valid? ? connect_with_deadline(deadline, exception) : connect
22
23
  post_connection_check(address.hostname) if should_verify?(ssl_params)
23
24
  end
@@ -25,7 +26,19 @@ class TCPClient
25
26
  private
26
27
 
27
28
  def create_context(ssl_params)
28
- OpenSSL::SSL::SSLContext.new.tap { |ctx| ctx.set_params(ssl_params) }
29
+ @new_session = nil
30
+ ::OpenSSL::SSL::SSLContext.new.tap do |ctx|
31
+ ctx.set_params(ssl_params)
32
+ ctx.session_cache_mode = CONTEXT_CACHE_MODE
33
+ ctx.session_new_cb = proc { |_, sess| @new_session = sess }
34
+ end
35
+ end
36
+
37
+ def check_new_session
38
+ time = @new_session.time.to_f + @new_session.timeout
39
+ if Process.clock_gettime(Process::CLOCK_REALTIME) < time
40
+ self.session = @new_session
41
+ end
29
42
  end
30
43
 
31
44
  def connect_with_deadline(deadline, exception)
@@ -33,8 +46,13 @@ class TCPClient
33
46
  end
34
47
 
35
48
  def should_verify?(ssl_params)
36
- ssl_params[:verify_mode] != OpenSSL::SSL::VERIFY_NONE
49
+ ssl_params[:verify_mode] != ::OpenSSL::SSL::VERIFY_NONE &&
50
+ context.verify_hostname
37
51
  end
52
+
53
+ CONTEXT_CACHE_MODE =
54
+ ::OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
55
+ ::OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
38
56
  end
39
57
 
40
58
  private_constant(:SSLSocket)
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
- VERSION = '0.5.1'
4
+ #
5
+ # The current gem version.
6
+ #
7
+ VERSION = '0.9.0'
5
8
  end
data/lib/tcp-client.rb CHANGED
@@ -9,102 +9,272 @@ require_relative 'tcp-client/configuration'
9
9
  require_relative 'tcp-client/default_configuration'
10
10
  require_relative 'tcp-client/version'
11
11
 
12
+ #
13
+ # Client class to communicate with a server via TCP w/o SSL.
14
+ #
15
+ # All connect/read/write actions can be monitored to ensure that all actions
16
+ # terminate before given time limits - or raise an exception.
17
+ #
18
+ # @example - request to Google.com and limit all network interactions to 1.5 seconds
19
+ # TCPClient.with_deadline(1.5, 'www.google.com:443') do |client|
20
+ # client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
21
+ # client.read(12)
22
+ # end
23
+ # # => "HTTP/1.1 200"
24
+ #
25
+ #
12
26
  class TCPClient
13
- def self.open(address, configuration = Configuration.default)
27
+ #
28
+ # Creates a new instance which is connected to the server on the given
29
+ # address and uses the given or the {.default_configuration}.
30
+ #
31
+ # If an optional block is given, then the block's result is returned and the
32
+ # connection will be closed when the block execution ends.
33
+ # This can be used to create an ad-hoc connection which is garanteed to be
34
+ # closed.
35
+ #
36
+ # If no block is giiven the connected client instance is returned.
37
+ # This can be used as a shorthand to create & connect a client.
38
+ #
39
+ # @param address [Address, String, Addrinfo, Integer] the address to connect to, see {Address#initialize} for valid formats
40
+ # @param configuration [Configuration] the {Configuration} to be used for this instance
41
+ #
42
+ # @yieldparam client [TCPClient] the connected client
43
+ # @yieldreturn [Object] any result
44
+ #
45
+ # @return [Object, TCPClient] the block result or the connected client
46
+ #
47
+ # @see #connect
48
+ #
49
+ def self.open(address, configuration = nil)
14
50
  client = new
15
51
  client.connect(Address.new(address), configuration)
16
- return client unless block_given?
17
- begin
18
- yield(client)
19
- ensure
20
- client.close
52
+ block_given? ? yield(client) : client
53
+ ensure
54
+ client.close if block_given?
55
+ end
56
+
57
+ #
58
+ # Yields a new instance which is connected to the server on the given
59
+ # address and uses the given or the {.default_configuration}.
60
+ # It ensures to close the connection when the block execution ends.
61
+ # It also limits all {#read} and {#write} actions within the block to a given
62
+ # time.
63
+ #
64
+ # This can be used to create an ad-hoc connection which is garanteed to be
65
+ # closed and which read/write calls should not last longer than the timeout
66
+ # limit.
67
+ #
68
+ # @param timeout [Numeric] maximum time in seconds for all {#read} and {#write} calls within the block
69
+ # @param address [Address, String, Addrinfo, Integer] the address to connect to, see {Address#initialize} for valid formats
70
+ # @param configuration [Configuration] the {Configuration} to be used for this instance
71
+ #
72
+ # @yieldparam client [TCPClient] the connected client
73
+ # @yieldreturn [Object] any result
74
+ #
75
+ # @return [Object] the block result
76
+ #
77
+ # @see #with_deadline
78
+ #
79
+ def self.with_deadline(timeout, address, configuration = nil)
80
+ client = nil
81
+ raise(NoBlockGivenError) unless block_given?
82
+ address = Address.new(address)
83
+ client = new
84
+ client.with_deadline(timeout) do
85
+ yield(client.connect(address, configuration))
21
86
  end
87
+ ensure
88
+ client&.close
22
89
  end
23
90
 
24
- attr_reader :address, :configuration
91
+ #
92
+ # @return [Address] the address used for this client
93
+ #
94
+ attr_reader :address
25
95
 
26
- def initialize
27
- @socket = @address = @deadline = @configuration = nil
96
+ #
97
+ # @return [Configuration] the configuration used by this client.
98
+ #
99
+ attr_reader :configuration
100
+
101
+ #
102
+ # @attribute [r] closed?
103
+ # @return [Boolean] true when the connection is closed, false when connected
104
+ #
105
+ def closed?
106
+ @socket.nil? || @socket.closed?
28
107
  end
29
108
 
109
+ #
110
+ # @return [String] the currently used address as text.
111
+ #
112
+ # @see Address#to_s
113
+ #
30
114
  def to_s
31
115
  @address&.to_s || ''
32
116
  end
33
117
 
34
- def connect(address, configuration, exception: nil)
35
- raise(NoOpenSSL) if configuration.ssl? && !defined?(SSLSocket)
36
- close
118
+ #
119
+ # Establishes a new connection to a given address.
120
+ #
121
+ # It accepts a connection-specific configuration or uses the global {.default_configuration}. The {#configuration} used by this instance will
122
+ # be a copy of the configuration used for this method call. This allows to
123
+ # configure the behavior per connection.
124
+ #
125
+ # @param address [Address, String, Addrinfo, Integer] the address to connect to, see {Address#initialize} for valid formats
126
+ # @param configuration [Configuration] the {Configuration} to be used for this instance
127
+ # @param timeout [Numeric] maximum time in seconds to read; used to override the configuration's +connect_timeout+.
128
+ # @param exception [Class] exception class to be used when the read timeout reached; used to override the configuration's +connect_timeout_error+.
129
+ #
130
+ # @return [self]
131
+ #
132
+ # @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
133
+ #
134
+ def connect(address, configuration = nil, timeout: nil, exception: nil)
135
+ close if @socket
136
+ raise(NoOpenSSLError) if configuration.ssl? && !defined?(SSLSocket)
37
137
  @address = Address.new(address)
38
- @configuration = configuration.dup.freeze
39
- @socket = create_socket(exception)
138
+ @configuration = (configuration || Configuration.default).dup
139
+ @socket = create_socket(timeout, exception)
40
140
  self
41
141
  end
42
142
 
143
+ #
144
+ # Close the current connection.
145
+ #
146
+ # @return [self]
147
+ #
43
148
  def close
44
149
  @socket&.close
45
150
  self
46
- rescue IOError
151
+ rescue *NETWORK_ERRORS
47
152
  self
48
153
  ensure
49
154
  @socket = @deadline = nil
50
155
  end
51
156
 
52
- def closed?
53
- @socket.nil? || @socket.closed?
54
- end
55
-
157
+ #
158
+ # Executes a block with a given overall timeout.
159
+ #
160
+ # When you like to ensure that a complete read/write communication sequence
161
+ # with the server is finished before a given amount of time you can use this
162
+ # method to define such a deadline.
163
+ #
164
+ # @example - ensure to send a welcome message and receive a 64 byte answer from server
165
+ # answer = client.with_deadline(2.5) do
166
+ # client.write('Helo')
167
+ # client.read(64)
168
+ # end
169
+ #
170
+ # @param timeout [Numeric] maximum time in seconds for all {#read} and {#write} calls within the block
171
+ #
172
+ # @yieldparam client [TCPClient] self
173
+ #
174
+ # @return [Object] result of the given block
175
+ #
176
+ # @raise [NoBlockGivenError] if the block is missing
177
+ #
56
178
  def with_deadline(timeout)
57
179
  previous_deadline = @deadline
58
- raise(NoBlockGiven) unless block_given?
180
+ raise(NoBlockGivenError) unless block_given?
59
181
  @deadline = Deadline.new(timeout)
60
- raise(InvalidDeadLine, timeout) unless @deadline.valid?
182
+ raise(InvalidDeadLineError, timeout) unless @deadline.valid?
61
183
  yield(self)
62
184
  ensure
63
185
  @deadline = previous_deadline
64
186
  end
65
187
 
66
- def read(nbytes, timeout: nil, exception: nil)
67
- 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)
71
- return @socket.read(nbytes) unless deadline.valid?
72
- read_with_deadline(nbytes, deadline, exception)
188
+ #
189
+ # Read the given nbytes or the next available buffer from server.
190
+ #
191
+ # @param nbytes [Integer] the number of bytes to read
192
+ # @param timeout [Numeric] maximum time in seconds to read; used to override the configuration's +read_timeout+.
193
+ # @param exception [Class] exception class to be used when the read timeout reached; used to override the configuration's +read_timeout_error+.
194
+ #
195
+ # @return [String] buffer read
196
+ #
197
+ # @raise [NotConnectedError] if {#connect} was not called before
198
+ #
199
+ def read(nbytes = nil, timeout: nil, exception: nil)
200
+ raise(NotConnectedError) if closed?
201
+ deadline = create_deadline(timeout, configuration.read_timeout)
202
+ return stem_errors { @socket.read(nbytes) } unless deadline.valid?
203
+ exception ||= configuration.read_timeout_error
204
+ stem_errors(exception) do
205
+ @socket.read_with_deadline(nbytes, deadline, exception)
206
+ end
73
207
  end
74
208
 
75
- def write(*msg, timeout: nil, exception: nil)
76
- 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)
80
- return @socket.write(*msg) unless deadline.valid?
81
- write_with_deadline(msg, deadline, exception)
209
+ #
210
+ # Write the given messages to the server.
211
+ #
212
+ # @param messages [Array<String>] messages to write
213
+ # @param timeout [Numeric] maximum time in seconds to read; used to override the configuration's +write_timeout+.
214
+ # @param exception [Class] exception class to be used when the read timeout reached; used to override the configuration's +write_timeout_error+.
215
+ #
216
+ # @return [Integer] bytes written
217
+ #
218
+ # @raise [NotConnectedError] if {#connect} was not called before
219
+ #
220
+ def write(*messages, timeout: nil, exception: nil)
221
+ raise(NotConnectedError) if closed?
222
+ deadline = create_deadline(timeout, configuration.write_timeout)
223
+ return stem_errors { @socket.write(*messages) } unless deadline.valid?
224
+ exception ||= configuration.write_timeout_error
225
+ stem_errors(exception) do
226
+ messages.sum do |chunk|
227
+ @socket.write_with_deadline(chunk.b, deadline, exception)
228
+ end
229
+ end
82
230
  end
83
231
 
232
+ #
233
+ # Flush all internal buffers (write all through).
234
+ #
235
+ # @return [self]
236
+ #
84
237
  def flush
85
- @socket.flush unless closed?
238
+ stem_errors { @socket&.flush }
86
239
  self
87
240
  end
88
241
 
89
242
  private
90
243
 
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)
244
+ def create_deadline(timeout, default)
245
+ timeout.nil? && @deadline ? @deadline : Deadline.new(timeout || default)
97
246
  end
98
247
 
99
- def read_with_deadline(nbytes, deadline, exception)
100
- exception ||= @configuration.read_timeout_error
101
- @socket.read_with_deadline(nbytes, deadline, exception)
248
+ def create_socket(timeout, exception)
249
+ deadline = create_deadline(timeout, configuration.connect_timeout)
250
+ exception ||= configuration.connect_timeout_error
251
+ stem_errors(exception) do
252
+ @socket = TCPSocket.new(address, configuration, deadline, exception)
253
+ return @socket unless configuration.ssl?
254
+ SSLSocket.new(@socket, address, configuration, deadline, exception)
255
+ end
102
256
  end
103
257
 
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
258
+ def stem_errors(except = nil)
259
+ yield
260
+ rescue *NETWORK_ERRORS => e
261
+ raise unless configuration.normalize_network_errors
262
+ (except && e.is_a?(except)) ? raise : raise(NetworkError, e)
109
263
  end
264
+
265
+ NETWORK_ERRORS =
266
+ [
267
+ Errno::EADDRNOTAVAIL,
268
+ Errno::ECONNABORTED,
269
+ Errno::ECONNREFUSED,
270
+ Errno::ECONNRESET,
271
+ Errno::EHOSTUNREACH,
272
+ Errno::EINVAL,
273
+ Errno::ENETUNREACH,
274
+ Errno::EPIPE,
275
+ IOError,
276
+ SocketError
277
+ ].tap do |errors|
278
+ errors << ::OpenSSL::SSL::SSLError if defined?(::OpenSSL::SSL::SSLError)
279
+ end.freeze
110
280
  end
data/rakefile.rb CHANGED
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rake/clean'
4
- require 'rake/testtask'
5
4
  require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+ require 'yard'
6
7
 
7
8
  $stdout.sync = $stderr.sync = true
8
9
 
9
- CLOBBER << 'prj'
10
-
10
+ CLOBBER << 'prj' << 'doc'
11
11
  task(:default) { exec('rake --tasks') }
12
-
13
- Rake::TestTask.new(:test) do |task|
14
- task.pattern = 'test/**/*_test.rb'
15
- task.warning = task.verbose = true
16
- end
12
+ RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
13
+ YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
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