DEV Community

André Diego Piske
André Diego Piske

Posted on

Building my next HTTP server, part 2

This is the second post of a series about my HTTP server

On my first post of this series I explained how the plan for my HTTP server is for it to be asynchronous, so it makes the best use of I/O and CPU at the same time.

In order to be asynchronous, the problem I have to solve is that a call to TCPServer#accept or to TCPServer#read is a blocking call. This means that the method call will block the execution flow until there is something to be read from the other side.

If there are two clients connected to the server and one of the clients gets stuck, it may block the whole server. A single client must never be able to block a whole server!

There are a few technologies that can be used to solve this issue. But all of them have the same basic idea behind them. If we think about a blocking read operation, we can break it down into two pieces. Let's take those pieces to picture what could be the implementation of the TCPServer#read method:

class TCPServer
  def read(length)
    wait_until_bytes_available(length)
    read_available_bytes(length)
  end
end
Enter fullscreen mode Exit fullscreen mode

Of course those two methods being called are fictitious, only for the purpose of illustrating. The idea is that the wait_until_bytes_available method waits until length amount of bytes are available to be read from the wire. This is where the actual blocking occurs, as this method will only return when there is enough bytes to be read. If the client never sends more data, the method would not return. In reality, the method would return with an error state if the connection is broken.

After that waiting, the read_available_bytes method call then does the actual reading. It reads length amount of bytes from the wire without blocking and then returns those bytes.

Now, in order to have it working asynchronously, it's just a matter of removing the waiting part. That is done by removing the wait_until_bytes_available method call. Now we're only left with the actual reading method. And ruby, in fact, has a method just for that. It's the IO#read_nonblock

The #read_nonblock method call takes one argument that is how many bytes should be read at maximum. That is, the method will never read more bytes than what was passed in the argument, but it can read less than it in case there just isn't enough bytes available to be read.

Now, the read method is not the only one that will block. We also have to solve the issue with the accept method. And that is very easy, because Ruby has the TCPServer#accept_nonblock method! Also, which will be needed later, there is the non-blocking counterpart for the write method, which is the, you guessed it, IO#write_nonblock.

Those *_nonblock methods are the way to go from here. But they introduce a lot of other difficulties to be dealt with. For instance, what should be done if there is nothing to read from the wire right now? That doesn't mean there won't be anything in the near future. Also, since the read_nonblock method can read less bytes than specified in its argument, this would mean that the data can be received in small pieces. How to manage those pieces and process them together afterwards?

I intend to cover those issues in the next post of this series, so see you there!

Top comments (0)