Building my next HTTP server, part 2
This article was originally published on dev.to.
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
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!