Reputation: 4015
I have following (simplified) ruby code, which opens ruby's TCP-Socket and reads from it:
socket = TCPSocket.open(server, port)
while line = socket.gets
line = line.chop
end
Now I want read from the socket just for a given period of time (i.e. 1 minute). So after 1 one minute, the while block should be breaked and the process should exit.
Putting a line like
break if (elapsed_time > 1000)
into the gets
-block is not possible, because if nothing is written to the socket, this line of code is not reached.
Upvotes: 2
Views: 228
Reputation: 114168
To read from an IO in a non-blocking way, there's read_nonblock
. It either reads the available bytes (up to a given maximum) or raise an exception if the IO isn't ready for reading. Upon rescuing the exception, you can call IO.select
to wait for the IO to become ready for reading.
IO.select
takes up to 3 arrays with IOs to be monitored for 1) reading, 2) writing and 3) exceptions (you can monitor many IOs at once). A basic loop could look like this:
buffer = String.new
loop do
begin
buffer << socket.read_nonblock(1024)
rescue IO::WaitReadable
IO.select([socket])
end
end
The above attempts to reads up to 1024 bytes which will be appended to buffer
. If the read succeeds, it will read the next 1024 bytes and so on. If a read fails because the socket doesn't have any data, it calls IO.select
which monitors the socket and returns as soon as more data is available.
IO.select
also takes a 4th argument timeout
. If the given value (in seconds) is exceeded, it will return nil
which can be used to break
the loop conditionally:
loop do
begin
buffer << socket.read_nonblock(1024)
rescue IO::WaitReadable
break unless IO.select([socket], nil, nil, 10)
end
end
The above will wait up to 10 seconds for more data to become available or break the loop otherwise.
However, it will wait for 10 seconds per (failed) read attempt. To get a "global" timeout for the whole loop you might need something along these lines:
timeout = 10
buffer = String.new
t = Time.now
remaining = timeout
while remaining > 0
begin
buffer << socket.read_nonblock(1024)
rescue IO::WaitReadable
break unless IO.select([socket], nil, nil, remaining)
end
elapsed = Time.now - t
remaining = timeout - elapsed
end
The above keeps track of the remaining time (in seconds) and passed that value as a timeout to IO.select
.
Finally, you might want to process the lines as soon as they become available. To do so you could check the string buffer for a newline character and extract the line via slice!
(possibly in a loop since you might have read multiple lines at once). Each extracted line could then be yielded:
def gets_while(io, timeout)
buffer = String.new
t = Time.now
remaining = timeout
while remaining > 0
begin
buffer << io.read_nonblock(1024)
while (newline = buffer.index("\n"))
yield buffer.slice!(0..newline)
end
rescue IO::WaitReadable
break unless IO.select([io], nil, nil, remaining)
rescue EOFError
break # end of stream / connection closed
end
elapsed = Time.now - t
remaining = timeout - elapsed
end
end
Example usage:
socket = TCPSocket.open(server, port)
gets_while(socket, 60) do |line|
l = line.chomp
# ...
end
Upvotes: 2