nio4r is a low-level library which is somewhat difficult to understand. This page will hopefully guide you through the basics of what to do when. This tutorial will assume we're using TCP but the basic principles can apply to any type of socket.
Make a "Reactor" Loop
Nobody's forcing you to make an event loop, but that's generally how people use a nio4r-like API. We might start with something like this:
selector = NIO::Selector.new
# Make some servers and/or new connections here
#...
loop do
# Wait for stuff to happen
selector.select do |monitor|
#...
end
end
Registering Servers
After we've made a server (e.g. TCPServer
) we want to register it with the selector so we know it has a new connection available, after which we want to call #accept
We will always want to wait for servers in the :r
state. This means we've gotten a new connection:
server = TCPServer.new('localhost', 1234)
selector.register(server, :r)
Registering Clients
We want to wait on outgoing connections to be in the :w
state. Until the connection has succeeded, it isn't writable. After it's succeeded it's writable! Make sense?
Since we're doing this inside an event loop, we want to make sure we do a nonblocking connect. Problem: Ruby doesn't provide TCPSocket.connect_nonblock
so we'll have to use the Socket class instead:
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
begin
socket.connect_nonblock Socket.sockaddr_in(remote_port, remote_addr)
rescue Errno::EINPROGRESS
# Ruby's a-tryin' to connect us, we swear!
selector.register(socket, :w)
end
# If we didn't get an exception, we're already connected. Yay!
Running the Loop
Now that we've wired up a selector and are inside the event loop, we'll start getting some events when we call #select
on the selector. For now we're just getting started so the only sockets we'll have registered to the server are either servers waiting for connections or connecting sockets that are waiting to be connected to a remote host.
Here's how we handle each of those cases:
Servers Accepting Connections
When a TCPServer
or other server object selects as #readable?
, it means it has a new connection i.e.:
selector.select do |monitor|
case monitor.io
when TCPServer
if monitor.readable?
# This means our TCPServer has new connections!
client = monitor.io.accept_nonblock
#...
end
end
end
Clients Completing Connections
When a Socket
object we previously called #connect_nonblock
on selects as #writable?
it means something has happened, either we connected successfully or an error occurred. To get the result, we have to call #connect_nonblock
again:
selector.select do |monitor|
case monitor.io
when Socket
if monitor.writable?
begin
socket.connect_nonblock Socket.sockaddr_in(remote_port, remote_addr)
rescue Errno::EISCONN
# SUCCESS! Since Ruby is crazy we discover we're successful via an exception
end
end
end
end
Performing I/O Operations
At this point we either have a TCPSocket
we accepted via TCPServer#accept
or a Socket
we've successfully connected to a remote server via Socket#connect_nonblock
.
What should we do now? Probably register it with the selector right? Nope! No selector required at this point! Instead here's what you do:
- Reading: If you want to read from the socket, call
#read_nonblock
. Perhaps there's already data waiting in the read buffer! If there is, you'll get some data. At that point you're done! - Writing: If you want to write to the socket, call
#write_nonblock
. We have a fresh socket here and its buffer is totally empty, so this is pretty much guaranteed to succeed.
Don't register the socket with the selector right off the bat! You should always try to do the IO operation you care about first. It will probably work!
Using the Selector
Now here's the tricky part: we use the selector for error handling. Specifically, if we do an #*_nonblock
operation, and it would block, that's an error!
- Reading: If the read buffer is empty, calling
#read_nonblock
will raiseIO::WaitReadable
(or rather, a subclass of Errno::EAGAIN with IO::WaitReadable mixed in) - Writing: If the write buffer is full, calling
#write_nonblock
will raiseIO::WaitWritable
(or rather, a subclass of Errno::EAGAIN with IO::WaitWritable mixed in)
Now we need to use the selector! We have a socket that's stuck in some state where we can't make any progress until that state changes. The selector will tell us when it's ready:
read_complete = proc { |data| puts "Got data! #{data}" }
begin
data = socket.read_nonblock(16384)
read_complete.call(data)
rescue IO::WaitReadable
monitor = selector.register(socket, :r)
monitor.value = proc do
data = socket.read_nonblock(16384)
read_complete.call(data)
end
end
Now the next time the selector comes around, we need to call the proc on the monitor to have it fire the read_complete
proc:
selector.select do |monitor|
monitor.value.call
end