Skip to content

Getting Started

Tony Arcieri edited this page Jun 2, 2017 · 4 revisions

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 raise IO::WaitReadable (or rather, a subclass of Errno::EAGAIN with IO::WaitReadable mixed in)
  • Writing: If the write buffer is full, calling #write_nonblock will raise IO::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

See also