Experiences writing an async HTTP server (2 Part Series)
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
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
#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.
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,
*_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!