Understanding Io.Select When Reading Socket in Ruby

Understanding IO.select when reading socket in Ruby

1) IO.select takes a set of sockets and waits until it's possible to read or write with them (or if error happens). It returns sockets event happened with.

2) array contains sockets that are checked for events. In your case you specify only sockets for reading.

3) IO.select returns an array of arrays of sockets. Element 0 contains sockets you can read from, element 1 - sockets you can write to and element 2 - sockets with errors.

After getting list of sockets you can read the data.

4) yes, recv_nonblock argument is size in byte. Note that size of data actually being read may be less than 1024, in this case you may need to repeat select (if actual data matters for you).

Ruby - Character-wise IO.select

You can set stdin to raw mode (taken from this answer):

begin
state = `stty -g`
`stty raw -echo -icanon isig`
loop do
rd, _, _ = IO.select([$stdin])
handle_keypress(rd[0].getc)
end
ensure
`stty #{state}`
end

IO#getc returns a single character from stdin. Another option is IO#read_nonblock to read all available data.

Socket.read() won't block in Ruby

read will block if there is no data, but not at EOF. The IO#read docs say:

When this method is called at end of file, it returns nil or "", depending on length: read, read(nil), and read(0) return "", read(positive_integer) returns nil.

Since calling read on EOF doesn’t block, select will return the IO as readable straight away.

In your code the first call to read will block until all the data is read from the connection (i.e. the other end has closed it). From then it will be at EOF, so select will return it as ready, and read will return an empty string immediately.

Sockets in Ruby, how can I asynchronously read and write data through a Socket

Based on Casper's recommendation in the question comments, I changed the proc to use IO.select. It worked as a breese.

The first thing to know is that attempting to read the socket (@socket.recv) when there is no data in its buffer will make the socket "hang" waiting for a new message from the server. This has the the annoying side effect of preventing us from writing to the socket (via separated thread).
So to fix this, the IO.select call would try to read the socked for 2 seconds (the 2 used as last parameter below). When the 2s elapse without data, IO.select returns nil which allowed me to not call @socket.recv when we know it would "hang".

A great side effect is that my while true does not kill the client when there is no data, as IO.select takes 2s to timeout on each loop.

This is the end result:

@socket = TCPSocket.new(localhost, 1234)

def read_data_loop
while true
while IO.select([@socket], nil, nil, 2) && (line = @socket.recv(50))
p line
end
end
end

@buffer_thread = Thread.new { read_data_loop }

@socket.write("send me something!!")

sleep(10)
@buffer_thread.exit

This made the background thread write to the console:

server response
hi there

as these were two messages I had set up on the server.

IO#read blocks on non-blocking socket?

You were right to expect the answer "IO#read does not respect the flags set on the underlying file descriptor":

ruby 1.9.3 IO#read

Note that this method behaves like fread() function in C. If you need
the behavior like read(2) system call, consider readpartial,
read_nonblock and sysread.

I appreciate that you're using 1.8, but

  • you're seeing the process stuck in io_fread, and
  • IO#read_nonblock is available in 1.8 as well.

How to read from a TCPServer socket in ruby using read, readpartial and read_nonblock

It's done in unicorn here
https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_request.rb#L69-L71

There is add_parse method(read the comments above methods)
https://github.com/defunkt/unicorn/blob/master/ext/unicorn_http/unicorn_http.rl#L760-L778

Also take a look at some explanations here http://www.ruby-forum.com/topic/2267632#1014288

Here is your working code using http_parser.rb https://gist.github.com/4136962

gem install http_parser.rb

require 'socket'
require "http/parser"

READ_CHUNK = 1024 * 4
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
addr = Socket.pack_sockaddr_in(9799, '127.0.0.1')
socket.bind(addr)
socket.listen(Socket::SOMAXCONN)
socket.setsockopt(:SOCKET, :REUSEADDR, true)

puts "Server is listening on port = 9799"
loop do
connection, addr_info = socket.accept

parser = Http::Parser.new
begin
data = connection.readpartial(READ_CHUNK)
puts "Buffer = #{data}"
parser << data
end until parser.headers

connection.write("HTTP/1.1 200 \r\n")
connection.write("Content-Type: text/html\r\n")
connection.write("Status 200 \r\n")
connection.write("Connection: close \r\n")
connection.write("\r\n\r\n")
connection.write("Hello World \r\n")
connection.close
end


Related Topics



Leave a reply



Submit